На переднем крае

Условный рендеринг в ASP.NET AJAX 4.0

Дино Эспозито

Рендеринг на клиентской стороне на данный момент наиболее интересная и давно ожидаемая новинка, которая появилась в ASP.NET AJAX 4. Такой рендеринг позволяет использовать HTML-шаблоны для определения нужной вам разметки и синтаксис на основе текста (text-based syntax) для подстановки данных периода выполнения. Все это очень похоже на связывание с данными на серверной стороне с тем исключением, что оно происходит в клиентском браузере с внешними данными, получаемыми через веб-сервисы.

В прошлый раз я объяснил основы нового клиентского элемента управления DataView и методики связывания, которые будут применяться чаще всего. В этой статье будет сделан еще один шаг — мы рассмотрим условный рендеринг шаблонов (conditional template rendering).

Условный рендеринг шаблонов

HTML-шаблон для связывания с данными в ASP.NET AJAX 4 — это блок разметки, которая может содержать ASP.NET-разметку, литералы HTML и некоторые блоки подстановки для данных периода выполнения. Алгоритм рендеринга довольно прост: элемент управления DataView, связанный с такими шаблонами, извлекает некие данные и использует их для заполнения шаблонов. Затем полученная в итоге разметка, где блоки подстановки замещены реальными данными, отображается вместо исходного HTML-шаблона.

Создать экземпляр DataView можно двумя способами: декларативным или программным. Однако алгоритм генерации разметки остается одним и тем же. Как раз на этом месте мы и остановились в прошлый раз.

Тут сразу возникает естественный вопрос. А как быть, если для рендеринга шаблона требуется какая-то логика? Например, вам нужен условный рендеринг, который дает разную разметку в зависимости от условий в период выполнения. Для проверки значений связанных элементов данных и состояния других глобальных JavaScript-объектов в разметку придется вплетать какой-то код на клиентской стороне.

В ASP.NET AJAX 4 определен специальный набор атрибутов с именами пространств, с помощью которых вы подключаете к HTML-шаблону собственные поведения. Эти поведения представляют собой JavaScript-выражения, которые должны оцениваться и выполняться на очень специфических стадиях процесса рендеринга. Предопределенные атрибуты кода для условного рендеринга шаблонов перечислены в табл. 1.

Атрибуты кода распознаются формирователем шаблонов (template builder), и их содержимое должным образом используется в процессе рендеринга. Атрибуты, показанные в табл. 1 можно подключать к любым DOM-элементам в шаблоне ASP.NET AJAX.

Заметьте, что атрибуты из табл. 1 были видимы в пространстве имен code вплоть до версии Preview 4 библиотеки ASP.NET AJAX 4 и Visual Studio 2010 beta 1. Начиная с Preview 5 группа ASP.NET убрала пространство имен code и теперь использует только пространство имен sys.

Табл. 1. Атрибуты для условного рендеринга DOM-элементов в шаблоне

Условный рендеринг в действии

Атрибуту sys:if присваивается булево выражение. Если это выражение возвращает true, рендеринг элемента выполняется; в ином случае алгоритм переходит к следующему шагу. Вот тривиальный пример — просто чтобы быстро проиллюстрировать сказанное:

<div sys:if="false">
:
</div>

Обрабатывая эту разметку, формирователь игнорирует тег DIV и все его содержимое. В каком-то смысле атрибут sys:if можно использовать для того, чтобы закомментировать части шаблона на этапе разработки. В конце концов, присваивание значения константы false атрибуту sys:if мало чем отличается от такого выражения на C#:

if (false)
{
  :
}

Установка sys:if в false не обязательно приведен к скрытию HTML-элемента. Стоит отметить, что любой шаблон ASP.NET AJAX изначально обрабатывается браузером как простой HTML. То есть любой шаблон полностью обрабатывается до дерева объектной модели документа (DOM). Однако, поскольку шаблон ASP.NET AJAX дополнен атрибутом sys-template, при отображении на странице не появится ни один из элементов дерева DOM. Фактически атрибут sys-template — это CSS-класс, содержащий:

.sys-template { display:none; visibility:hidden; }

Атрибут sys:if изымает HTML-элемент из реальной разметки для шаблона. Этот атрибут игнорируется, если он подключен к HTML-элементу вне какого-либо шаблона ASP.NET AJAX. В настоящее время атрибут sys:if не имеется ветвления else.

Атрибуты sys:codebefore и sys:codeafter, если они определены, выполняются соответственно до и после рендеринга HTML-элемента. Эти два атрибута можно использовать совместно или индивидуально — выбор за вами. Содержимым атрибутов должен быть блок исполняемого JavaScript-кода.

В общем, атрибуты кода дают достаточно возможностей, чтобы справиться почти с любой ситуацией, пусть даже решение не всегда получается прямолинейным. А потому мы рассмотрим менее тривиальный пример использования sys:if и sys:codebefore.

(Кстати, вы, вероятно, заметили несколько странно выглядящих переменных с префиксами $ в предыдущем коде. Позвольте мне кратко пояснить их, прежде чем переходить к примеру.)

Псевдопеременные шаблона

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

В настоящее время в документации они обозначаются как псевдостолбцы (pseudo-columns), но лично я предпочитаю термин «псевдопеременные» (pseudo-variables). Все они перечислены в табл. 2.

Эти псевдопеременные дают некоторое представление о внутреннем состоянии механизма рендеринга в процессе его работы. Вы можете использовать любую из таких переменных так же, как и любые JavaScript-переменные в шаблоне ASP.NET AJAX.

Табл. 2. Псевдопеременные, поддерживаемые формирователем шаблонов ASP.NET AJAX

Окрашивание искомых строк в таблице

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


Рис. 3. Пример страницы ASP.NET AJAX с условным рендерингом шаблона

На рис. 4 показана разметка страницы-примера. Как видите, это страница контента (content page), сопоставленная с эталонной страницей (master page). Необходимое пространство имен Sys объявляется в теге Body, определенном в эталонной странице.

Рис. 4. Атрибуты кода в действии

<asp:Content ContentPlaceHolderID="PH_Body" runat="server">
<asp:ScriptManagerProxy runat="server" ID="ScriptManagerProxy1">
<Scripts>
<asp:ScriptReference Name="MicrosoftAjax.js"
Path="~/MicrosoftAjax.js" />
<asp:ScriptReference Path="~/MicrosoftAjaxTemplates.js" />
</Scripts>
</asp:ScriptManagerProxy>
<div>
<asp:DropDownList ID="listOfCountries" runat="server"
ClientIDMode="Static"
onchange="listOfCountries_onchange()">
</asp:DropDownList>
<table id="gridLayout">
<tr>
<th>ID</th>
<th>COMPANY</th>
<th>COUNTRY</th>
</tr>
<tbody id="grid" class="sys-template">
<tr sys:if="$dataItem.Country != currentCountry">
<td align="left">{{ ID }}</td>
<td align="right">{{ CompanyName }}</td>
<td align="right">{{ Country }}</td>
</tr>
<tr sys:if="$dataItem.Country == currentCountry"
class="highlight">
<td align="left"
sys:codebefore="if($dataItem.Country == 'USA') {
$element.style.color = 'orange';
$element.style.fontWeight=700;
}">
{{ ID }}
</td>
<td align="right">{{ CompanyName }}</td>
<td align="right">{{ Country }}</td>
</tr>
</tbody>
</table>
</div>
</asp:Content>

Заметьте, что в Preview 5 инфраструктуры ASP.NET AJAX нужно обязательно заменить файл MicrosoftAjax.js, поставляемый с элементом управления ScriptManager и Beta 1. Это временное исправление, которое не понадобится, когда сборки будут обновлены до версии Beta 2.

Прежде чем рассматривать шаблоны ASP.NET AJAX, обсудим код разметки для элемента управления «раскрывающийся список».

Настройка DropDownList

Код для раскрывающегося списка, который позволяет выбирать страны или регионы, выглядит так:

<asp:DropDownList ID="listOfCountries" runat="server" 
     ClientIDMode="Static" 
     onchange="listOfCountries_onchange()">
</asp:DropDownList>

Как видите, этот элемент управления присваивает значение новому свойству ClientIDMode и предоставляет клиентский обработчик для события onchange уровня DOM. Элемент управления связывается со своими данными на сервере в традиционном методе Page_Load:

protected void Page_Load(object sender, EventArgs e)
{
   if (!IsPostBack)
   {
      // Fetch data
      string[] countries = new string [] {"[All]", "USA", ... "};

      // Serialize data to a string
      string countriesAsString = "’[All]’, ‘USA’, ...’";

      // Emit the array in the page
      this.ClientScript.RegisterArrayDeclaration(
           "theCountries", countriesAsString);

      // Bind data to the drop-down list
      listOfCountries.DataSource = countries;
      listOfCountries.DataBind();
   }
}

Процедура связывания проходит в два этапа. Сначала создается JavaScript-массив в ответе, который содержит те же данные, что и программно связанные с элементом управления DropDownList. Затем осуществляется традиционное связывание с данными на серверной стороне.

Эта методика, известная под названием Dual-Side Templating, является разновидностью стандартного связывания с данными на клиентской стороне. Разница заключается в том, что данные, подлежащие связыванию, извлекаются на сервере при первом обращении к странице и передаются клиенту как встроенный JavaScript-массив.

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

Используя серверный элемент управления для создания HTML-разметки, вы не располагаете полным контролем над назначением идентификатора, если применяете эталонную страницу. В ASP.NET AJAX 4 новое свойство ClientIDMode дает вам более гибкие способы решения этой задачи.

В частности, если вы устанавливаете ClientIDMode в Static (как в примере), то клиентский идентификатор HTML-элемента будет точно совпадать с серверным. Этот фокус бесполезен, если вы собираетесь повторять один и тот же серверный элемент управления в контексте элемента управления, связанного с данными и помещенного в шаблон.

Следующий код сценария обрабатывает событие выбора в раскрывающемся списке:

<script language="javascript" type="text/javascript">
    var currentCountry = "";

    function listOfCountries_onchange() {
        // Pick up the currently selected item
        var dd = $get("listOfCountries");
        currentCountry = dd.options[dd.selectedIndex].value;
        
        // Refresh the template
        refreshTemplate();
    }

    function refreshTemplate() {
        var theDataView = $find("grid");
        theDataView.refresh();
    }
</script>

Заметьте, что этот код вызовет ошибку JavaScript, если вы не установите свойство ClientIDMode элемента управления DropDownList в Static. Это связано с манипуляциями над идентификаторами, обычно выполняемыми ASP.NET для того, чтобы при использовании эталонных страниц гарантировать получение каждым генерируемым HTML-элементом уникального идентификатора.

Показанный ранее обработчик события onchange сохраняет название текущей выбранной страны или региона в глобальной JavaScript-переменной, а затем обновляет шаблон ASP.NET AJAX. Теперь сосредоточимся на шаблонах.

Условные шаблоны

Шаблон создается и заполняется программным способом, как показано ниже:

<script language="javascript" type="text/javascript">
   function pageLoad()
   {
      dv = $create(Sys.UI.DataView,
              {
                 autoFetch: true,
                 dataProvider: “/ajax40/mydataservice.asmx",
                 fetchOperation: “LookupAllCustomers"
              },
              {},
              {},
              $get(“grid")
       );
    }
</script>

Клиентский элемент управления DataView вызывает указанный веб-сервис, выполняет заданную операцию выборки и использует любые возвращаемые данные для заполнения шаблона ASP.NET AJAX, встроенный в DOM-элемент с именем «grid».

Рендеринг шаблона в целом осуществляется с применением экземпляра DataView, связанного с возвращаемым значением метода LookupAllCustomers веб-сервиса в примере. Этот метод возвращает набор объектов Customer с такими свойствами, как ID, CompanyName и Country.

Шаблон остается связанным со своими данными в течение всего жизненного цикла страницы независимо от изменений, которые могут происходить в данных. А что если вместо этого вы хотите просто модифицировать рендеринг шаблона — без всякого обновления данных — при изменении определенных условий в период выполнения? Для этого вам нужно вставить в шаблон атрибуты кода.

Здесь вам, по сути, требуется настоящий условный рендеринг: если некое условие соблюдается, рендеринг шаблона идет по одному пути; нет — по другому. Как уже упоминалось, атрибут sys: if не поддерживает семантику «if-then-else», и все, что он позволяет делать, — либо выполнять рендеринг своего родительского элемента, либо игнорировать его в зависимости от значения булева выражения.

Возможный обходной путь для имитации двух ветвлений по условию — использование двух взаимоисключающих частей шаблона, каждая из которых контролируется своим булевым выражением. Такой вариант кода (он показан и на рис. 4) следует схеме, приведенной ниже:

<tr sys:if="$dataItem.Country != currentCountry">
  :
</tr>
<tr sys:if="$dataItem.Country == currentCountry" 
    class="highlight">
  :
</tr>

Переменная currentCountry является глобальной JavaScript-переменной, которая содержит название текущей выбранной страны или региона. Эта переменная обновляется всякий раз, когда HTML-разметкой для серверного элемента управления DropDownList генерируется событие onchange.

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

Когда пользователь делает выбор в раскрывающемся списке, глобальная переменная currentCountry обновляется. Однако эта операция не запускает автоматически какое-либо обновление шаблона, как можно увидеть на рис. 3. Обновление шаблона осуществляется только явным образом в обработчике события onchange. Вот один из возможных вариантов:

var theDataView = $find("grid");
theDataView.refresh();

Функция $find — сокращенный вариант функции lookup, которая в библиотеке Microsoft AJAX получает экземпляры компонентов. Чтобы воспользоваться $find (или $get), вы должны подготовить элемент управления ScriptManager к работе и настроить его так, чтобы он ссылался на базовую библиотеку MicrosoftAjax.js. Получив экземпляр DataView, сопоставленный с шаблоном сетки, вы просто вызываете его метод refresh. На внутреннем уровне этот метод заново компилирует шаблон и обновляет DOM. Заметьте, что вам не обязательно получать экземпляр DataView из списке зарегистрированных компонентов. Вы также можете сохранить экземпляр DataView в глобальной переменной, когда он создается при загрузке страницы:

var theDataView = null;
function pageLoad()
{
   theDataView = $create(Sys.UI.DataView, ...); 
   :
}

Далее в обработчике onchange вы просто вызываете метод refresh глобального экземпляра:

theDataView.refresh();

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

Однако ASP.NET AJAX предоставляет более универсальный механизм запуска событий изменения/уведомления, которые вызывают операции, специфичные для страницы. Давайте переделаем предыдущий пример с использованием простого, формируемого вручную списка вместо DropDownList.

Атрибут sys:command

Список стран или регионов теперь генерируется с помощью HTML-тегов для неупорядоченного маркированного списка. Шаблон выглядит так:

<fieldset>
   <legend><b>Countries</b></legend>
   <ul id="listOfCountries" class="sys-template">
       <li>
           {{ $dataItem }}
       </li>
   </ul>
</fieldset>

Для рендеринга шаблон программно подключается к элементу управления DataView. Данные для заполнения шаблона передаются через встроенный JavaScript-массив. Этот массив, содержащий список стран, принимается от сервера с использованием сервисов объекта ClientScript в классе Page. В отличие от предыдущего примера в код Page_Load не включаются операции связывания на серверной стороне:

protected void Page_Load(object sender, EventArgs e)
{
   if (!IsPostBack)
   {
      string[] countries = new string [] {"[All]", "USA", ... "};
      string countriesAsString = "’[All]’, ‘USA’, ...’";
      this.ClientScript.RegisterArrayDeclaration(
           "theCountries", countriesAsString);
   }
}

При загрузке страницы в клиенте создается второй экземпляр элемента управления DataView. Модифицированный код для JavaScript-функции pageLoad показан на рис. 6.

Рис. 5. JavaScript-функция pageLoad

<script language="javascript" type="text/javascript">
    function pageLoad()
    {
        $create(Sys.UI.DataView,
            {
                autoFetch: true,
                dataProvider: "/ajax40/mydataservice.asmx",
                fetchOperation: "LookupAllCustomers"
            },
            {},
            {},
            $get("grid")
        );
        $create(Sys.UI.DataView,
            {
                autoFetch: true,
                initialSelectedIndex: 0,
                selectedItemClass:"selectedItem",
                onCommand: refreshTemplate,
                data:theCountries
            },
            {},
            {},
            $get("listOfCountries")
        );
    }
</script>

Как видите, второй DataView, примененный для связывания названий стран или регионов с шаблоном на основе UL, имеет совершенно другую структуру.

Первое отличие в том, что свойство data служит для импорта данных. Это корректная процедура, когда используются встроенные данные.

Когда источник данных является массивом пользовательских объектов, связывание осуществляется по синтаксису {{выражение}}. Содержимое выражения — это обычно имя открытого свойства, которое предоставляется элементом данных. В этом примере, напротив, источником связываемых данных служит обыкновенный строковый массив. Соответственно элемент данных — строка без всяких открытых свойств, на которые можно было бы ссылаться в выражении привязки. Тогда вы прибегаете к такому варианту:

<ul>
  <li>{{ $dataItem }}</li>
</ul>

Свойства initialSelectedIndex и selectedItemClass настраивают ожидаемое поведение DataView, если дело касается выбора отображаемого элемента.

К DataView можно подключать шаблон со встроенным поведением выбора. К элементу в позиции, определяемой initialSelectedIndex, будут применяться стили в соответствии с CSS-классом, заданным через свойство selectedItemClass. Вы записываете в initialSelectedIndex значение –1, если при первом отображении вам не нужно делать никакого выбора.

Список, который получается по шаблону, является обыкновенным UL-списком и, как таковой, не содержит никакой логики обработки выбора:

<ul>
  <li>[All]</li>
  <li>USA</li> 
  :
</ul>

Используя атрибут sys:command в HTML-элементе, который находится внутри шаблона, вы указываете формирователю шаблона динамически присоединять к этому элементу группу обработчиков событий:

<ul id="listOfCountries" class="sys-template">
    <li sys:command="select">
        {{ $dataItem }}
    </li>
</ul>

На рис. 6 показано, как такие модифицированные LI-элементы отображаются в окне Developer Tools браузера Internet Explorer 8. Атрибут sys:command принимает строковое значение, представляющее имя инициируемой команды. Это имя определяется вами. Команды срабатывают при щелчке элемента. Наиболее часто применяемые команды — select, delete, insert и update. При запуске команды DataView генерирует событие onCommand. Вот код, обрабатывающий событие onCommand и обновляющий шаблон:

<script type="text/javascript">
    var currentCountry = "";
    function refreshTemplate(args) 
    {
      if (args.get_commandName() == "select") 
      {
        // Pick up the currently selected item
        currentCountry = args.get_commandSource().innerHTML.trim();
                
        // Refresh
        var theDataView = $find("grid");
        theDataView.refresh();
      }
    }
</script>


Рис. 4. Обработчики событий, динамически добавленные в результате действия sys:command

Тот же подход применим и к раскрывающемуся списку, если только вы создаете его напрямую в HTML:

<select>
  <option sys:command="select"> {{ $dataItem }} </option>
</select>

Но заметьте, что одна ошибка в первой бета-версии не позволяет предыдущему коду работать так, как ожидается (см. рис. 7).


Рис. 5. Для обработки выбора используются команды

HTML-атрибуты

В ASP.NET AJAX 4 особая группа атрибутов sys: определяет конкретные привязки для HTML-атрибутов. С функциональной точки зрения, эти атрибуты похожи на HTML-атрибуты, и вы не заметите никакой разницы в их поведении. HTML-атрибуты, помещенные в пространство имен, перечислены в табл. 8.

Все атрибуты элементов в шаблоне могут быть дополнены префиксом пространства имен sys. Каков же высший смысл использования сопоставленных атрибутов (mapped attributes)? И почему в табл. 8 перечислены лишь немногие из них?

Табл. 3. Сопоставленные с HTML атрибуты

Очень часто требуется связывание с HTML-атрибутами, но вы вряд ли захотите применять сам атрибут к своему выражению привязки {{...}}. С точки зрения DOM, выражение привязки — просто значение, присвоенное атрибуту, поэтому оно так и обрабатывается. Это, конечно, может давать некоторые неприятные побочные эффекты. Например, если вы выполняете привязку к атрибуту value элемента input или к содержимому элемента, строка привязки может на мгновение появиться перед глазами пользователя в процессе загрузки страницы. То же самое происходит, если вы используете выражения привязки вне шаблона (скажем, применяете постоянно обновляемую или двухстороннюю привязку). Кроме того, существует группа HTML-атрибутов (перечисленных в табл. 3), где использование выражений привязки может вызывать нежелательные эффекты. Рассмотрим, например, такую разметку:

<img src="{{ URL }}" />

Она инициирует запрос строки «URL» вместо значения свойства URL элемента данных.

Могут возникать и другие проблемы, в том числе связанные с проверкой XHTML на допустимость и неправильным разрешением атрибутов браузером. Если вы дополните такие критические атрибуты префиксом пространства имен sys, все эти проблемы будут устранены.

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

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

Совершенно новый мир

Шаблоны и связывание с данными открывают совершенно новый мир возможностей разработчикам для ASP.NET AJAX. В следующий раз я расскажу о различных типах привязки и представлениях «родительский/дочерний».   

Дино Эспозито (Dino Esposito) — архитектор в компании IDesign и соавтор книги «Microsoft .NET: Architecting Applications for the Enterprise» (Microsoft Press, 2008). Проживает в Италии и часто выступает на отраслевых мероприятиях по всему миру. С ним можно связаться через его блог weblogs.asp.net/despos.