XML — хранилище данных иерархии меню
Отображение иерархии — файл Menu.asp
Рекурсивное построение меню — функция DisplayNode()
Вспомогательные JavaScript-функции
Правый фрейм — файл Content.asp
ак
известно, одной из наиболее важных составляющих любого приложения является система
навигации в содержании. Это та неотъемлемая составляющая, благодаря которой
пользователи получают удобный и быстрый доступ к нужному разделу информации.
Как показал опыт развития интеллектуальных интерфейсов современных операционных
систем, наибольшей интуитивностью обладает иерархический древовидный интерфейс
— такой, в частности, служит основой навигации в различных Windows-приложениях
(Windows Explorer, Microsoft Management Console, Registry Editor и т.д.). Информация
отображается иерархически, причем дочерние разделы отображаются правее (глубже)
разделов родителей. Подобная система уже давно зарекомендовала себя как одна
из самых наглядных, например когда дело касается организации систем отображения
иерархий.
о
недавнего времени иерархическая навигационная система была присуща лишь так
называемым настольным (Desktop) приложениям, или, проще говоря, приложениям,
выполняющимся в операционной системе. Позднее стали появляться системы, эмулирующие
поведение своих старших программных «собратьев», однако основным их недостатком
являлась огромная транзакционная нагрузка на сервер. Дело в том, что для отображения
различных состояний дерева использовались различные html-файлы и, таким образом,
задача сводилась к передаче управления от одного html-файла другому, а это при
увеличении количества уровней в иерархии приводило к множеству проблем как в
ходе разработки, так и при использовании таких систем. Затем появились аплеты
Java, и хотя они решали задачу отображения информации требуемым образом, однако
выполнялись интерпретатором Java не на сервере, а непосредственно в браузере
клиента, что создавало дополнительный ненужный трафик. И вот, наконец, в последнее
время, благодаря развитию технологий Internet-программирования и появлению ASP-технологии,
такой способ организации представления данных стал в полной мере доступен и
Web-приложениям.
Многие наши читатели вправе усомниться в необходимости иерархического дерева. И будут правы, если посчитают подобную систему навигации излишней роскошью на сайте с относительно небольшим количеством страниц. Но везде, где структура отображаемых данных представлена иерархией и количество отдельных страниц данных велико, она окажется просто незаменимой.
десь
может возникнуть вопрос: а при чем тут XML? Отвечаем: лучше всего разработать
систему таким образом, чтобы предоставить возможность в любой момент изменить
как структуру, так и наименования иерархического меню, сделав сам код по его
отображению независимым от структуры. Конечно, можно было бы создать несколько
таблиц в какой-нибудь реляционной СУБД и, последовательно подсоединив их одну
к другой, заполнить связанными иерархией отношений значениями. Однако СУБД —
не самый простой и удобный способ решения этой задачи. При увеличении уровней
вложенности как нельзя лучше подходит XML-организация хранения иерархии. Для
удобства изложения материала и большей наглядности представим меню как информацию
по отдельной стране, например по некоторым фактам из истории США:
<country type="root" value="United States of America" url="content.asp?page=usa"> <states type="folder" value="States" url="content.asp?page=states"> <state type="document" url="content.asp?page=ca" value="California"/> <state type="document" url="content.asp?page=nj" value="New Jersey"/> <state type="document" url="content.asp?page=az" value="Arizona"/> </states> <hist_fig type="folder" value="Historical Figures" url="content.asp?page=histfig"> <figure type="document" value="George Washington" url="content.asp?page=george"/> <figure type="document" value="Thomas Jefferson" url="content.asp?page=tom"/> </hist_fig> <history type="folder" value="History" url="content.asp?page=history"> <Cent20 type="folder" url="content.asp?page=20th" value="20th Century"> <inventions type="folder" url="content.asp?page=inv" value="Inventions"> <technologies type="folder" url="content.asp?page=tec" value="Technology"> <radio type="folder" url="content.asp?page=radio" value="Radio"> <bground type="document" url="content.asp?page=invprof" value="Inventor Profile"/> <bground type="document" url="content.asp?page=first" value="First Use"/> </radio> <computers type="folder" url="content.asp?page=computers" value="Computers"> <begin type="folder" url="content.asp?page=begin" value="Beginnings"> <summary type="document" url="content.asp?page=sum" value="Summary"/> <transistor type="folder" url="content.asp?page=trans" value="Transistor"> <trans type="document" url="content.asp?page=inventor" value="Inventor"/> <trans type="document" url="content.asp?page=app" value="Applications"/> </transistor> </begin> </computers> </technologies> </inventions> <wars type="folder" url="content.asp?page=wars" value="Wars"> <war type="document" url="content.asp?page=wwi" value="World War I"/> <war type="document" url="content.asp?page=wwii" value="World War II"/> <war type="document" url="content.asp?page=viet" value="Vietnam"/> </wars> </Cent20> <Cent21 type="folder" url="content.asp?page=21st" value="21st Century"/> </history> </country>
Как видите, XML во многом напоминает HTML, однако, в отличие от последнего, XML не ограничивает разработчика в определении тэгов и организации структуры хранения данных. В вышеприведенном примере все тэги содержат пункты меню и имеют по три атрибута:
редставим
себе наш интерфейс в виде двух вертикальных фреймов: левого, служащего для отображения
иерархии объектов меню, и правого — для отображения содержимого текущего пункта
меню. Левый фрейм представим файлом menu.asp, а правый файлом content.asp (см.
рис. 1):

Рис. 1
<html> <head> <title><% ASP на блюдечке %>. Часть 17</title> </head> <FRAMESET cols="250,*"> <FRAME src="menu.asp" name="treeframe" > <FRAME SRC="content.asp" name="basefrm"> </FRAMESET> </HTML>
ля
начала определим табличку стиля node, который будем использовать в дальнейшем:
<STYLE TYPE="text/css">
<!--
.node { color: black;
font-family : "Helvetica", "Arial", "MS Sans Serif", sans-serif;
font-size : 9pt;}
.node A:link { color: black; text-decoration: none; }
.node A:visited { color: black; text-decoration: none; }
.node A:active { color: black; text-decoration: none; }
.node A:hover { color: black; text-decoration: none; }
-->
</STYLE>
Далее создадим экземпляр ActiveX объекта и загрузим в него XML-файл с иерархией нашего меню:
<%
On Error Resume Next
dim sXMLSourceFile
dim iTotal, sLeftIndent, bLoaded
iTotal = 0
sLeftIndent = ""
sXMLSourceFile = "menuitems.xml"
'Создаем экземпляр COM объекта XML
Set objDocument = Server.CreateObject("MSXML2.FreeThreadedDOMDocument.3.0")
if objDocument is nothing then
Response.Write "objDocument object not created<br>"
else
If Err Then
Response.Write "XML DomDocument Object Creation Error - <BR>"
Response.write Err.Description
else
objDocument.async = false
bLoaded = objDocument.load(Server.MapPath(sXMLSourceFile))
if (bLoaded = False) then
sbShowXMLParseError objDocument
else
dim arArray(3)
arArray(0) = objDocument.firstChild.getAttribute("value")
'Корневой уровень в нашей иерархии
arArray(1) = "History"
'Строим таблицу нашего меню
%>
<table border="0" cellspacing="0" cellpadding="0" width="100%">
<tr><td>
<%
'Покажем текущий пункт нашего иерархического меню
DisplayNode objDocument.childNodes, iTotal, sLeftIndent, arArray
%>
</td></tr>
</table>
<%
end if
end if
end if
%>
Как видите, вызов процедуры
DisplayNode objDocument.childNodes, iTotal, sLeftIndent, arArray
собственно говоря, служит для создания иерархии нашего меню. Параметр iTotal, передающийся по ссылке, отслеживает общее количество элементов нашего меню и будет использоваться в дальнейшем. Функция продолжает рекурсивно вызывать саму себя, пока не будет осуществлен обход всего дерева элементов меню. Так, параметр iTotal используется для определения массивов, служащих для управления отображением нашего меню:
var arClickedElementID =
new Array(<% for i = 1 to iTotal %> "<%=i%>"<%if
i < iTotal then%>,<%end if%> <%next%>);
var arAffectedMenuItemID =
new Array(<% for i = 1 to iTotal %> "<%=i+1%>"<%if
i < iTotal then%>,<%end if%> <%next%>);
Теперь HTML-страница сформирована, и на этом этапе XML-файл совершил свою функцию: данные из него прочитаны и дерево уже построено. Но по-прежнему «черным ящиком» остается функция DisplayNode().
то,
по сути, и есть ядро нашей системы, осуществляющее обход дерева и формирующее
HTML-код. У процедуры четыре входных параметра: objNodes, iElement, sLeftIndent
и arOpenFolders. Первый — objNodes — служит для определения всего
набора уровней иерархии, начиная с уровня root. Второй — iElement
— содержит целое идентифицирующее количество уже отображенных элементов иерархии.
Этот параметр передается по ссылке и таким образом обновляется при каждом вызове
процедуры. Параметр sLeftIndent передает строку, содержащую HTML-форматирование
для отображения того или иного элемента меню. Параметр arOpenFolders
— это массив наших элементов.
Кроме того, в ходе каждого выполнения процедуры DisplayNode() проверяется:
For Each oNode In objNodes
bHasChildren = oNode.hasChildNodes
if not(oNode.nextSibling is nothing) then
bIsLast = false
else
bIsLast = true
end if
if oNode.nodeType = NODE_ELEMENT Then
sAttrValue = oNode.getAttribute("value")
sNodeType = lcase(oNode.getAttribute("type"))
bForceOpen = fnInArray(sAttrValue, arOpenFolders)
sURL = oNode.getAttribute("url")
if (sNodeType = "document") then
%>
<table border="0" cellspacing="0" cellpadding="0" width="100%">
<tr valign="bottom">
<% Response.write sLeftIndent %>
<td width="31"><img src="images/<%=fnChooseIcon(bIsLast, sNodeType, bHasChildren, bForceOpen)%>"
width="31" height="16" border="0"></td>
<td nowrap class="node"> <a href="<%=sURL%>" target="basefrm"><%=sAttrValue%></a></td>
</tr>
</table>
<% else %>
<table border="0" cellspacing="0" cellpadding="0" width="100%">
<tr valign="bottom">
<% Response.write sLeftIndent %>
<td width="31"><img class="LEVEL<%=iElement%>" src="images/
<%= fnChooseIcon(bIsLast, sNodeType, bHasChildren, bForceOpen) %>" id="<%=iElement%>"
width="31" height="16" border="0"></td>
<td nowrap class="node"> <a href="<%=sURL%>" target="basefrm"><%=sAttrValue%></a></td>
</tr>
</table>
<% If bHasChildren Then iElement = iElement + 1 %> <table border="0" cellspacing="0" cellpadding="0" width="100%"> <tr valign="bottom" class="LEVEL<%=iElement%>" id="<%=iElement%>" style="display: <% if ( fnInArray(sAttrValue, arOpenFolders) = false ) then%>none<%end if %> "> <td> <% sTempLeft = sLeftIndent if (iElement > 1) then sLeftIndent = fnBuildLeftIndent(oNode, bIsLast, sLeftIndent) end if 'Рекурсивный вызов и продолжение обхода дерева вглубь. DisplayNode oNode.childNodes, iElement, sLeftIndent, arOpenFolders sLeftIndent = sTempLeft %> </td> </tr> </table> <% End If end if End If Next
Как видите, нам осталось разобраться в нескольких вспомогательных JavaScript-функциях, служащих для выбора необходимого графического значка (fnChooseIcon), функции обхода массива при поисках нужного значения (fnInArray), отрисовки элементов (fnBuildLeftIndent) и показа сообщения об ошибке с указанием строки, колонки и другой полезной отладочной информации (sbShowXMLParseError).
режде
всего нам необходимо понять ту роль, которую играют два массива данных:
var arClickedElementID = new Array( "1", "2", "3", "4", "5", "6", ...); var arAffectedMenuItemID = new Array( "2", "3", "4", "5", "6", ...);
Эти массивы служат для определения отношения «родитель-потомок», показывая, какие элементы нашего списка должны быть свернуты, а какие развернуты. Первый массив (arClickedElementID[]) содержит идентификаторы всех элементов нашей иерархии. Второй (arAffectedMenuItemID[]) — идентификаторы всех потомков заданного элемента из первого массива. В приведенном выше примере это — потомки первого элемента первого массива данных.
Развертывание/свертывание элементов — функция doChangeTree()
Сначала определим функцию-реакцию на действия пользователя. Перехватим событие onClick нашего HTML-документа:
document.onclick = doChangeTree;
Первое, что нам надо будет сделать, как только пользователь нажмет на тот или иной пункт в иерархии, это получить ссылку на «нажатый» элемент. Далее продолжаем только в том случае, если элемент представляет собой класс и если в начале его имени содержится строковая константа "LEVEL":
srcElement = window.event.srcElement;
if(srcElement.className.substr(0,5) == "LEVEL")
{
Затем мы должны сослаться на потомок данного родителя, который должен быть развернут или свернут:
targetElement = fnLookupElementRef(srcElement.id)
Для этого мы передаем параметр ID нажатого пользователем элемента меню функции fnLookupElementRef(), которая с помощью описанных нами выше массивов arClickedElementID[] и arAffectedMenuItemID[] получает ссылку на требуемый потомок, как показано ниже:
for (i=0; i<arClickedElementID.length; i++) if (arClickedElementID[i] == sID) return document.all(arAffectedMenuItemID[i]);
Нам потребуется также исключить обработки нажатий на пустых папках. Для этого заранее проименуем соответствующие файлы с изображениями пустых папок таким образом, чтобы они содержали слово "empty" и будем анализировать название соответствующего файла:
var sImageSource = srcElement.src;
if (sImageSource.indexOf("empty") == -1)
{
...
Потом мы проверим текущий статус папки. Если она свернута, то нам потребуется ее развернуть, и наоборот. Статус будем определять исходя из значения параметра style.display. Если его значение равно "none", это означает, что пункт скрыт и свернут. А пустое значение будет означать, что он видим и развернут:
if (targetElement.style.display == "none")
{
targetElement.style.display = "";
if (srcElement.className == "LEVEL1")
srcElement.src = "images/minusonly.gif";
else
srcElement.src = "images/folderopen.gif";
}
else
{
targetElement.style.display = "none";
if (srcElement.className == "LEVEL1")
srcElement.src = "images/plusonly.gif";
else
srcElement.src = "images/folderclosed.gif";
}
И наконец, функция, помогающая обнаружить ошибку и устранить ее:
Sub sbShowXMLParseError(byVal objDocument) dim objParseError Set objParseError = objDocument.parseError Response.Write "XML File failed to load<BR>" Response.Write "---------------------------<BR>" Response.Write "Error: " & objParseError.reason & "<BR>" Response.Write "Line: " & objParseError.Line & "<BR>" Response.Write "Line Position: " & objParseError.linepos & "<BR>" Response.Write "Position In File: " & objParseError.filepos & "<BR>" Response.Write "Source Text: " & objParseError.srcText & "<BR>" Response.Write "Document URL: " & objParseError.url & "<BR>" set objParseError = nothing end sub
айл,
по сути, содержит интерпретатор передаваемого ему параметра Page:
<%@ Language=VBScript %>
<HTML>
<HEAD></HEAD>
<BODY>
<%
Dim sPage
sPage = Request.QueryString("page")
select case (sPage)
case "":
%>Please choose a menu item on the left<%
case "usa":
%>United States of America<%
case "states":
%>States<%
case "ca":
%>California<%
case "nj":
%>New Jersey<%
case "az":
%>Arizona<%
case "histfig":
%>Historical Figures<%
case "george":
%>George Washington<%
case "tom":
%>Thomas Jefferson<%
case "history":
%>History<%
case "20th":
%>20th Century<%
case "inv":
%>Inventions<%
case "tec":
%>Technology<%
case "radio":
%>Radio<%
case "invprof":
%>Inventor Profile<%
case "first":
%>First Uses<%
case "computers":
%>Computers<%
case "begin":
%>Beginnings<%
case "sum":
%>Summary<%
case "trans":
%>Transistor<%
case "inventor":
%>Inventor<%
case "app":
%>Applications<%
case "wars":
%>Wars<%
case "wwi":
%>World War I<%
case "wwii":
%>World War II<%
case "viet":
%>Vietnam<%
case "21st":
%>21st Century<%
case else:
%>Your menu selection is not recognized.<%
end select
%>
</BODY>
</HTML>
истема
динамического иерархического меню является достаточно мощным и удобным инструментом,
позволяющим пользователю получить доступ к нужным разделам любой иерархии объектов,
независимо от их характера и структуры. Данная система может быть с успехом
применена в иерархиях практически любой степени сложности, особенно при организации
сложных электронных магазинов, в которых имеется большое количество уровней
вложенности категорий товаров. Реализация системы являет собой удачное сочетание
использования технологий Web-программирования — ASP, XML и JavaScript, каждая
из которых используется с определенной целью, а именно:
Архив исходных текстов к настоящей статье лежит здесь.
В статье использованы материалы ресурса: http://www.4guysfromrolla.com
КомпьютерПресс 1'2002