Опубликован: 10.12.2007 | Доступ: свободный | Студентов: 822 / 20 | Оценка: 5.00 / 5.00 | Длительность: 58:33:00
Лекция 13:

Списки и Деревья

13.5.3. Списки и деревья под управлением данных

Теперь мы создали блок диалога так, что основная его нормальная функциональность реализована, но начальные ключевые слова ограничены статически определенными тегами XUL. Наша конечная цель - заполнять этот контент из RDF, но для краткого эксперимента мы воспользуемся JavaScript.

Достаточной причиной для такого эксперимента служит проблема связанных слов. Если A родственник B, то B тоже, конечно, родственник A. Хотя иерархический XML - не лучшее средство для представления таких двунаправленных связей, мы хотим, чтобы наш виджет дерево показывал связи в обоих направлениях. Картинка дерева по-прежнему будет организована иерархически (иначе и не может быть), но наши предварительные преобразования позволят заполнить виджет более качественно организованной информацией. До сих пор для нас единственным средством заполнить дерево информацией остаются DOM операции нижнего уровня. Это иногда называют динамическим списком.

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

Существует несколько способов сделать это. Например, библиотеки JavaScript в cview и DOM Inspector tools имеют такие объекты JavaScript, чтобы можно было легко разрабатывать снимки дерева для конкретного приложения. Некоторые древоподобные данные, такие как данные IMAP и SNMP, накладывают ограничения на производительность обработки или функциональность, что необходимо учитывать при реализации. Но здесь мы сталкиваемся с чистым, базовым случаем.

В терминах подхода MVC, "Контроллер Модель-Представление", мы реализуем MVC-модель как простую JavaScript структуру данных и для <tree> и для <listbox>. MVC-Представление будет построено как (a) базовая система рендеринга Mozilla, (b) DOM иерархия и (c) специальные блочные объекты для тегов <listbox> и <tree>. Для тега <listbox> стандартный DOM интерфейс - это плоть и кровь того, на чем будет основано наше MVC-представление. В случае дерева нам нужно лишь создать конкретный снимок, чтобы реализовать MVC-представление. Наконец, MVC для списка нам предстоит создать, в то время как для дерева это уже существующий конструктор, так что нам ничего не придется здесь делать вообще.

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

// window.addEventListener("load",init_handlers, true)

Также нужно удалить существующий статический контент для тегов <listbox> и <tree> в XUL документе. Эти теги уменьшатся до вот такого XUL фрагмента:

<listbox id="dialog.keywords" rows="3"/> 
<tree id="dialog.related" hidecolumnpicker="true" seltype="single" flex="1">
  <treecols>
    <treecol id="tree.all" hideheader="true" flex="1" primary="true"/ >
  </treecols> 
  <treechildren flex="1"/> 
</tree>

Как обычно, нам нужен некий инициализационный код, чтобы все установить. Этот код показан в Листинге 13.13.

var listdata = ["checkpointed", "reviewed", "fun", "visual" ]; 
var treedata = [ 
  [ "checkpointed", "breakdown" ], 
  [ "checkpointed", "first draft" ], 
  [ "checkpointed", "final" ], 
  [ "reviewed", "guru" ], 
  [ "reviewed", "rubbish" ], 
  [ "fun", "cool" ], 
  [ "guru", "cool" ]
];

function init_views() { 
  var listbox = document.getElementById("dialog.keywords"); 
  var tree = document.getElementById("dialog.related"); 
  listbox.myview = new dynamicListBoxView(); 
  listbox.mybuilder = new dynamicListBoxBuilder(listbox); 
  listbox.mybuilder.rebuild(); 
  tree.view = new customTreeView(tree); 
}

window.addEventListener("load",init_views, true);
Листинг 13.13. Инициализация виджета ключевых слов под управлением данных.

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

<- "checkpoint", related, "breakdown" ->

А это, заметим, потихоньку ведет нас в сторону RDF. Тем не менее, в этом примере RDF еще нет.

Функция init_views() создает три пользовательских JavaScript объекта - два для списка и один для дерева. Для дерева эдесь дела нет, потому что оно имеет встроенный конструктор, который вызывается автоматически, когда тег <tree> показывает картинку. Список тоже имеет конструктор, но он не будет доступен для программиста приложения, до тех пор пока мы не узнаем про шаблоны. Мы использовали свойства myview и mybuilder, чтобы подчеркнуть, что в случае списка мы ничего не перезаписываем. Этот инициализационный этап выполняется в тот момент, когда документ загружается, и нет никаких обработчиков событий, которые стартуют позже; см., однако, раздел "Пользовательский снимок дерева".

13.5.3.1. Динамический список

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

Первый объект, показанный в листинге 13.14 - это снимок объекта "список".

function dynamicListBoxView() {}

dynamicListBoxView.prototype = { 
  get rowCount () { 
    return listdata.length; 
  }, 
  getItemText: function (index) { 
    return listdata[index]; 
  } 
};
Листинг 13.14. JavaScript-объект для снимка динамического списка.

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

Сам по себе снимок ничего не делает. Его использует конструктор. Конструктор описывается в Листинге 13.15.

function dynamicListBoxBuilder(listbox) {
  this._listbox = listbox; 
}
dynamicListBoxBuilder.prototype = { 
  _listbox : null, 
  rebuild : function () { 
    var rows, item; 
    while (_listbox.hasChildNodes())
      _listbox.removeChild(_listbox.lastChild); 
    rows = _listbox.myview.rowCount; 
    for (var i=0; i < rows; i++) { 
      item = document.createElement("listitem"); 
      item.setAttribute("label", 
      listbox.myview.getItemText(i)); 
      _listbox.appendChild(item); 
    } 
  } 
}
Листинг 13.15. JavaScript-конструктор для динамического списка.

Конструктор так же прост - он содержит всего один метод: rebuild(). Когда вызывается rebuild(), он обрабатывает DOM 1 Core Element объект. Сначала он уничтожает все существующие строчки; затем вновь наполняет его списком обновленных строк. Здесь мы предполагали, что тег <listbox> имеет только дочерние теги <listitem> и не имеет заголовков колонок. В нашем случае это верно. И вновь заметим, что удобно, но не необходимо, чтобы каждая высвечиваемая строчка соответствовала в точности одному DOM элементу (элементу тега <listbox> ).

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

И снимок, и конструктор можно улучшить так, чтобы список можно было изменять после того, как он создан. В нашей реализации обновление списка требует, чтобы (a) массив был бы изменен и затем (b) вновь была бы вызвана функция rebuild() конструктора. Каждое такое изменение перерисовывает список с нуля. Чтобы сделать частичное изменение, в объект "конструктор" следует внести дополнительные методы и создать целую систему, чтобы конструктор знал, когда, куда и какие изменения вносить.

13.5.3.2. Пользовательский снимок дерева

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

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

function customTreeView(tree) { 
  this.calculate(); 
  this._tree = tree; 
}
customTreeView.prototype = {  
  // 1. application specific properties and
  methods calculate : function () { ... }, ... 
  // 2. Important nsITreeView features 
  getCellText : function (index) { ... }, ... 
  // 3. Unimportant nsITreeView features 
  getImageSrc : function (index) {... }, 
  ... 
}
Листинг 13.16. Прототип JavaScript-объекта снимок дерева

Произведем анализ этого объекта. Конструктор customTreeVew() создается специально для нашего приложения. Он обрабатывает некоторую внутреннюю информацию и обеспечивает связь с объектом <tree>. Это просто подготовительная работа. Прототип объекта содержит все стандартные свойства и методы. Мы вольны добавить все, что нам нужно, поскольку интерфейс nsITreeView у нас также реализован. Часть 1 прототипа - это добавочные свойства, о которых конструктор ничего не знает и которые не использует. Часть 2 - порция интерфейса nsITreeView. Этот интерфейс имеет множество методов для работы с контентом дерева: drag and drop, стили, специализированный контент наподобие счетчиков и так далее. Некоторые аспекты интерфейса критически важны, либо для конструктора, либо для наших целей - и эту часть интерфейса мы реализуем соответствующим образом. Часть 3 - оставшаяся часть nsITreeView. Для этой части мы напишем заглушки, не делающие ничего или почти ничего.

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

Давайте теперь займемся этими тремя частями нашего объекта "снимок".

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

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

Ниже описана наша тактика реализации этих структур данных.

Создать структуру данных relatedMatrix, которая представляет все пары слово-слово. С ней будет удобнее работать, чем с простым списком, который мы имеем в начале.

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

Создать структуру viewMap. Это массив, связывающий индекс строк дерева со структурой openTree. Эти структуры приведены в Листинге 13.17, который соответствует части первой нашего прототипа снимка дерева.

_relatedMatrix : null, 
_openTree : null, 
_viewMap : null, 

calculate : function () { 
  this.calcRelatedMatrix(); 
  this.calcTopOfTree();
  this.calcViewMap(); 
}, 
calcRelatedMatrix : function () {
  this._relatedMatrix = {}; 
  var i = 0, r = this._relatedMatrix; 
  while (i < treedata.length ) { 
    if ( ! (treedata[i][0] in r) )
      r[treedata[i][0]] = {}; 
    if ( ! (treedata[i][1] in r) )
      r[treedata[i][1]] = {}; 
      r[treedata[i][0]][treedata[i][1]] = true;
      r[treedata[i][1]][treedata[i][0]] = true; 
      i++; 
    } 
  }, 
calcTopOfTree : function () { 
  var i; 
  this._openTree = []; 
  for (i=0; i < listdata.length; i++) { 
    this._openTree[i] = { container : false, 
          open : false, 
          keyword : listdata[i], 
          kids : null, 
          level : 0 }; 
    if (listdata[i] in this._relatedMatrix ) 
      this._openTree[i].container = true; 
  } 
}, 
calcViewMap : function () { 
  this._viewMap = [];
  this.calcViewMapTreeWalker(this._openTree, 0); 
},
calcViewMapTreeWalker : function(kids, level) { 
  for (var i=0; i < kids.length; i++ ) { 
    this._viewMap.push(kids[i]); 
  if (kids[i].container == true && kids[i].open == true )
    this.calcViewMapTreeWalker(kids[i].kids, level + 1); 
  } 
},
Листинг 13.17. Специфичная для NoteTaker часть объекта "снимок"

Здесь много кода, но он ясный. Сначала три свойства, все они отмечены подчеркиванием, чтобы показать, что их не должны касаться пользователи объекта. Они должны содержать внутренние структуры данных снимка. Затем метод calculate(), который строит эти структуры данных, используя свой метод для каждой структуры. Остальное - три конкретные метода.

Функция calcRelatedMatrix() дает лучше организованную копию исходных данных. Для данных из Листинга 13.13 она предоставляет структуру Листинга 13.18.

_relatedMatrix = { 
  "checkpointed" : { 
    "breakdown" : true, 
    "first draft" : true, 
    "final" : true 
  }, 
    "reviewed" : { 
    "guru" : true,
    "rubbish" : true 
  },
  "fun" : { "cool" : true }, 
  "guru" : { "cool" : true, 
  "reviewed" : true 
  },
  "breakdown" : { "checkpointed" : true },
  "first draft" : { "checkpointed" : true },
  "final" : {"checkpointed" : true }, 
  "rubbish" : { "guru" : true }, 
  "cool" : { "fun" : true, 
    "cool" : true }
};
Листинг 13.18. Пример матрицы keyword-to-keyword

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

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

В корне структуры - calcTopOfTree(). Структура описана в Листинге 13.19:

calcTopOfTree : function () { 
  var i; this._openTree = []; 
  for (i=0; i < listdata.length; i++) { 
    this._openTree[i] = { 
    container : false, 
    open : false, 
    keyword : listdata[i], 
    kids : null, 
    level : 0 
  }; 
  if ( listdata[i] in this._relatedMatrix ) 
    this._openTree[i].container = true; 
  } 
},
Листинг 13.19. Создание корневого узла в иерархическом дереве данных:

Этот метод создает массив записей ключевых слов как узел верхнего уровня и помещает в него слова из списка, которые являются массивом listdata. Таким образом, ключевые слова списка занимают верхний уровень дерева на экране. Каждая запись ключевого слова является объектом, содержащим строку ключевого слова, ссылку на непосредственные дочерние объекты и некоторую служебную информацию. Служебная информация - это: является ли элемент контейнером ( container="true" в записи XUL), открыт ли данный контейнер ( open="true" в записи XUL), какова глубина расположения данного узла в дереве. Когда настанет соответствующее время, другие методы снимка будут добавлены к этому дереву.

Наконец, нам нужно иметь проиндексированный список строк, видимых в дереве. В Листинге 13.20 показано, как это делается.

calcViewMap : function () { 
  this._viewMap =[]; 
  this.calcViewMapTreeWalker(this._openTree, 0); 
},
calcViewMapTreeWalker : function(kids, level) { 
  for (var i=0; i < kids.length; i++ ) {   
    this._viewMap.push(kids[i]); 
  if (kids[i].container == true && kids[i].open == true )
    this.calcViewMapTreeWalker(kids[i].kids, level + 1); 
  } 
},
Листинг 13.20. Создание корневой ноды в иерархии данных дерева

Метод calcViewMap() - стартовая точка в конструировании этого списка. Он передает корень иерархии дерева рекурсивной функции calcViewMapTreeWalker(). Она проходит по раскрытой части дерева слева направо, что то же самое, что сверху вниз для тега <tree>. Для каждого найденного ключевого слова она добавляет ссылку на это слово в индексируемый список элементов. Таки образом каждая запись ключевого слова отслеживается и собственно деревом, и списком.

Эти утилиты создают структуры данных, которые пока не использовались. Такие структуры существуют внутри объекта "снимок". Структура relatedMatrix статична, до тех пор, пока не будет пересоздана заново. Остальные две структуры изменяются с течением времени.

Теперь обратимся к важным частям интерфейса nsITreeView. В Листинге 13.21 показано большинство этих методов. Встроенный конструктор дерева вызывает эти методы, когда ему нужно получить доступ к данным, предоставляемым снимком. Если нам понадобится позже какой-либо скрипт, он также сможет использовать данные методы.

get rowCount() { 
  return this._viewMap.length; 
}, 
getCellText: function(row, column) { 
  return this._viewMap[row].keyword; 
}, 
isContainer: function(index) { 
  return this._viewMap[index].container; 
},
isContainerOpen: function(index) { 
  return this._viewMap[index].open;
}, 
isContainerEmpty: function(index) { 
  var item = this._viewMap[index]; 
  if ( ! item.container ) 
    return false; 
    return ( item.kids.length == 0); // empty? 
}, 
getLevel: function(index) {
  return this._viewMap[index].level; 
}, 
getParentIndex: function(index) { 
  var level = this._viewMap[index].level; 
  while ( --index >= 0 ) 
    if (this._viewMap[index].level < level ) 
      return index; 
    return -1; 
},
hasNextSibling: function(index, after) { 
  var level = this._viewMap[index].level; 
  while ( ++index < this._viewMap.length ) {
    if ( this._viewMap[index].level < level ) 
      return false; 
    if (this._viewMap[index].level == level && index > after ) 
      return true; 
    }
  return false; 
},
Листинг 13.21. Наиболее важные методы интерфейса nsITreeView.

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

Последний важный метод - toggleOpenState(). Он вызывается, когда пользователь открывает или закрывает поддерево. Это действие сложное, потому что изменяется число строк в списке видимых строк дерева. Реализация toggleOpenState() должна обновлять структуру данных снимка, когда это случается, а также должна предписывать нижележащей системе отображения виджета обновить, или перерисовать дерево. Если хоть что- то из этого не реализовать, программный снимок перестанет соответствовать видимому изображению. В Листинге 13.22 приведен код данного метода.

toggleOpenState: function(index) { 
  var i = 0; 
  var node = this._viewMap[index]; 
  if ( !node.container ) 
    return; 
  if ( node.open ) { 
    node.open = false;
    node.kids = null; 
    i = index + 1; 
    while ( this._viewMap[index].level > this._viewMap[i].level) 
      i++; 
    i = i - index; 
  } 
  else { 
    node.open = true;
    node.kids = []; 
    for (var key in this._relatedMatrix[node.keyword]) {
      node.kids[i] = { 
      container : false, 
      open : false, 
      keyword : key, 
      kids : null, 
      level : node.level + 1 
      }; 
    if (typeof(this._relatedMatrix[key]) != "undefined" )
      node.kids[i].container = true; i++; 
    } 
  } 
  this.calcViewMap();
  this._tree.treeBoxObject.rowCountChanged(index,i); 
},
Листинг 13.22. Важный метод toggleOpenState() интерфейса nsITreeView

Функция завершает работу, если рассматриваемый элемент не является контейнером; в противном случае она умеет открыть закрытое поддерево и закрыть открытое. В одних случаях она выполняет следующие задачи: обновляет запись ключевого слова так, чтобы та соответствовала новому состоянию, обновляет иерархию openTree, убирая лишние или добавляя дочерние записи, вычисляет число удаляемых или добавляемых строк с помощью viewMap. После каждой операции viewMap пересчитывается целиком, чтобы сохранить соответствие программного снимка видимому снимку экрана. Последний шаг - команда дереву обновить изображение на экране. Чтобы этого добиться, нужно отдать команду специальному объекту дерева - box. Обычно мы используем этот объект лишь для скролирования, но его метод rowCountChanged() (принадлежащий интерфейсу nsITreeBoxObject ) - в точности то, что нам нужно. Он требует массив строк ( index ) и число строк из этого массива как параметры, описывающие ту часть дерева, которую необходимо перерисовать.

В простых деревьях, когда мы сами пишем код снимка, как в данном случае, единственный способ динамически изменять список видимых в настоящее время строк - открывать и закрывать поддеревья. Если же добавить дереву обработчики событий, они также смогут создавать динамические изменения. Например, клик мышкой по строке, который удаляет эту строку. Чтобы это реализовать, конструктор дерева должен манипулировать структурами данных снимка и вызывать методы calcViewMap() и rowCountChange() так же, как это делает метод toggleOpenView(). Самый очевидный способ это осуществить - добавить соответствующие методы снимку, выполняющие эту работу, и затем вызывать их из обработчика события.

Описав специфичные для нашего приложения методы программного снимка и важные методы nsITreeView, обратимся к менее важным. Для другого приложения они могут быть критически важны, но не в нашем случае. В Листинге 13.23 приведена их реализация.

canDropBeforeAfter: function(index, before) { return false; }, 
canDropOn: function(index) { return false; }, 
cycleCell: function(row, column) {}, 
cycleHeader: function(col, elem) {}, 
drop: function(row, orientation) { return false; }, 
getCellProperties: function(row, prop) {}, 
getCellValue: function(row, column) {}, 
getColumnProperties: function(column, elem, prop) {}, 
getImageSrc: function(row, column) {}, 
getProgressMode: function(row, column) {}, 
getRowProperties: function(row, column, prop) {}, 
isEditable: function(row, column) { return false; },
isSeparator: function(index) { return false; }, 
isSorted: function() { return false; }, 
performAction: function(action) {},
performActionOnCell: function(action, row, column) {},
performActionOnRow: function(action, row) {}, 
selectionChanged: function() {}, 
setCellText: function(row, column, value) {}, 
setTree: function(tree) {} 
}; // end of customTreeView.prototype
Листинг 13.23. Менее важные методы nsITreeView

Эти методы либо говорят "Нет", либо не делают ничего. Наш снимок не поддерживает drag-and-drop в дереве. Нет свойств ячеек, строк, колонок, нет значений ячеек, нет изображений, счетчиков состояния, редактируемых полей, либо каких-то действий с полями. У нас нет сортируемых колонок или тегов <treeseparator>, и мы не обращаем внимания на то, что пользователь может высветить строку. Метод setTree() запускается во время инициализации дерева, и больше нас не интересует, хотя мы и могли бы вызвать calculate() в этом методе, если бы захотели. Мы просто вызываем calculate(), когда создается снимок.

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

Первый результат очевиден: с помощью снимка и JavaScript мы можем базировать дерево на отличном от XML контенте. Это может быть предпочтительно для манипулирования DOM, особенно в случае не- иерархических данных.

Второй результат не столь очевиден. Программный снимок, сконструированный нами, имеет бесконечный размер. Если слово A связано с B, то и B тоже, очевидно, связано с A, так что открытие поддерева на первом слове ведет к открытию поддерева на втором и наоборот. Ясно, что подобные бесконечные деревья не могут быть созданы средствами статического XUL. В следующей лекции мы увидим, что построенные лишь частично (а, значит, возможно, и бесконечные) деревья - обычное свойство системы шаблонов.

Последний результат: мы увидели кое-что из внутренней механики тега <tree>. Платформа предоставляет для этого тега некоторые интерфейсы и структуры (такие как конструктор дерева) и на эту механику очень рекомендуется взглянуть.

На этом мы заканчиваем конструирование модели данных и графического интерфейса утилиты NoteTaker. В разделах "Практика" мы займемся шаблонами и RDF. Это будут последние вносимые нами изменения в способ хранения данных в рассматриваемой утилите.