Сообщений 0    Оценка 345 [+1/-0]         Оценить  
Система Orphus

Динамическая генерация элементов управления для веб

Автор: Воронков Василий
Источник: RSDN Magazine #4-2003
Опубликовано: 13.03.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Описание
Использование
Создание XML-файла с содержанием вертикального меню
Главный заголовок (header)
Заголовок (caption)
Статический текст (static)
Случайный текст (random)
Линия (line)
Пункт меню (item)
Раскрывающийся пункт меню (root)
Создание XML-файла с содержанием дерева
Обычный пункт меню
Раскрывающийся пункт меню
Создание XML-файла с содержанием горизонтального меню
Основные идеи реализации
Описание класса
Использование XML
Создание простого горизонтального меню
Создание меню с раскрывающимися списками
Создание дерева
ASP.NET и XSLT (вместо заключения)

Код к статье

Описание

Данная статья представляет собой описание одной из технологий динамической компоновки веб-элементов управления, которая может быть использована на сайтах, разработанных с помощью ASP.NET. Структура элементов управления хранится в файлах, написанных на языке XML, в силу чего легко поддается модификации.

В статье вашему вниманию предлагаются три элемента Web-навигации – простое горизонтальное меню, которое обычно используется в качестве главного магистрального элемента управления на сайтах, вертикальное меню с раскрывающимися списками в стиле microsoft.com, и дерево. Динамическая компоновка данных элементов управления осуществляется с помощью класса WebGenerator.Generator, пример реализации которого содержится в демонстрационном проекте. Данный класс содержит три public-метода типа string (BuildMenu, BuildSimple и BuildTree), которые формируют полный HTML-код соответствующего элемента управления. После того, как код сгенерирован, остается лишь разместить его на веб-странице:

Данный класс рассчитан на использование в ASP.NET-приложении, поэтому и демонстрационный проект написан именно на ASP.NET. Тем не менее, благодаря универсальности технологии .NET, он может быть использован и в обычном настольном приложении, например, для динамического создания HTML-страниц. Неотьемлемой частью проекта являются XML-файлы WebGenerator.smenu.xml, WebGenerator.vmenu.xml и WebGenerator.tree.xml, которые содержат шаблоны HTML. Подобный подход позволит вам изменять шаблоны HTML, а следовательно, кардинально изменять представление ваших элементов управления, без перекомпиляции основного проекта. Однако если данный момент для вас не является принципиальным вы можете включить их в ресурсы сборки. В состав проекта также включен файл с таблицей стилей general.css и файлы со скриптами expand.js и vmenu.js. Эти файлы располагаются в папке _include прилагаемого к статье проекта и необходимы для динамической компоновки элементов управления. Кроме этого, в состав демонстрационного проекта входят примеры элементов управления, точнее, структуры этих элементов, описанные на языке XML. Это файлы vmenu.xml, tree.xml и smenu.xml, которые находятся в папке _xml.

ПРИМЕЧАНИЕ

Демонстрационный проект был разработан в Visual Studio .NET 2003, но совместим с обеими версиями .NET Framework, 1.0 и 1.1.

Несмотря на то, что в демонстрационном проекте содержится уже готовая реализация, мы, тем не менее, рассмотрим все этапы создания динамических элементов Web-навигации, от создания шаблонов HTML до написания серверного кода на языке C#. Сначала мы рассмотрим использование уже готовой реализации класса WebGenerator.Generator. Также я приведу спецификации по созданию XML-файлов со структурой рассматриваемых здесь элементов управления.

Использование

Для того, чтобы подготовить ASPX-страницу к работе с элементами навигации, создание которых будет описано в последующих разделах, следует добавить в ее заголовок ссылки на файлы с таблицей стилей и кодом javascript. В итоге заголовок страницы будет выглядеть примерно так:

<HEAD>
  <TITLE>WebGenerator</TITLE>
  <LINK href="_include\general.css" rel="stylesheet">
  <SCRIPT src="_include\expand.js"></SCRIPT>
  <SCRIPT src="_include\vmenu.js"></SCRIPT>
  <META content="Microsoft Visual Studio .NET 7.1" name="GENERATOR">
  <META content="C#" name="CODE_LANGUAGE">
  <META content="JavaScript" name="vs_defaultClientScript">
  <META content="http://schemas.microsoft.com/intellisense/ie5" name="vs_targetSchema">
</HEAD>

General.css – это файл с таблицей стилей, в котором определены все классы, используемые обсуждаемыми здесь элементами навигации. Файл vmenu.js содержит клиентский скрипт для вертикального меню. Файл expand.js содержит скрипт для дерева. Для вывода сгенерированного HTML-кода мы будем использовать элемент управления Literal.

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

Конструктор выглядит так:

      public Generator(string _resource, XmlNodeList _source)

_resource – это полное имя файла с ресурсами, который будет использован при генерации элемента управления, _source – коллекция элементов XML (System.Xml.XmlNodeList), содержащая структуру элемента управления. Для удобства в качестве второго входного параметра класса используется не текстовая строка, содержащая путь к файлу с данными меню, а объект типа XmlNodeList. Почему это сделано так? Казалось бы удобнее, с точки зрения клиентского кода, передавать непосредственно путь к файлу XML, в котором хранится описание элемента управления. Но на самом деле предлагаемый вариант значительно повышает гибкость рассматриваемого здесь класса. Я сам неоднократно сталкивался с тем, что некоторые элементы меню должны добавляться динамически в период исполнения – причем на основе информации, считываемой, скажем, из базы данных. Это может быть имя зарегистрированного посетителя или какая-либо другая информация, специфичная для конкретного пользователя сайта. Используя предлагаемую модель, достаточно будет прочитать XML-файл и добавить в него нужный элемент уже непосредственно в коде C#. Если бы в конструктор в качестве параметра передавалась бы ссылка на файл, то такое было бы невозможно.

Итак, код для создания экземпляра класса может выглядеть примерно так:

XmlDocument xml = new XmlDocument();
string res = Request.MapPath(null) + @"\myResource.xml";
structure = Request.MapPath(null) + @"\myMenu.xml";
xml.Load(structure);
Generator _generator = new Generator(res, xml.GetElementsByTagName("node"));  

Затем следует вызвать метод, отвечающий за создание того или иного элемента управления. Данный метод вернет значение типа string, содержащее весь необходимый HTML-код. Ниже приведены сигнатуры публичных методов класса WebGenerator.Generator с объяснением параметров.

Метод BuildMenu:

      public
      string BuildMenu (string picture)

picture - путь и имя файла с изображением «стрелки», используемым для оформления тех пунктов меню, которые содержат вложенные списки.

Метод BuildTree:

      public
      string BuildTree (string _plus, string minus, string _path);

_plus – путь и имя файла с изображением («крестик»), используемым для оформления дерева. _minus – путь и название файла с изображением («минус»), используемым для оформления дерева. _path – путь к папке с картинками. Если по каким-то причинам вы считаете нужным указывать путь непосредственно в атрибутах элементов XML, то просто передавайте в качестве данного параметра null.

Метод BuildSimple, отвечающий за создание простого горизонтального меню, не имеет входных параметров.

Итак, вызов метода для создания, к примеру, дерева может выглядеть так:

Literal1.Text = _generator.BuildTree(plus, minus, @"_images\");

Создание XML-файла с содержанием вертикального меню

Все элементы XML, кроме корневого и элементов первого уровня вложенности, должны иметь название «node» (<node>). Все дочерние элементы XML должны иметь название «cnode» (<cnode>). Корневой элемент XML может иметь любое название, кроме уже упоминавшихся. Приведу для пущей ясности пример:

<root>
<node>
<cnode />
</node>
</root>

Моей задачей было создание меню, которое не только является удобным элементом веб-навигации, но также может нести информативную функцию. Поэтому меню может включать в себя большое количество разных элементов, которые условно можно разделить на статические и динамические. Статические элементы меню не могут содержать гиперссылок и никак не регируют на прохождение курсора мыши по их области. Динамические элементы меню могут содержать гиперссылки и подсвечиваются, когда курсор мыши попадает в их область. Ниже я рассмотрю все типы элементов и приведу их краткое описание, стили, принятые по умолчанию, а также XML-синтаксис. В примерах синтаксиса будет использовать название элемента node, но, конечно же, синтаксис останется точно таким же и для вложенных элементов с названием cnode.

Главный заголовок (header)

Меню должно содержать только один главный заголовок, который выводися в самое его начало. Главный заголовок - это статический элемент. По умолчанию он выводится полужирным шрифтом без подчеркивания. Синтаксис XML:

<node type=”header”>Text</node>

где Text – текст элемента, type – тип элемента.

Заголовок (caption)

Заголовок является простым статическим элементом меню, который может использоваться в тех случаях, когда меню делится на логические категории (как, например, меню на www.microsoft.com). Он выводится полужирным шрифтом и подчеркивается сверху. Синтаксис XML:

<node type=”caption”>Text</node>

где Text – текст элемента, type – тип элемента.

Статический текст (static)

Статический элемент меню, который может быть использован для вывода простого статического текста. Текст выводится обычным шрифтом без эффектов. Синтактисис XML:

<node type=”static”>Text</node>

где Text – текст элемента, type – тип элемента.

Случайный текст (random)

Статический элемент меню, который случайным образом выбирает текст из вложенных тегов. Данный элемент может содержать любое количество вложенных тегов. Его можно использовать, к примеру, для создания небольшого раздела «совет дня». Синтаксис XML:

<node type=”random”>
  <cnode>Text1</cnode>
  <cnode>Text2</cnode>
</node>

где Text1, Text2 – текст элемента, type – тип элемента.

Линия (line)

Статический элемент меню, представляющий собой горизонтальную линию-разделитель. Синтаксис XML:

<node type=”line” />

где type – тип элемента.

Пункт меню (item)

Основной динамический элемент меню. При наведении на него курсора мыши подсвечивается. При нажатии происходит переход по ссылке. Синтаксис XML:

<node type=”item” url=”microsoft.com”>Text</node>

где Text – текст элемента, url – текст гиперссылки, type – тип элемента

Раскрывающийся пункт меню (root)

Динамический элемент меню, открывающий доступ к другим элементам. При наведении на него курсора мыши он подсвечивается, а рядом появляется дополнительное меню. Данный элемент не может содержать гиперссылок. Синтаксис XML:

<node type=”root” name=”Text”> 
  <cnode type=”item” url=”microsoft.com”>Item 1</cnode>
  <cnode type=”static”>Item 2</cnode>
</node>

где name – текст элемента, type – тип элемента.

Создание XML-файла с содержанием дерева

Общие принципы те же, что и для вертикального меню. Дерево поддерживает всего два типа элементов.

Обычный пункт меню

Данный пункт меню выделяется при наведении на него курсора мышки. При щелчке открывается гиперссылка. Синтаксис XML:

<cnode name="Text" image="image.gif" url="none" target="main"/>

где :

Раскрывающийся пункт меню

Данный пункт меню выделяется при наведении на него курсора мыши. При щелчке по нему будет раскрываться ветка дерева. Я специально не снабдил данный пункт атрибутами url и target, так как считаю, что ссылки, которые содержатся в таких пунктах дерева, в некотором роде «скрыты» от пользователя, и он может не сразу их обнаружить. Впрочем, если вы со мной не согласны, то можете немного модифицировать код для генерации дерева – так, что можно было открыть ссылку, щелкнув и на дочернем элементе дерева и на родительском. Синтаксис XML:

<node name="Text" image="image.gif">

где name – текст элемента, image – изображение для элемента

Создание XML-файла с содержанием горизонтального меню

Это совсем просто. Общие правила те же, что и в предыдущих случаях. Генератор использует только один тип элементов. Вот его XML-синтаксис:

<node url="http://www.somewhere.com" target=”main”>Text</node>

где url – текст гиперссылки, target определяет, в какой кадр будет выводиться документ, а Text – текст элемента

Основные идеи реализации

Описание класса

Класс WebGenerator.Generator является экземплярным и содержит три открытых и несколько private-методов. В классе определены следующие private-переменные:

        private
        string header, caption, statics;
privatestring image, plus, path;
privatestring item, root, line;
privateint i, j, k, l;      
privatestring resource;
private XmlNodeList source;

Также в классе определен один конструктор с параметрами:

        public Generator(string _resource, XmlNodeList _source)
{
  resource = _resource;
  source = _source;
}

Параметры resource и source используются во всех public-методах и задают полное имя файла с XML-ресурсами, и коллекцию элементов с данными, на основе которых генерируются элементы управления.

Использование XML

Основной идеей данного проекта является использование XML для хранения ресурсов HTML и данных, на основе которых будут генерироваться элементы навигации. С помощью XML можно легко описывать древовидные структуры, что для нас является особенно важным, так как основные элементы управления, создание которых будет описано далее, являются иерархическими. К тому же вы сможете легко вносить изменения в файлы со структурами меню и даже хранить несколько вариантов одного и того же меню (например, рассчитанного на пользователей с разным уровнем допуска) в одном файле. Хранение шаблонов HTML-кода в файлах XML также весьма удобно. Это удачно согласуется с политикой хранения скриптов и таблиц стилей в отдельных файлах. Благодаря этому HTML-код можно легко изменять, расширяя тем самым функциональность ваших Web-меню без перекомпиляции основного проекта, что, в общем, является весьма серьезным преимуществом.

Мы не будем использовать никаких «специальных» возможностей XML, поэтому для создания файлов с ресурсами HTML и описания структуры меню достаточно лишь самых начальных знаний XML.

Создание простого горизонтального меню

Рассматриваемые здесь элементы навигации во многих смыслах работают аналогично серверным элементам управления. Класс WebGenerator.Generator использует пользовательские данные из XML-файла и на их основе генерирует HTML, который и размещается на клиентской странице. Для удобства мы рассмотрим этот принцип работы на примере самого простого (хотя, как ни странно, куда более универсального, чем все остальные) элемента управления – стандартного горизонтального меню без раскрывающихся списков.

Итак, первым шагом на пути создания данного элемента управления является написание HTML-кода. Наиболее удобно, на мой взгляд, использовать для отображения меню HTML-элемент <table>. Теперь следует определиться с тем, из каких частей будет состоять таблица. Разделение на части здесь, конечно же, совершенно условно и продиктовано тем, что код таблицы будет храниться в файле ресурсов в формате XML. Главная часть таблицы – это ее заголовок со всеми прилагающимися атрибутами. Так как мы хотим создать горизонтальное меню, то вся таблица будет состоять из одной строки. Поэтому следующей частью описания таблицы будет код, представляющий собой пункт меню. Для того, чтобы каждый из пунктов меню был более выделенным, следует использовать в HTML-коде разделители между ними – например, обычный текстовый символ «|». Можно, конечно, добавлять его после каждого пункта меню вручную в самом коде генерации элемента управления, но мне кажется более удобным сделать специальную ячейку.

Далее я приведу HTML-код всех логических частей данного элемента управления. В скобках будет указано название XML-тега файла WebGenerator.smenu.xml, в котором содержится шаблон данного кода.

Итак, основная часть нашей таблицы будет выглядеть примерно так (SimpleBody):

<TABLE height=20px border="0" cellpadding="0" cellspacing="0" class="SimpleBody">

Высота таблицы здесь задается фиксированно, в соответствующем атрибуте, а не в классе стиля, так как этот параметр не будет изменяться. SimpleBody – это класс стиля, который будет использоваться для оформления таблицы. Я сразу использую указание на класс вместо inline-стилей, так как в большинстве случаев раздельное написание HTML-кода и классов стилей является не только наиболее удобным для разработчика, но и позволяет легко настраивать внешний вид HTML-страницы.

Таким будет пункт меню – ячейка таблицы (simpleItem):

<TD id="Simple|" border="0" class="SimpleItemBody"> <A href="urlplace" target=”targetplace”>
<SPAN onmouseover="this.className='SimpleOn';"
<onmouseout="this.className='SimpleOut';" class="SimpleItem">Text</SPAN></A><TD>

В идентификаторе, как вы заметили, содержится символ «|». Данный символ будет во время генерации элемента управления заменяться на порядковый индекс элемента. Также в атрибутах «onmouseover» и «onmouseout» содержатся строки javascript, которые динамически меняют класс стиля данного элемента при наведении на него курсора мыши. В атрибуте «href» тега А содержится текст «urlplace», который также будет заменяться на реальную гиперссылку, – как и «Text», содержащийся между открывающим и закрывающим тегами SPAN.

А так выглядит пункт меню – разделитель (simpleDel):

<TD border="0" class="SimpleItemBody"> <SPAN class="SimpleDel"> &nbsp;|</SPAN></TD>

Здесь символ «|» используется уже как символ разделителя, а индентификатора данный элемент не имеет.

Ну и, наконец, замыкающий пункт меню (simpleEnd):

<TD border="0" class="SimpleItemBody" width=100%><SPAN class = "SimpleEnd"> &nbsp; </SPAN> </TD>

Для настройки внешнего вида меню я использовал таблицы стилей. Благодаря им меню приобретает ставший уже стандартным вид – черный фон и белый цвет пунктов, который меняется на красный при наведении на них курсора мыши. Классы стилей содержатся в файле general.css, который прилагается к демонстрационному проекту.

Теперь все, что остается сделать – это «упаковать» HTML-код в XML-файл и написать один простой метод на языке C#. Поместите все выделенные нами «части» HTML-кода в отдельные теги XML, которые будут иметь ранее приводившиеся названия – simpleBody, simpleNode и пр. Затем нужно написать примерно такой метод:

        public
        string BuildSimple()
{
  string tmp = null;
  int i = 0;
  string result = null;      
  
  XmlDocument xml = new XmlDocument();
  xml.Load(resource);  
    
  XmlNodeList xel = xml.GetElementsByTagName("simpleBody");
  string body = xel[0].InnerText;
  xel = xml.GetElementsByTagName("simpleItem");
  string item = xel[0].InnerText;
  xel = xml.GetElementsByTagName("simpleDel");
  string del = xel[0].InnerText;
  xel = xml.GetElementsByTagName("simpleEnd");
  string simpleEnd = xel[0].InnerText;
      
foreach (XmlNode c in source) 
  {        
    i++;
    tmp = item.Replace("|", i.ToString());
    tmp = tmp.Replace("urlplace", c.Attributes["url"].InnerText);
    tmp = tmp.Replace("targetplace", c.Attributes["target"].InnerText);
    result += tmp.Replace("Text", "&nbsp;" + 
    c.InnerText.Replace(" ", "&nbsp;"));
    result += del;
  }
      
  return body + "<tr>" + result + simpleEnd + @"</tr></table>";
}

Как вы уже заметили, некоторые элементы HTML приходится добавлять динамически. По завершении данный метод возвращает переменную типа string, в которой и содержится динамически скомпонованный HTML-код.

ПРИМЕЧАНИЕ

В процессе подготовки ASPX-страницы к размещению данного элемента управления лучше указать в стиле для «корпуса» (BODY) следующие свойства: margin-left:0px; margin-right:0px. Это несколько улучшит внешний вид меню.

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

Создание меню с раскрывающимися списками

Основные принципы создания вертикального меню с раскрывающимися списками точно такие же, как в приведенном выше примере. Для его отображения также используются таблицы (теперь уже во множественном числе). Я не буду приводить весь HTML-код меню, так как в данном случае имеет место лишь его количественное, но не качественное усложнение. Большего внимания заслуживают скрипты, необходимые для работы меню. В любом случае, полные шаблоны HTML можно найти в файле ресурсов WebGenerator.vmenu.xml, который прилагается к проекту.

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

function overItem(item) 
{
  item.className = "Highlight";
}

function outItem(item) 
{
  item.className = "Item";
} 

Указатели на эти функции надо добавить в атрибуты onmousemove и onmouseout, как уже делалось раньше:

<TD id="Menu_|" class="Item"  onclick="document.location.href='urlplace';"
  onmouseover="overItem(this);" onmouseout="outItem(this);”>

Я также добавил в атрибут onclick строку javascript, которая будет использоваться для перехода между страницами. Все по-прежнему чрезвычайно просто. Вторая задача немного сложнее, хотя и здесь, как говорится, нет ничего непреодолимого. Для этого нужно создать вторую таблицу, которая обязательно будет иметь уникальный идентификатор. Например, MenuNested_1:

<TABLE cellpadding="2" cellspacing="2" id="MenuNested_|" class="Popup" 
  style="visibility:none”>

Вместо символа «|» опять-таки будет подставлен индекс. К тому же данная таблица должна быть скрытой на этапе первичного рендеринга страницы, поэтому я и поместил в тег TABLE атрибут style="visibility:hidden".

ПРИМЕЧАНИЕ

Свойство «visibility», в отличие от свойства «display», которое в сочетании со значением «none» привело бы на этапе рендеринга страницы к такому же результату, определяет, является ли видимым содержимое элемента HTML, при этом «спрятанный» элемент будет по-прежнему присутствовать на странице и занимать отведенную ему область, но будет невидим для пользователя. «Display» же определяет тип рендеринга элемента, «display:none», соответственно, устанавливает полное отсутствие такового.

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

Стиль, используемый для оформления подменю, называется «Popup». В таблице стилей, которая прилагается к демонстрационному проекту, внешнее оформление подменю практически полностью совпадает с оформлением корневого уровня меню, но использование отдельного класса стилей для скрытых таблиц оправдано, так как одно маленькое отличие найти все же можно. В классе Popup помимо стандартных содержится еще и такое свойство, на которое следует обратить внимание:

filter:progid:DXImageTransform.Microsoft.Shadow(color='#666666', direction=135, Strength=3)

Теперь, я думаю, вы понимаете, что «стиль Microsoft» был упомянут в начале статьи не случайно. Данное свойство позволяет использовать для оформления элементов HTML интересный визуальный эффект – тень, практически такую же, как в системных меню Windows XP. Можно даже подправить данное свойство на свой вкус – например, изменить цвет или интенсивность тени (Strength).

Итак, как я уже писал, выделение пункта меню с вложенным списком должно вызывать появление скрытой таблицы, описанной выше, притом с произвольными координатами. Это можно осуществить примерно так:

function overRoot(parent, _item) 
{
  var item = document.getElementById(_item);
  parent.className = "Highlight";  
  item.style.top = parent.offsetTop + parent.offsetParent.offsetTop;  
  item.style.left = parent.offsetLeft + 
    parent.offsetParent.offsetLeft + parent.offsetWidth; 
  item.style.visibility = 'visible';    
}

В этой функции для того, чтобы сделать таблицу с дополнительными пунктами меню видимой, также используется стилевое свойство «visibility». Расчет координат специально проводится так, чтобы он был пригоден и для тех списков с пунктами меню, которые открываются уже из вложенных списков и, соответственно, должны выводиться с несколько иными координатами. Если вы хотите использовать в меню только один вложенный уровень, то можно написать просто:

  item.style.top = parent.offsetTop;  
  item.style.left = parent.offsetLeft;

Итак, основная часть элемента управления уже реализована. Мы получили упрощенный вариант вертикального меню, который по сложности незначительно отличается от горизонтального меню, рассмотренного выше. Однако такое меню еще далеко от совершенства. Прежде всего возникает такая проблема – если навести курсор мышки на пункт меню с открывающимся списком дополнительного меню, то этот список так до конца и останется видимым, что бы вы ни делали. Он не исчезнет, даже когда появится другой список с дополнительными пунктами меню.

Нужно, чтобы вложенные списки исчезали. Для начала нужно сделать так, чтобы, условно говоря, «перевод фокуса» на другой элемент HTML вызывал автоматическое закрытие всех списков меню. Есть несколько вариантов реализации этого, но наиболее удобным мне кажется использование специальной функции «mouseup», при исполнении которой будут удаляться выведенные на экран списки. Пользователю достаточно будет щелкнуть в любом месте HTML-страницы и таблица с дополнительным пунктами меню исчезнет. При желании можно использовать и другие модели, которые я рассматривать не буду. Итак, задача состоит в том, чтобы добавить к документу событие «mouseup». Можно, конечно, сделать это, просто вставив указание на него в тег «BODY», но я рассмотрю более интересный и более удобный, на мой взгляд, способ – динамическое создание привязки к данному «событию» через код скрипта. Для этого нужно создать такую функцию:

function init ()
{
  document.onmouseup = function() 
  {
    mouseup();  
  }
}

Ее следует вызывать при загрузке страницы – просто поместите в код инструкцию «init();», и дело сделано. Осталось только написать саму функцию, которая будет приемником события mouseup. Здесь – для сокращения размера скрипта – я предлагаю пойти на маленькую хитрость. Правда, она предполагает, что все таблицы с дополнительными списками меню являются дочерними элементами тега DIV, однако в нашем случае это несложно реализовать уже в самом генераторе DHTML-кода. Когда это условие соблюдено, данная функция реализуется довольно просто:

function mouseup() 
{
  if (curItem != null)
  {
    var _hidden = document.getElementById("HiddenPlace");
    if (_hidden != null)
    {  
      for (var i = 0; i < _hidden.children.length; i++)
        _hidden.children.item(i).style.visibility = 'hidden';
    }
    curItem = null;
  }
}

HiddenPlace – это идентификатор элемента DIV, который является родительским для всех таблиц с дополнительными списками меню. На первый взгляд, конечно, непонятно, зачем в данной функции используется дополнительная переменная и производится выборка элемента DIV по текстовой строке индентификатора. Однако, к сожалению, это суровая необходимость. Событие может быть проинициализировано еще до рендеринга элемента управления, к которому оно обращается, поэтому нам и нужны все эти изощрения. В остальном, как вы видите, принцип работы события чрезвычайно прост. Пусть вас не пугает циклический перебор всех дочерних элементов HiddenPlace, ведь в их качестве выступают сами таблицы, т.е. дополнительные списки меню, количество которых никогда не будет до такой степени высоким, чтобы всерьез понизить производительность кода. К тому же и здесь можно применить небольшую хитрость. Для повышения производительности мы будем использовать переменную curItem. Так как функция mouseup будет часто вызываться «вручную» через код, то циклический перебор всех дочерних элементов тега DIV с целью сокрытия таблиц с дополнительными списками меню нам потребуется далеко не всегда, а только в тех случаях, когда раскрыт какой-нибудь из этих списков. Это и будет проверяться с помощью переменной curItem. В функцию overRoot, листинг которой приводился выше, мы добавим такую строчку (предварительно объявив переменную curItem в начале скрипта):

curItem = item;

Теперь циклический перебор всех скрытых таблиц будет производиться только тогда, когда это действительно необходимо.

Однако нам нужно, чтобы списки с дополнительными пунктами меню исчезали не только при щелчке по свободной области страницы, но и при переводе фокуса на другой пункт меню, независимо от того, является ли он раскрывающимся. В каких случаях это должно происходить? Понятно, что данная функция должна исполняться каждый раз, когда курсор мышки проходит над пунктами только основной таблицы меню. Ее не следует вызывать, когда мы путешествуем по таблицам с дополнительными меню, в противном случае они исчезали бы, едва успев появиться. Поэтому нужно проверять, над какой из таблиц проходит сейчас курсор и, если эта таблица является основным «корпусом» меню, вызывать функцию mouseup. Для проверки можно использовать такую функцию:

function check(item)
{
  if (item.parentNode.parentNode.parentNode.id == "MenuMain")
    mouseup();
}

Здесь для определения, над какой из таблиц проходит курсор, используется индентификатор основного «корпуса» меню MenuMain. Если вы собираетесь менять его, то не забудьте модифицировать и эту функцию.

На этом создание клиентского скрипта заканчивается. Нам осталось лишь написать методы на языке C# для динамической компоновки данного элемента управления. Весь код данных методов будет выглядеть примерно так:

        public
        string BuildMenu (string picture)
{      
  string result = null, resultSub = null, resultSub2 = null, 
resultSub3 = null;
  
  XmlDocument xml = new XmlDocument();
  xml.Load(resource);
  XmlNodeList xel = xml.GetElementsByTagName("menuBody");
  string body = xel[0].InnerText;
  xel = xml.GetElementsByTagName("menuSub");
  string sub = xel[0].InnerText;
  
  xel = xml.GetElementsByTagName("header");
  header = xel[0].InnerText;
  xel = xml.GetElementsByTagName("caption");
  caption = xel[0].InnerText;
  xel = xml.GetElementsByTagName("static");
  statics = xel[0].InnerText;
  xel = xml.GetElementsByTagName("item");
  item = xel[0].InnerText;
  xel = xml.GetElementsByTagName("root");
  root = xel[0].InnerText;      
  xel = xml.GetElementsByTagName("line");
  line = xel[0].InnerText;
  image = picture;
  
  for (i = 0; i < source.Count; i++)
  {
    result += _buildSwitch (source[i], 1);
    
    if (source[i].Attributes["type"].InnerText == "root")
    {
      resultSub += _buildItem(sub, i.ToString());          
      foreach (XmlNode c in source[i].ChildNodes)
      {
        j++;
        resultSub += _buildSwitch (c, 2);

        if (c.Attributes["type"].InnerText == "root")
        {
          resultSub2 += _buildItem (sub, 
            i.ToString() + "_" + j.ToString());
          foreach (XmlNode c2 in c.ChildNodes)
          {
            k++;
            resultSub2 += _buildSwitch (c2, 3);

            if (c2.Attributes["type"].InnerText == "root")
            {
              resultSub3 += _buildItem (sub, 
                i.ToString() + "_" + j.ToString() + 
                "_" + k.ToString());
              foreach (XmlNode c3 in c2.ChildNodes)
              {
                l++;
                resultSub3 += _buildSwitch (c3, 4);
              }
              resultSub3 += @"</table>";
            }
          }
          resultSub2 += @"</table>";
        }
      }
      resultSub += @"</table>";
    }
  }
  
  result = body + result + @"</table>";      
  return result  + @"<DIV id=""HiddenPlace"">" + 
    resultSub + resultSub2 + resultSub3 + "</DIV>";      
}

privatestring _buildItem (string _source, string index, string text)
{
  if (_source == "header") _source = header;        
  elseif (_source == "caption") _source = caption;
  elseif (_source == "statics") _source = statics;
  return _buildItem (_source.Replace("Text", text), index);    
}

privatestring _buildItem (string _source, string index, string text, string url)
{
  return _buildItem (_source.Replace ("urlplace", url), index, text);
}

privatestring _buildItem (string _source, string index, XmlNodeList n)
{
  Random rnd = new Random();  
  return _buildItem (_source, index, n[rnd.Next(n.Count)].InnerText);
}

privatestring _buildItem (string _source, string index)
{
  return _source.Replace("|", index);
}      

privatestring _buildSwitch(XmlNode n, int index)
{
  string _result = null;
  string tmp = i.ToString();
  int _index = 1;

  if (index > 1) _index = 2;      
  
  if (index == 2)
    tmp = i.ToString() + "_" + j.ToString();        
  elseif (index == 3)
    tmp = i.ToString() + "_" + j.ToString()+ "_" + k.ToString();
  elseif (index == 4)
    tmp = i.ToString() + "_" + j.ToString() + k.ToString() + "_" + l.ToString();
  
  switch (n.Attributes["type"].InnerText)
  {                  
    default:
      _result = _buildItem (n.Attributes["type"].InnerText, tmp, n.InnerText);
      break;          
    case"item":
      _result = _buildItem (item, tmp, n.InnerText, n.Attributes["url"].InnerText);
      break;          
    case"random":            
      _result = _buildItem(statics, tmp, n.ChildNodes);
      break;
    case"line":
      _result = _buildItem(line, tmp);
      break;
    case"root":      
      _result = _buildItem (root.Replace("1", _index.ToString()), tmp, 
        n.Attributes["name"].InnerText).Replace ("imageplace", image);
      break;
  }
  
  return _result;      
}

Некоторые из используемых здесь переменных объявлены в теле самого класса и инициализируются в конструкторе. Это описано в разделе, посвященном общему описанию класса WebGenerator.Generator. Для извлечения данных из XML используется класс XmlDocument из пространства имен System.Xml, это показалось мне наиболее удобным для данного случая. Код специально построен так, чтобы можно было достаточно легко добавлять новые вложенные уровни меню. Компоновка элемента управления состоит по сути дела из постоянно повторяющихся действий, например, замены символа «|» на порядковый индекс. Поэтому для удобства используется многократно перегруженный частный метод _buildItem, в котором и осуществляется генерация HTML-кода на основе описанных выше шаблонов. Код основного «корпуса» меню и дополнительных таблиц на этапе генерации хранится в отдельных переменных. Это значительно упрощает итоговую компоновку меню. Код всех меню первого уровня вложенности хранится в переменной resultSub, второго уровня – в переменной resultSub2 и т.д. Как вы наверняка уже заметили, при итоговой компоновке все вложенные подуровни resultSub обрамляются тегом DIV с идентификатором HiddenPlace. Именно благодаря ему и стала возможной такая простая реализация функции mouseup клиентского скрипта.

ПРИМЕЧАНИЕ

Данный код можно значительно сократить, если вместо того, чтобы один раз делать выборку содержимого XML-тегов в переменные типа string, динамически извлекать данные из XML внутри самого алгоритма генерации веб-меню. Однако такой подход несколько затормозил бы компоновку элемента управления, что для нас по понятным причинам нежелательно.

Вот и все. Осталось только написать классы стилей для нашего меню. Они содержатся в файле global.css.

Создание дерева

К сожалению, вертикальное меню, описанное в предыдущем разделе, неприемлемо при использовании фреймов. Так как дополнительные списки меню являются ни чем иным, как обычными таблицами, то они по понятным причинам не могут находиться на двух страницах сразу, а следовательно, и выводиться поверх фреймов. Если использование фреймов принципиально, нужен другой элемент навигации – дерево, принципы создания которого (аналогичные рассмотренным выше) я и приведу в данном разделе.

Шаблоны элемента управления «дерево» состоят всего из двух частей. Первая часть представляет собой корневой элемент дерева (slider):

<DIV CLASS="Root|" style="display:block; position:relative" nowrap>
  <IMG name="Icon|" src="plus" onClick="Expand(Child|, Icon|)" 
    onmouseover="this.style.cursor='hand'"  style="position:relative; left:5;">
  <IMG src="null" onmouseover="this.style.cursor='hand'" 
    style="position:relative; left:5;">
  <SPAN onmouseover="this.className='TreeHover'" class="TreeRoot"
    onmouseout="this.className='TreeOut'" onClick="Expand(Child|, Icon|)" 
    style="position:relative; left:5;">
  Title
  </SPAN>
</DIV>
<DIV CLASS="Child|" id="Child|" style="position:relative; 
  left:20px; width=50%; display:none;" nowrap>

Здесь используются те же принципы, что и раньше. Символ «|» динамически заменяется в коде на порядковый индекс элемента дерева. Некоторые элементы стиля, изменять которые нельзя, устанавливаются сразу в атрибуте style. Завершается данная часть шаблона открывающим тегом DIV с рядом атрибутов, включая также и style, который содержит инструкцию display:none. Как вы догадались, именно в элементе DIV и будут находиться вложенные ветки.

Вторая часть шаблона представляет собой HTML-код вложенных элементов дерева (sliderDiv):

<DIV nowrap>  
  <IMG src="null" onmouseover="this.style.cursor='hand'"
    style="position:relative; left:18;">
  <A href="url" target="targetplace" class="TreeNode">
    <SPAN onmouseover="this.className='TreeHover'" 
      onmouseout="this.className='TreeOut'" 
      style="position:relative; left:18;">    
    Text
    </SPAN>
  </A>
</DIV>

Атрибут «nowrap» нужен для того, чтобы при масштабировании кадра текст элемента не переносился на другую строку, а оставался на одной строке вместе с иконкой. В атрибутах тега А «href» и «target» содержится текст, который будет динамически заменяться генератором элемента управления на реальные гиперссылки. В теге A также определен класс стилей для того, чтобы стандартный стиль гиперссылки не нарушал оформления дерева.

Скрипт, необходимый для работы данного элемента управления, чрезвычайно прост. Он должен лишь менять стилевой элемент display для вложенных веток дерева, а также картинки корневых элементов дерева («плюсы» и «минусы»). Весь скрипт умещается в одной функции Expand:

function Expand (item, pic) 
{
  if (item.style.display == "block") 
  {
    item.style.display = "none";
    pic.src = "_images\\plus.gif";  
  }
  else
  {
    item.style.display = "block";
    pic.src = "_images\\minus.gif";
  }    
}

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

А вот и сам C#-код для компоновки данного элемента управления на основе XML:

        public
        string BuildTree (string _plus, string minus, string _path)
{
  string result = null;
  int h = 0;
  plus = _plus;
  path = _path;
  XmlDocument xml = new XmlDocument();
  xml.Load(resource);  
  XmlNodeList xel = xml.GetElementsByTagName("slider");
  string slider = xel[0].InnerText;
  xel = xml.GetElementsByTagName("sliderDiv");
  string sliderDiv = xel[0].InnerText;      
  
  for (int i = 0; i < source.Count; i++)
  {
    if (source[i].HasChildNodes)
    {
      result += _rootBuild (slider, i.ToString(), source[i]);

      foreach (XmlNode c in source[i].ChildNodes)
      {          
        if (c.HasChildNodes)
        {
          j++;
          result += _rootBuild (slider, i.ToString() + "_" +
            j.ToString(), c);
          
          foreach (XmlNode c2 in c.ChildNodes)
          {                
            if (c2.HasChildNodes)
            {
              h++;
              result += _rootBuild (slider, i.ToString() + "_" +
                  j.ToString()  + "_" + h.ToString(), c2);
              
              foreach (XmlNode c3 in c2.ChildNodes)
                result += _nodeBuild (sliderDiv, c3);
              
              result += @"</DIV>";
            }
            else
              result += _nodeBuild (sliderDiv, c2);
          }
          result += @"</DIV>"; 
        }
        else  result += _nodeBuild (sliderDiv, c);
      }        
      result += @"</DIV>"; 
    }
    else  result += _nodeBuild (sliderDiv, source[i]);  
  }      
  return result;
}

privatestring _nodeBuild (string source, XmlNode n)
{
  string tmp = source.Replace("null", path + n.Attributes["image"].InnerText);
  tmp = tmp.Replace("url", n.Attributes["url"].InnerText);
  tmp = tmp.Replace("targetplace", n.Attributes["target"].InnerText);
  return tmp.Replace("Text", n.Attributes["name"].InnerText);              
  
}

privatestring _rootBuild (string source, string index, XmlNode n)
{
  string tmp = source.Replace("|", index);
  tmp = tmp.Replace("null", path + n.Attributes["image"].InnerText);
  tmp = tmp.Replace("plus", plus);
  return tmp.Replace("Title", n.Attributes["name"].InnerText);
}

В коде используются те же принципы, что и ранее. Индентификаторы формируются динамически путем замены символа «|» на порядковый индекс. Основная работа по генерации HTML вынесена в частные методы _nodeBuild и _rootBuild.

ASP.NET и XSLT (вместо заключения)

В конце данной стать мне хотелось бы ненадолго оставиться на XSL-преобразовании, которое обычно используется для динамического создания элементов управления на Web-страницах. Много здесь говорить не придется, так как в MSDN уже была опубликована довольно интересная статья («A Practical Comparison of XSLT and ASP.NET»), представляющая собой сравнение генерации HTML-кода через код на сервере и через XSL-преобразование. Все желающие могут найти статью по адресу http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnexxml/html/xml02192001.asp. Конечно же, результаты приведенных в данной тестов далеко не однозначны, однако, если верить авторам MSDN, код C# все же имеет одно весьма значительное преимущество перед XSLT, а именно – скорость. Количество запросов в версии с генерацией HTML через C#-код достигает 120 в секунду, а при использовании XSLT – только 33 в секунду. Разумеется, я в ни в каком смысле не собираюсь утверждать, что предлагаемый в настоящей статье подход в любом случае предпочтительнее, чем XSL-преобразование, однако не стоит и забывать о том, что скорость все же один из наиболее важных факторов для Web-приложения. Правда, на мой взгляд, XSL-преобразование больше относится к временам обычных ASP-приложений, когда для него не было никакой действенной альтернативы, однако с появлением ASP.NET и возможности писать логику Web-приложения на компилируемых языках с довольно высокой скоростью кода XSL-преобразование может стать менее актуальным, чем раньше.


Эта статья опубликована в журнале RSDN Magazine #4-2003. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 345 [+1/-0]         Оценить