<% ASP на блюдечке %>. Часть 17
Иерархическая навигационная система меню (с помощью ASP, XML и JavaScript)
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? Отвечаем: лучше всего разработать систему таким образом, чтобы предоставить возможность в любой момент изменить как структуру, так и наименования иерархического меню, сделав сам код по его отображению независимым от структуры. Конечно, можно было бы создать несколько таблиц в какой-нибудь реляционной СУБД и, последовательно подсоединив их одну к другой, заполнить связанными иерархией отношений значениями. Однако СУБД — не самый простой и удобный способ решения этой задачи. При увеличении уровней вложенности как нельзя лучше подходит 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 не ограничивает разработчика в определении тэгов и организации структуры хранения данных. В вышеприведенном примере все тэги содержат пункты меню и имеют по три атрибута:
- type — определяет тип текущего уровня в иерархии и может принимать
одно из трех следующих значений:
- root — обозначает корневой уровень иерархии;
- folder — обозначает контейнер (папку), содержащий конечные документы иерархии;
- document — обозначает содержимое контейнера или конечный документ в иерархии;
- value — содержит текстовую константу, которая будет служить для отображения данного пункта в иерархии;
- url — содержит адрес ресурса, соответствующего данному пункту в иерархии.
Итак…
редставим себе наш интерфейс в виде двух вертикальных фреймов: левого, служащего для отображения иерархии объектов меню, и правого — для отображения содержимого текущего пункта меню. Левый фрейм представим файлом menu.asp, а правый файлом content.asp (см. рис. 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>
Отображение иерархии — файл Menu.asp
ля начала определим табличку стиля 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().
Рекурсивное построение меню — функция 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).
Вспомогательные JavaScript-функции
режде всего нам необходимо понять ту роль, которую играют два массива данных:
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
Правый фрейм — файл Content.asp
айл, по сути, содержит интерпретатор передаваемого ему параметра 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, каждая из которых используется с определенной целью, а именно:
- XML — для хранения иерархии меню;
- ASP — для извлечения данных из XML-файла, посредством вызова ActiveX компонента MSXML2.FreeThreadedDOMDocument.3.0 для анализа XML-документа и формирования соответствующих HTML-файлов;
- JavaScript — для организации отображения и управления иерархическим меню в его HTML-представлении, сгенерированном посредством ASP.
В статье использованы материалы ресурса: http://www.4guysfromrolla.com
КомпьютерПресс 1'2002