Создание элемента Tabstrip, управляемого данными, с помощью пользовательского интерфейса jQuery

Дино Эспозито (Dino Esposito) | 2 февраля 2010 г.

В предыдущей статье я рассмотрел основы мини-приложения Tabstrip, предоставляемого библиотекой jQuery UI. В этой статье я расширяю предыдущую статью и рассматриваю, как создавать полосу вкладок динамически, используя данные, полученные из удаленного источника. Удаленный источник данных (например, веб-служба) предоставляет блок данных конфигурации, прочитанный из базы данных или XML-файла. На клиенте некоторый код JavaScript обрабатывает эти данные и создает полосу вкладок на лету. Код примера является приложением ASP.NET MVC, но базовые приемы, показанные в этой статье, можно применить и для страницы ASP.NET Web Forms.

Обзор проекта ASP.NET MVC

Процедура добавления библиотеки пользовательского интерфейса jQuery в проект ASP.NET состоит из двух шагов. Сначала в папку Scripts добавляются файлы скриптов пользовательского интерфейса jQuery, а затем в папку Content добавляются выбранные темы пользовательского интерфейса jQuery Content. Кроме того, может понадобиться настроить главную страницу (если она есть) или создаваемую страницу, чтобы связать ее с файлами пользовательского интерфейса jQuery и темой CSS. Например:

<head runat="server">
    <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
    <link href="../../Content/jq-ui-Themes/sunny/jquery-ui-sunny.css" type="text/css" />   
    <script type="text/javascript" src="/Scripts/MicrosoftAjax.js" />
    <script type="text/javascript" src="/Scripts/jquery-1.3.2.min.js" /> 
    <script type="text/javascript" src="/Scripts/jQueryUI/jquery-ui-1.7.2.custom.min.js" /> 
</head>

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

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <script src="/helper.asmx/js" type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function() {
            loadTabsInfo();
        });
    </script>
</asp:Content>

Первый тег <script> ссылается на веб-службу, используемую с целью загрузки данных конфигурации для создания полосы вкладок. Второй тег <script> инициализирует страницу. Сначала изучим код функции loadTabsInfo.

function loadTabsInfo() {
    loadFromSource();
} 

function loadFromSource() {
    DataDrivenTabs.Helper.GetTabstripConfiguration(
        loadFromSource_onSuccess, loadFromSource_onFailure, null);
}

function loadFromSource_onSuccess(results, context) {
    fillViewInternal(results);
}

function loadFromSource_onFailure(results, context) {
    createErrorTabstrip();
}

Функция loadTabsInfo инициирует процесс, который, в конечном счете, создает и заполняет полосу вкладок. Она помещает вызов в другую вспомогательную функцию, подключающуюся к удаленному поставщику данных для вкладки. GetTabstripConfiguration — это метод веб-службы Helper.asmx, возвращающий массив объектов TabInfo:

public class Helper : WebService
{
    [WebMethod]
    public TabInfo[] GetTabstripConfiguration()
    {
        var tabs = new List<TabInfo>();
        :
        return tabs;    
    }
}

public sealed class TabInfo
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string ContentUrl { get; set; }
    public string InnerHtml { get; set; }
}

Для целей этой статьи объект tab определяется заголовком, идентификатором ID и некоторым контентом. Этот контент может быть фрагментом статической разметки (свойство InnerHtml) или URL-адресом ресурса на том же веб-сайте.

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

function fillViewInternal(data) {
    tabstripReset();
    tabstripDataBind(data);
}

В случае сбоя динамически создается вкладка ошибки, выглядящая подобно приведенной на рис. 1.


Рис. 1. Вкладка, динамически создаваемая для визуализации некоторых сведений об ошибке.

Создание динамической полосы вкладок нужно начать со следующей минимальной структуры:

<div id="...">
   <ul></ul>
</div>

Эта структура может быть создана динамически, либо она может быть статически включена в модель DOM страницы. Сфокусируемся на шагах, необходимых для создания полосы вкладок ошибки на рис. 1. Для создания полосы вкладок, управляемой данными, используются те же приемы.

Функцией, вызываемой из закончившегося сбоем обратного вызова метода веб-службы, является createErrorTabstrip:

function createErrorTabstrip() {
   createEmptyTabStructure();
   var target = "#MainMenu";
   var body = "<div><h3>Не удалось найти данные.</h3></div>";
   var id = "tabError";
   
   createTabSection(body, id, target);
   $(target).tabs();
   $(target).tabs('add', "#" + id, "Error");
}
function createEmptyTabStructure() {
   $("#MainMenu").tabs('destroy');
   $("#MainMenu").empty();
   var emptyStruct = $("<ul></ul>");
   emptyStruct.appendTo("#MainMenu");
}
 
function createTabSection(body, id, target) {
   var div = $(body);
   div.attr("id", id).appendTo(target);
}

Первый шаг состоит из стирания всей структуры в выбранном сегменте DOM. В этом случае мы предполагаем, что поддерево DOM, которое будет содержать полосу вкладок, содержит ID меню MainMenu. Любой элемент Tabstrip, подключенный к элементу MainMenu, разрушается, поддерево стирается и добавляется пустой тег UL. На этот момент минимальная структура элемента Tabstrip уже была воссоздана из пепла, каким бы ни был предыдущий контент полосы вкладок.

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

<div id="MainMenu">
   <ul>
      <li><a href="#tabError">Error</li>
   </ul>
   <div id="tabError">
      <h3> Не удалось найти данные. </h3>
   </div>
</div>

Для динамического создания поддерева DOM можно использовать некоторые из возможностей библиотеки jQuery, например возможность создания дерева из строки:

var emptyStruct = $("<ul></ul>");
emptyStruct.appendTo("#MainMenu");

В предыдущем фрагменте кода сначала создается объект, представляющий поддерево DOM, соответствующее заданной разметке, а затем оно добавляется в конечный элемент — MainMenu в данном примере.

Обработка загруженных данных вкладки

После загрузки данных для вкладки они доступны в качестве массива объектов TabInfo JavaScript. Чтобы создать полосу вкладок из массива, для настраиваемой привязки данных используется форма на стороне клиента. Первым шагом является стирание любой существующей структуры, как показано ниже:

function tabstripReset() {
    createEmptyTabStructure();
}

Следующим шагом является создание вкладок на полосе. Любая вкладка пользовательского интерфейса jQuery получает свое содержимое из локального тега DIV или из внешнего URL-адреса. Объект TabInfo обладает двумя различными свойствами для соответствия этим сценариям —  свойством InnerHtml, если вкладка указывает на a DIV, и свойством ContentUrl, если вкладка указывает на URL-адрес. Если вкладка указывает на DIV, этот тег DIV должен быть добавлен в документ. Кроме того, DIV должен уже присутствовать в документе во время вызова функции tabs для физического создания полосы вкладок. Ниже приведен полный код:

function tabstripDataBind(tabInfoObjects) {
    // Добавить все пропущенные теги DIV
    for (var i = 0; i < tabInfoObjects.length; i++) {
        var tabInfo = tabInfoObjects[i];
        if (tabInfo.InnerHtml !== "") {
            addContentForTab(tabInfo);
        }
    }
 
    // Создать полосу вкладок
    createTab();
 
    // Добавить вкладки
    for (var i = 0; i < tabInfoObjects.length; i++) {
        var tabInfo = tabInfoObjects[i];
        if (tabInfo.InnerHtml !== "") {
            addTabFromContent(tabInfo);
        } else {
            addTabFromUrl(tabInfo);
        }
    }
}

Код, добавляющий DIV с контентом, не сильно отличается от кода, уже использованного с целью создания полосы вкладок для ошибки:

function addContentForTab(tabInfo) {
   var newDiv = $(tabInfo.InnerHtml);
   newDiv.attr("id", tabInfo.Id).appendTo("#MainMenu");
}

Как уже упоминалось, контент для нового DIV берется из свойства InnerHtml объекта TabInfo. После добавления всех дочерних тегов DIV, создается полоса вкладок:

function createTab() {
    $("#MainMenu").tabs();
}

Когда полоса вкладок создана, можно начать добавление одной вкладки для каждого загруженного объекта TabInfo. Необходимо различать вкладки, указывающие на локальный тег DIV, и вкладки, указывающие на внешний URL-адрес. Следующие две функции показывают, что делать в обоих случаях:

function addTabFromUrl(tabInfo) {
     $("#MainMenu").tabs('add', tabInfo.ContentUrl, tabInfo.Title);
}
function addTabFromContent(tabInfo) {
    $("#MainMenu").tabs('add', "#"+tabInfo.Id, tabInfo.Title);
}

При изучения исходного кода функций видно, что разница минимальна, хотя результирующее поведение полосы вкладок заметно отличается. Если вкладка указывает на локальный элемент DIV, на нее необходимо ссылаться с помощью префикса #, как в классической команде jQuery. В противном случае, для второго параметра метода tabs задается URL-адрес, используемый для заполнения вкладки.

Если вкладка указывает на внешний URL-адрес, мини-приложение Tabstrip загрузит ответ асинхронно и автоматически отобразит в заголовке вкладки сообщение “Loading …” (Загрузка) в течение операции. Весь загруженный контент добавляется в текущую модель DOM, и в течение оставшегося сеанса доступ к нему осуществляется локально. Независимо от количества переключений между вкладками повторная загрузка не выполняется. Если вкладка указывает на дочерний элемент DIV, контент доступен локально, и никаких удаленных вызовов не выполняется. Динамически загруженная полоса вкладок показана на рис. 2.


Рис. 2. Динамически загруженная полоса вкладок, созданная с помощью API пользовательского интерфейса jQuery.

Привязка к данным на стороне сервера

Несмотря на то, что данные, показанные на рис. 2, собраны с помощью кода JavaScript, они извлекаются с сервера вызовом веб-службы. Вот как выглядит код метода, возвращающего контент вкладки для рис. 2.

public class Helper : WebService
{
    [WebMethod]
    public TabInfo[] GetTabstripConfiguration()
    {
        var tabs = new List<TabInfo>();
        var tab1 = new TabInfo()
                       {
                           Id = "Tab1",
                           Title = "Первая",
                           InnerHtml = "<div><h1>Один</h1></div>"
                       };
        tabs.Add(tab1);
 
        var tab2 = new TabInfo()
                       {
                           Id = "Tab2",
                           Title = "Вторая",
                           InnerHtml = "<div><h1>Два</h1></div>"
                       };
        tabs.Add(tab2);
 
        var tab3 = new TabInfo()
                       {
                           Id = "Tab3",
                           Title = "Третья",
                           InnerHtml = "<div><h1>Три</h1></div>"                               
                       };
        tabs.Add(tab3);
 
        return tabs.ToArray();
    }
    :
}

Конечно, метод для веб-службы может получать данные из базы данных или из любого другого используемого постоянного типа хранилища. Рассмотрим слегка отличающийся пример, в котором данные для вкладки извлекаются из базы данных "Борей", а для заполнения контентом области каждой вкладки используются URL-адреса.

Замечательно то, что код на стороне клиента при этом не меняется абсолютно. Код, который я описывал ранее, является достаточно общим, чтобы справляться с любыми исключениями на стороне сервера, а также поддерживать для областей вкладок как статический, так и динамический контент. Ниже приведен новый вариант вместо метода GetTabStripConfiguration для веб-службы:

public TabInfo[] GetTabstripConfiguration()
{
    var context = new NorthwindDataContext();
    var customers = (from c in context.Customers
                    where c.City == "London"
                    orderby c.CompanyName
                    select c).ToList();

    var tabs = new List<TabInfo>();

    foreach(Customer c in customers)
    {
        var tab = new TabInfo()
                      {
                          Id = c.CustomerID,
                          Title = c.CompanyName,
                          InnerHtml = "",
                          ContentUrl = "/Home/GetMarkup/" + c.CustomerID
                      };
        tabs.Add(tab);
    }

    return tabs.ToArray();
}

В этом примере ожидается, что каждая вкладка будет содержать данные о заказчике в базе данных "Борей", у которого есть офисы в Лондоне. Свойство InnerHtml класса TabInfo установлено равным пустой строке, а свойство ContentUrl содержит URL-адрес. Рис. 3 структурно выглядит похожим на рис. 2 за исключением того, что контент каждой вкладки загружается из удаленного URL-адреса строго по запросу.


Рис. 3. Полоса вкладок, созданная с помощью контента из внешнего URL-адреса.

Следующий вопрос — как определить и представить разметку для отдельных вкладок. В этом примере я используют метод действия с именем GetMarkup и следующей сигнатурой:

public ActionResult GetMarkup(string id)

Затем для EASTC заказчика используется URL-адрес в виде /home/getmarkup/EASTC. Таким образом, можно использовать пару "контроллер/представление", чтобы легко упорядочить подходящий фрагмент разметки, используя любую привязку данных, выполняемую на сервере.

Заключение

Подавляющему большинству современных веб-приложений — как ASP.NET MVC, так и ASP.NET Web Forms — требуются вкладки или элементы Accordion. Количество элементов в этих типовых мини-приложениях пользовательского интерфейса часто фиксировано, но для каждого приложения желательно использовать несколько различных представлений с разным набором элементов, загружаемых из удаленного источника. В результате часто нужно создавать полосы вкладок или элементы Accordion программно.

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