<% ASP на блюдечке %>. Часть 17

Иерархическая навигационная система меню (с помощью ASP, XML и JavaScript)

Рубен Садоян (rouben@iname.com)

Введение

Историческая справка

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 не ограничивает разработчика в определении тэгов и организации структуры хранения данных. В вышеприведенном примере все тэги содержат пункты меню и имеют по три атрибута:

В начало В начало

Итак…

Представим себе наш интерфейс в виде двух вертикальных фреймов: левого, служащего для отображения иерархии объектов меню, и правого — для отображения содержимого текущего пункта меню. Левый фрейм представим файлом 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>
В начало В начало

Отображение иерархии — файл 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">&nbsp;<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">&nbsp;<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, каждая из которых используется с определенной целью, а именно:

Архив исходных текстов к настоящей статье лежит здесь.

В статье использованы материалы ресурса: http://www.4guysfromrolla.com

КомпьютерПресс 1'2002