Россия, Звенигород |
Объекты XPCOM
16.6. Практика: сохранение и загрузка заметок NoteTaker
Это практическое занятие посвящено чтению данных с диска и их записи при помощи объектов XPCOM. В нем показано, как работать с механизмами поддержки RDF в составе платформы непосредственно из скриптов JavaScript, не используя шаблонов XUL. Кроме того, в этом разделе продемонстрирован доступ к данным текущего профиля пользователя, что, впрочем, является довольно простой задачей.
В этом разделе мы завершим работу над приложением NoteTaker, добавив к нему механизмы сохранения, удаления и загрузки заметок. Для этого нам нужно создать необходимые источники данных RDF. Чтобы лучше познакомиться с инфраструктурой поддержки RDF, мы будем работать с ними при помощи интерфейсов XPCOM низкого уровня, не используя библиотеки RDFLib. Текущая заметка всегда хранится в виде структуры данных JavaScript; хранилище фактов, связанное с источником данных RDF, используется для хранения данных всей совокупности заметок.
Мы также попытаемся усовершенствовать запросы RDF. Запросы, основанные на шаблонах, имеют ограничения, которые можно преодолеть, работая с запросами из скриптов. До настоящего момента мы не могли найти заметку по данному URL, если только URL заметки не совпадал с образцом полностью. Теперь мы исправим эту ситуацию. Кроме того, текстовое поле для данных заметки в панели инструментов слишком просто для того, чтобы оправдать использование шаблона, и мы реализуем соответствующий запрос в форме скрипта. Фактически, в той или иной мере мы добавим скрипты ко всем используемым шаблонам.
Как подобает при разработке программ, мы начнем с проектирования.
16.6.1. Проектирование источников данных
В "Шаблоны" "Шаблоны" мы сделали содержимое динамическим, используя атрибуты элементов XUL. Для каждого шаблона был определен собственный источник данных. Хотя это простой и удобный способ работы с источниками, он предполагает, что разработчику известен точный URL файла notetaker.rdf на машине пользователя. Поскольку теперь мы решили, что notetaker.rdf будет храниться в составе профиля пользователя, его URL не может быть известен заранее.
Чтобы учесть это изменение, мы перенесем интеграцию с данными RDF из шаблонов XUL в скрипте JavaScript. Вместо указания источника данных в качестве атрибута XUL мы будем работать с ним как с объектом XPCOM в скрипте JavaScript. В этом же скрипте мы можем использовать другие объекты и интерфейсы, чтобы динамически установить местонахождение файла RDF.
При описании шаблонов нам все равно понадобится указать источник данных в коде XUL. Для этого мы используем "нулевой" источник rdf:null, предоставляемый платформой Mozilla. После загрузки шаблонов мы создадим новый источник данных на основе объекта, представляющего URL, и подключим его к каждому из шаблонов с помощью JavaScript. В этом случае весь обмен данными RDF будет управляться единственным объектом-источником.
Это не единственный метод создания источника данных, совместно используемого несколькими шаблонами XUL. Если два шаблона имеют один и тот же атрибут, определяющий источник данных, оба они будут совместно использовать один набор фактов (общее хранилище фактов). В противном случае код, написанный нами в "Шаблоны" , просто не смог бы корректно работать. Поэтому здесь мы всего лишь отделяем использование общего источника данных от XUL, чтобы иметь возможность работать с этим источником в виде отдельного объекта. В принципе, мы могли бы создать отдельный источник данных для каждого шаблона. Даже разные источники, основанные на одном и том же URL, фактически работают с одним общим набором фактов.
Получив объект источника данных, мы можем читать и записывать данные при помощи функций JavaScript, а также многочисленных интерфейсов для работы с RDF. В то же время шаблоны XUL будут получать данные из того же источника, используя внутренние механизмы шаблонов.
Чтобы усовершенствовать нашу объектную модель, мы дополним объект Note объектом NoteDataSource, представляющим источник данных. В последний раз мы работали с объектом Note в практическом разделе "Шаблоны" . Всякий раз, когда нам нужно добавить новую операцию с источником данных, мы можем реализовать ее как метод нашего нового объекта.
16.6.2. Создание источника данных
Прежде всего, следует поместить копию файла notetaker.rdf в папку текущего профиля пользователя.
Необходимое условие успешного создания источника данных – получение доступа к этому файлу. Мы начинаем, зная точное имя файла и имея представление о его местонахождении, а закончить должны, имея объект nsIRDFDataSource. Мы включим в код приложения имя файла, но не путь к нему. Чтобы создать источник данных, мы используем некоторые приемы, описанные в этой лекции.
Чтобы обнаружить файл независимо от платформы, мы используем службу каталогов Mozilla. Изучая таблицы псевдонимов, приведенные выше в этой лекции, мы обнаруживаем, что псевдоним ProfD, приведенный в таблице 16.18, позволяет получить доступ к папке текущего профиля. На основе этого псевдонима мы создадим объект nsIFile, представляющий папку профиля, дополним путь к папке, чтобы получить нужный файл, преобразуем файловый объект в URL и наконец создадим источник данных на основе этого URL. Все эти операции представлены в листинге 16.9.
var Cc = Components.classes; var Ci = Components.interfaces; // Объект сеанса работы с NoteTaker function NoteSession() { this.init(); } NoteSession.prototype = { config_file : "notetaker.rdf", datasource : null, init : function (otherfile) { var fdir, conv, rdf, file, url; if (otherfile) this.config_file = otherfile; with (window) { fdir = Cc["@mozilla.org/file/directory_service;1"]; fdir = fdir.getService(Ci.nsIProperties); conv = Cc["@mozilla.org/network/protocol;1?name=file"]; conv = conv.createInstance(Ci.nsIFileProtocolHandler); rdf = Cc["@mozilla.org/rdf/rdf-service;1"]; rdf = rdf.getService(Ci.nsIRDFService); } file = fdir.get("ProfD", Ci.nsIFile); file.append(this.config_file); if (!file.exists()) throw this.config_file + " is missing"; if (!file.isFile() || !file.isWritable() || !file.isReadable()) throw this.config_file + " has type or permission problems"; url = conv.newFileURI(file); this.datasource = rdf.GetDataSource(url.spec); } }; var noteSession = new NoteSession();Листинг 16.9. Обнаружение и инициализация локального источника данных.
Вся работа выполняется в методе init() объекта NoteSession. Сначала мы создаем три объекта XCOM. Затем мы получаем папку текущего профиля в виде объекта nsIFile. Метод append(), не возвращающий никакого результата, изменяет этот объект так, что он в точности соответствует нашему конфигурационному файлу. Затем мы убеждаемся, что нужный файл существует, и что он доступен для чтения и записи. Предполагается, что в нашем случае такой файл существует всегда, поскольку он будет создаваться при установке приложения. Однако к реальному приложению все же следовало бы добавить код, автоматически создающий файл в случае его отсутствия – нельзя исключить возможность случайного удаления файла. Затем мы преобразуем nsIFile в nsIURL с помощью метода newFileURI(), получаем URL в виде строки, используя свойство spec, и, наконец, создаем источник данных на основе этой строки при помощи метода GetDataSource().
Эта последовательность шагов – стандартные действия при подготовке источников данных. При использовании внутреннего или удаленного источника действия могут несколько отличаться от приведенного примера. Так, если URL источника данных известен заранее, вся процедура может свестись к вызову метода GetDataSource().
16.6.3. Динамическое подключение источников данных к шаблонам
Теперь, когда мы получили источник данных, давайте используем его. Мы хотим модифицировать существующие шаблоны так, чтобы они получали данные от созданного нами объекта, а не из файла, указанного в коде шаблона. Поэтому в коде шаблона мы укажем "нулевой" источник данных datasources="rdf:null", а реальный источник подключим при помощи скрипта.
16.6.3.1. Модификация панели инструментов
Мы полностью отказываемся от использования шаблона для текстового поля <textbox> в панели инструментов NoteTaker. Это слишком сложное решение для простого текстового поля. В предыдущих лекциях мы использовали его с единственной целью: продемонстрировать один из возможных способов работы с шаблонами. Однако шаблоны не являются универсальным инструментом. Итак, код для текстового поля принимает следующий вид:
<textbox id="notetaker-toolbar.summary"/>
Поле заполняется с помощью функции refresh_toolbar(), которая копирует нужное значение из объекта заметки. Таким образом, задача обновления текстового поля решена без обращения к шаблонам.
Раскрывающийся список ключевых слов в панели инструментов основан на стандартном использовании шаблона, и мы могли бы оставить его без изменений, если бы источник данных можно было указать в коде XUL. Однако, поскольку теперь местонахождение файла заметок нам заранее неизвестно, мы должны изменить код XUL и JavaScript.
Содержимое этого списка генерировалось на основе данных, начиная с "Шаблоны" "Шаблоны", однако оно было "недостаточно динамическим". Список заполнялся в момент создания страницы XUL и с этого момента оставался неизменным. Теперь он должен обновляться всякий раз при добавлении нового ключевого слова. Любое добавление или удаление содержимого XUL может вызвать полную перерисовку документа, включая список. Перерисовка выполняется автоматически, однако для сложных тегов, подобных <menulist>, она может выполняться неправильно. Необходимо использовать XUL аккуратно, иначе список после перерисовки будет выглядеть некорректно.
Чтобы отслеживать ошибки при перерисовке, обратимся к коду для тега <menulist> и шаблона, который представлен в листинге 16.10. В качестве источника данных в этом коде указан "rdf:null".
<menulist id="notetaker-toolbar.keywords" editable="true"> <menupopup datasources="rdf:null" ref="urn:notetaker:keywords"> <template> <menuitem uri="rdf:*" label="rdf:http://www.mozilla.org/notetaker-rdf#label"/> </template> </menupopup> </menulist>Листинг 16.10. Тег <menupopup> приложения NoteTaker до добавления динамической перерисовки
В данном случае в состав шаблона входят только теги <menuitem>. Если источником данных является "rdf:null", код XUL, полученный в результате обработки шаблона, будет иметь следующий вид:
<menulist id="notetaker-toolbar.keywords" editable="true"> <menupopup datsources="rdf:null" ref="urn:notetaker:keywords"> </menupopup> </menulist>
Панель инструментов, построенная на основе такого кода, показана на рисунке 16.2.
Этот интерфейс имеет существенные недостатки, как в части отображения, так и в части взаимодействия с пользователем. Мы могли бы проигнорировать эти проблемы, понадеявшись на то, что при отображении документа (событие onload ) к нему будет подключен созданный нами источник данных, на основе которого будут созданы элементы <menuitem> для списка.
К сожалению, дела обстоят не так хорошо. Размер содержимого списка определяется тегом <menupopup>, в состав которого входит фрейм. С момента создания размер этого фрейма не изменяется динамически, хотя в последующих версиях платформы ситуация может измениться. Это означает, что после первоначального отображения раскрывающегося списка его размер не будет изменяться, несмотря на изменение шаблона, определяющего содержимое списка.
Усовершенствованный вариант кода, позволяющий обойти эту проблему, представлен в листинге 16.11:
<menulist id="notetaker-toolbar.keywords" editable="true" datasources="rdf:null" ref="urn:notetaker:keywords" > <template> <menupopup> <menuitem uri="rdf:*" label="rdf:http://www.mozilla.org/notetaker-rdf#label"/> </menupopup> </template> </menulist>Листинг 16.11. Тег <menupopup>, обеспечивающий динамическую перерисовку.
В этом варианте тег <template> поднят на один уровень иерархии, так что тег <menupopup> оказался вложенным в него. В результате <menupopup> будет генерироваться заново при каждом обновлении шаблона. При этом будет создаваться лишь одна пара тегов, поскольку <menupopup> находится снаружи тега, в котором указан источник данных (атрибут uri ). Вспомним, что при обработке шаблона именно тег, имеющий атрибут uri, вместе со своим содержимым создается многократно – по числу элементов, возвращаемых в результате запроса. Поскольку теперь <menupopup> генерируется заново при каждом обновлении содержимого списка, раскрывающийся список будет иметь корректный размер. Это рекомендуемый подход для создания раскрывающихся списков на основе шаблонов, если содержимое списка может динамически изменяться после первоначального отображения.
Даже с учетом сделанных исправлений возможна еще одна проблема с отображением раскрывающегося списка, создаваемого на основе шаблона, хотя эта проблема не затрагивает нашего приложения.
На рисунке 16.3 показана тестовая панель до и после однократного щелчка по кнопке, вызывающей раскрытие списка, – сверху и снизу соответственно.
В этом примере текстовое поле в верхней части раскрывающегося списка первоначально имеет ширину по умолчанию для тега <textbox>. При щелчке по кнопке раскрывается список с элементами, и текстовое поле перерисовывается заново с новой шириной, которая определяется самым широким элементом списка. В результате ширина элемента управления скачкообразно изменяется, что является недостатком пользовательского интерфейса. Чтобы решить эту проблему, достаточно в явном виде указать ширину (атрибут width ) для тега <menulist>. К счастью, эта проблема не затрагивает NoteTaker, по крайней мере, при отображении реальных Web-страниц.
Необходимые изменения кода JavaScript очень просты. В функциях refresh_toolbar() и init_toolbar() следует подключить источник данных к шаблону раскрывающегося списка. Эти изменения представлены в листинге 16.12.
// слушатели onload работают в фазе перехвата window.addEventListener("load", init_handler, true); // загрузить содержимое RDF для панели инструментов. Используется объект заметки (note). function init_toolbar(origin) { if ( origin != "timed" ) { // избежать выполнения внутри любого обработчика onload setTimeout("init_toolbar('timed')",1); } else { var menu = window.document.getElementById('notetakertoolbar.keywords'); menu.database.AddDataSource(noteSession.datasource); menu.ref = 'urn:notetaker:keywords'; setInterval("content_poll()", 1000); } } // обновить панель инструментов на основе текущей заметки. function refresh_toolbar() { var box = document.getElementById('notetaker-toolbar.summary'); box.value = note.summary; var menu = document.getElementById('notetaker-toolbar.keywords'); menu.ref = 'urn:notetaker:keywords'; }Листинг 16.12. Изменение кода панели инструментов NoteTaker с учетом динамического подключения источников данных.
Варианты этих функций, приведенные в практическом разделе "Шаблоны" "Шаблоны", вели себя беспокойно, вызывая rebuild() при малейшем изменении данных шаблона. В данном случае, однако, это излишне, поскольку шаблоны основаны на источнике данных типа xml-datasource, который сам инициирует все необходимые действия при изменении шаблона. Однако если при разработке собственного приложения вы не знаете точно, стоит ли вызвать rebuild(), всегда вызывайте эту функцию.
Обновленная функция init_toolbar(), приведенная в листинге, подключает источник данных к шаблону раскрывающегося списка, обновляет свойство ref шаблона, а также обеспечивает периодическое выполнение функции content_poll(), которая следит за изменениями URL содержимого браузера. Даже если значение свойства ref в результате присваивания не изменилось, выполняется запрос к источнику данных и список строится заново на основе результатов запроса.
Вызов setTimeout(), как и раньше, представляет собой меру предосторожности на случай непредвиденных проблем в обработчике события onload. Функцию refresh_toolbar() можно сравнить с методом Refresh() интерфейса nsIRDFRemoteDataSource. Этот метод обновляет хранилище фактов, лежащее в основе источника данных. Функция refresh_toolbar() обновляет только содержимое XUL, включая содержимое, основанное на шаблоне.
На этом мы завершаем обсуждение изменений в коде панели инструментов, относящихся к отображению данных. Мы вернемся к панели инструментов, когда речь пойдет об обработке данных, вводимых пользователем.
16.6.3.2 Модификация диалогового окна Edit
Диалоговое окно Edit (окно редактирования заметки) – еще одна часть приложения NoteTaker, использующая шаблоны. Источники данных для этих шаблонов также должны динамически подключаться с использованием скриптов JavaScript. Панель Edit (Правка) диалогового окна не содержит никаких шаблонов. Панель Keyword (Ключевые слова) использует шаблоны для заполнения двух элементов – <listbox> и <tree>.
Процедура подключения источников данных к этим шаблонам очень похожа на использованную для панели инструментов. Мы заменяем datasources="notetaker.rdf" на datasources="rdf:null" в двух местах документа editDialog.xul. Затем мы создаем новую функцию init_dialog() в файле dialog_action.js и модифицируем функцию refresh_dialog().
Результаты модификации скриптов приведены в листинге 16.13.
window.addEventListener("load", init_dialog, "true"); function init_dialog() { if ( origin != "timed" ) { // избежать выполнения внутри любого обработчика onload setTimeout("init_dialog('timed')",1); } else { var listbox = document.getElementById('notetaker.keywords'); listbox.database.AddDataSource(window.opener.noteSession.datasource); var tree = document.getElementById('notetaker.related'); tree.database.AddDataSource(window.opener.noteSession.datasource); refresh_dialog(); } } function refresh_dialog() { var listbox = document.getElementById('dialog.keywords'); listbox.ref = window.opener.note.url; //listbox.ref = "http://saturn/test1.html"; // для тестирования var tree = document.getElementById('dialog.related'); tree.ref = window.opener.note.url; //tree.ref = "http://saturn/test1.html"; // для тестирования }Листинг 16.13. Изменение кода диалогового окна NoteTaker с учетом динамического подключения источников данных.
Функция init_dialog(), добавляющая один источник данных к двум шаблонам, практически идентична функции init_toolbar(). Функция refresh_dialog() также аналогична функции refresh_toolbar() и для целей тестирования содержит некоторые URL, находящиеся в файле notetaker.rdf. Все эти изменения не затрагивают пользовательского интерфейса, они лишь позволяют работать с файлом notetaker.rdf, местонахождение которого определяется динамически.
В практическом разделе "Списки и Деревья" "Списки и деревья" мы экспериментировали с динамическим заполнением списков при помощи интерфейсов DOM. Решение этой задачи потребовало около 30 строк JavaScript. В этой лекции мы достигли того же результата при помощи шаблона и всего нескольких строк кода.
В результате всех этих изменений все шаблоны NoteTaker могут работать с файлом notetaker.rdf, находящимся в папке профиля пользователя, местонахождение которой в момент разработки приложения неизвестно.