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

Развитие шаблонов дизайна сайтов. Верифицируемые шаблоны.

Автор: Alexander S. Klimov
SWsoft Inc.

Источник: RSDN Magazine #3-2006
Опубликовано: 06.12.2006
Исправлено: 28.02.2007
Версия текста: 1.1
Введение. Описание проблемы.
Типичная задача
Требования к движку страничек и формату шаблонов
Перебор существующих решений
Предлагаемый подход: html-шаблоны с абстрактными элементами управления
Основные идеи формата шаблонов
Плюсы и минусы описываемого формата шаблонов дизайна
Реализация на базе ASP.NET 2.0

Введение. Описание проблемы.

На данный момент существует много решений для создания сайтов с подгружаемыми шаблонами дизайна - как широко используемых (Smarty, Liquid Ruby template engine, Contemplate, Expose и пр.), так и созданных самостоятельно (например, портал Blogger.com использует собственный формат шаблонов дизайна). В данной статье рассматриваются те или иные недостатки существующих систем, а затем предлагается решение, основанное на новых возможностях платформы ASP.NET 2.0. Данное решение появилось в процессе работы над проектом SiteBuilder for Windows, но лежащие в его основе идеи могут быть использованы и в других проектах.

Типичная задача

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

Требования к движку страничек и формату шаблонов

Укажем конкретные требования к решению указанной задачи.

Перебор существующих решений

Самые первые шаблоны дизайна были набором html-страниц с переменными, которые движок мог распознать в html-коде и заменить значениями этих переменных.

Пример файла из такого шаблона дизайна:

<h1>Hello $Name$!</h1>

Код страницы, к которому применяется этот шаблон дизайна, присваивает переменной $Name$ значение «World», после этого к работе подключается движок сайта, который производит обычную замену и отдает на выход:

<h1>Hello World!</h1>

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

ПРИМЕЧАНИЕ

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

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

{* этот пример напечатает все переменные массива $custid *}
{foreach from=$custid item=curr_id}
    id: {$curr_id}<br />
{/foreach}

Теперь, если в коде страницы, для которой описан этот шаблон, объявить такой массив:

<?php
$arr = array(1000,1001,1002);
$smarty->assign('custid', $arr);
?>

То движок выведет:

id: 1000<br/>
id: 1001<br/>
id: 1002<br/>

Используя данный подход, уже можно создавать мощные шаблоны дизайна. Причем Smarty и Liquid выгодно отличает от многих других решений то, что указанные операторы интерпретируются только движком шаблонов. В них не содержится непосредственно серверного кода, как это можно наблюдать в примере шаблона PHP-Nuke или Zen Cart. Применение шаблона дизайна PHP-Nuke запросто может сломать даже защиту сайта, не говоря уже о гарантированном сохранении поведения этого сайта.

Но даже Smarty и Liquid Ruby не позволяют дизайнеру создавать шаблоны дизайна без помощи программиста. Описание разметки страницы с помощью императивных конструкций уже требует навыков программирования. Тем более что данный подход опять же не защищен от изменения поведения страницы после применения такого шаблона дизайна. То есть человек, создающий такой шаблон дизайна, должен хорошо понимать, как работает страница, чтобы не сломать ее логику работы.

ПРИМЕЧАНИЕ

Эта же критика применима к случаю, когда перечисленные императивные операторы выражаются в терминах XSLT. Имеется в виду решение, когда страница подает движку данные в виде XML, движок применяет XSLT-преобразование, которое подготовил дизайнер, и в результате получается html-страничка. Но такой подход скрывает еще более глубокие проблемы, чем невозможность проверки сохранения логики поведения страницы. Дело в том, что задача создания такого XSLT-шаблона может вызвать затруднение даже у программиста, что уж говорить о дизайнере? Ведь выражение обычных циклов, условий и прочих операторов через конструкции XSL – не самое естественное и распространенное. Так что в этом случае много сил тратится на само создание шаблонов дизайна – чаще всего сначала дизайнер подготавливает html-заготовку, которую программист переводит в XSLT-шаблон.

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

Например, необходимо описать форму авторизации. На форме есть два поля «Имя пользователя», «Пароль пользователя» и кнопка «Отправить». При нажатии на кнопку «Отправить» необходимо делать проверку заполнения полей и отправлять форму на сервер.

Вот как может выглядеть html-код такой формы (определение всех javascript-функций вынесено за пределы этой формы):

<div id="LoginForm" 
     onkeypress="javascript:return WebForm_FireDefaultButton(event, 'Login')">
  <table cellpadding="5" cellspacing="0" border="0">
    <tr>
      <td width="30%">User name <span id="UserNameRequiredFieldValidator" 
          style="color: Red; visibility: hidden;">*</span> 
      </td>
      <td width="70%">
        <input name="UserNameInput" type="text" value="asdasd" 
               id="UserNameInput" style="width: 100%" />
      </td>
    </tr>
    <tr>
      <td>Password 
       <span id="PasswordRequiredFieldValidator" 
             style="color: Red; visibility: hidden;">
        *</span> 
      </td>
      <td>
        <input name="PasswordInput" type="password" 
               id="PasswordInput" style="width: 100%" />
      </td>
    </tr>
    <tr>
      <td colspan="2" align="right">
        <input type="submit" name="Login" value="Login" 
          onclick="javascript:WebForm_DoPostBackWithOptions(
            new WebForm_PostBackOptions(&quot;Login&quot;, &quot;&quot;, 
            true, &quot;&quot;, &quot;&quot;, false, false))"
          id="Login" />
      </td>
    </tr>
  </table>
</div>
<script type="text/javascript">
<!--
var UserNameRequiredFieldValidator = document.all 
  ? document.all["UserNameRequiredFieldValidator"] 
  : document.getElementById("UserNameRequiredFieldValidator");
UserNameRequiredFieldValidator.controltovalidate = "UserNameInput";
UserNameRequiredFieldValidator.errormessage = "*";
UserNameRequiredFieldValidator.evaluationfunction 
  = "RequiredFieldValidatorEvaluateIsValid";
UserNameRequiredFieldValidator.initialvalue = "";
var PasswordRequiredFieldValidator = document.all 
  ? document.all["PasswordRequiredFieldValidator"] 
  : document.getElementById("PasswordRequiredFieldValidator");
PasswordRequiredFieldValidator.controltovalidate = "PasswordInput";
PasswordRequiredFieldValidator.errormessage = "*";
PasswordRequiredFieldValidator.evaluationfunction 
  = "RequiredFieldValidatorEvaluateIsValid";
PasswordRequiredFieldValidator.initialvalue = "";
// -->
</script>

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

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

Итак, подведем итог. Современные форматы шаблонов дизайна имеют следующие проблемы:

Учитывая эти проблемы, можно оценить формат шаблонов, использованный на портале Blogger.com. Ниже приведен код одного из шаблонов дизайна, который применяется к странице списка сообщений блога.

Пример дизайна странички списка сообщений блога на Blogger.com

Как видно из этого примера – шаблоны дизайна блогов на Blogger.com избавлены от императивных выражений для описания разметки страницы. Список здесь описывается с помощью xml-тегов: повторяющиеся элементы списка описываются в специальных тегах. Javascript на странице отсутствует. Это позволяет продвинутым пользователям изменять шаблон дизайна странички, создавая свой, уникальный дизайн блога.

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

<BlogItemUrl><a href="<$BlogItemUrl$>" title="external link"></BlogItemUrl>
  <$BlogItemTitle$>
<BlogItemUrl></a></BlogItemUrl>

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

Предлагаемый подход: html-шаблоны с абстрактными элементами управления

Основные идеи формата шаблонов

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

Вот как может выглядеть описанная выше форма авторизации в технологии ASP.NET 2.0:

<asp:Panel ID="LoginForm" runat="server" DefaultButton="Login">
  <table cellpadding="5" cellspacing="0" border="0">
    <tr>
      <td width="30%">User name
        <asp:RequiredFieldValidator ID="UserNameRequiredFieldValidator" 
          ErrorMessage="*" runat="server" Display="Static" 
          ControlToValidate="UserNameInput" />
      </td>
      <td width="70%">
        <asp:TextBox ID="UserNameInput" runat="server" style="width: 100%" />
      </td>
    </tr>
    <tr>
      <td>Password
        <asp:RequiredFieldValidator ID="PasswordRequiredFieldValidator" 
          ErrorMessage="*" runat="server" Display="Static" 
          ControlToValidate="PasswordInput" />
      </td>
      <td>
        <asp:TextBox ID="PasswordInput" runat="server" TextMode="Password" 
          style="width: 100%" />
      </td>
    </tr>
    <tr>
      <td colspan="2" align="right">
        <asp:Button ID="Login" runat="server" Text="Login" 
          OnClick="Login_Click" />
      </td>
    </tr>
  </table>
</asp:Panel>

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

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

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

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

<table cellpadding="5" cellspacing="0" border="0">
  <tr>
    <td width="30%">$UserName$
      <SiteBuilder:ValidationText display="Static" id="UserNameIsRequired" />
    </td>
    <td width="70%">
      <SiteBuilder:TextInput id="UserNameInput" style="width: 100%" />
    </td>
  </tr>
  <tr>
    <td width="30%">$Password$
      <SiteBuilder:ValidationText display="Static" id="PasswordIsRequired" />
    </td>
    <td width="70%">
      <SiteBuilder:TextInput id="PasswordInput" style="width: 100%;" />
    </td>
  </tr>
  <tr>
    <td align="right">
      <SiteBuilder:Button id="Login" />
    </td>
  </tr>
</table>

Здесь ValidationText, TextInput и Button – абстрактные элементы управления, имеющие только стилевые свойства. $UserName$ и $Password$ – это обычные переменные шаблона, которые после обработки движком сайта будут заменены конкретными значениями (предназначенными для поддержки разных языков).

Важным здесь является то, что TextInput может в действительности оказаться гораздо сложнее обычного тега input – например, это может быть сложное поле ввода, как в Google Suggest. Но дизайнера при этом будет заботить только внешний вид этого поля, и работать он будет по-прежнему с абстрактным элементом управления TextInput. То же самое относится к любому другому абстрактному элементу управления.

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

<div style="padding: 10px; margin: 10px 0 0 0;">
  $CategoryDescription$
</div>
<sitebuilder:list id="EntryList" style="width: 100%;">
  <ItemTemplate>
    <table cellpadding="0" cellspacing="0" border="0" width="100%" 
                   style="border-collapse: collapse; margin-top: 10px;">
      <tr>
        <td class="mod-item-header" style="padding: 5px 10px;">
          <div style="float:left;">
            <b>$Title$</b>
          </div>
          <div style="float:right;">$Time$</div>
        </td>
      </tr>
      <tr>
        <td class="mod-item-body" style="padding: 5px 10px;">
          <div id="truncationEnvelope">$Entry$</div>
          <div align="right">
            <!-- эта ссылка видна, если вхождение слишком велико -->
            <SiteBuilder:Link ID="ReadMore" class="mod-item-body-a-strong" />
            &nbsp;
            <SiteBuilder:Link ID="ToComments" class="mod-item-body-a-strong"/>
          </div>
        </td>
      </tr>
    </table>
  </ItemTemplate>
  <AlternatingItemTemplate>
    <table cellpadding="0" cellspacing="0" border="0" width="100%" 
           style="border-collapse: collapse; margin-top: 10px;">
      <tr>
        <td class="mod-item-header" style="padding: 5px 10px;">
          <div style="float:left;">
            <b>$Title$</b>
          </div>
          <div style="float:right;">$Time$</div>
        </td>
      </tr>
      <tr>
        <td class="mod-item-body-alter" style="padding: 5px 10px;">
          <div id="Div1">$Entry$</div>
          <div align="right">
            <!-- эта ссылка видна, если вхождение слишком велико -->
            <SiteBuilder:Link ID="ReadMore" class="mod-item-body-a-strong"/>
            &nbsp;
            <SiteBuilder:Link ID="ToComments" class="mod-item-body-a-strong"/>
          </div>
        </td>
      </tr>
    </table>
  </AlternatingItemTemplate>
</sitebuilder:list>
<sitebuilder:container id="Pager" />
<div align="right">
  <!-- Видно, если пользователь выбирает месяц и год -->
  <sitebuilder:link id="BackLink" />
</div>

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


Рисунок 1. Пример разметки по умолчанию.


Рисунок 2. Пример измененной разметки.

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

public class ListPostsComponent : ModulePageComponent
{
  public ListPostsComponent()
    : base("ListPosts", "ListPosts.page")
  {
    _controls.Add(new TextDiv("CategoryDescription"));

    List entryList = new List("EntryList", true);
    entryList.ItemControls.Add(new TextDiv("Title"));
    entryList.ItemControls.Add(new TextDiv("Time"));
    entryList.ItemControls.Add(new TextDiv("Entry"));
    entryList.ItemControls.Add(new Link("ReadMore", true));
    entryList.ItemControls.Add(new Link("ToComments", true));
    entryList.AlternatingItemControls.AddRange(entryList.ItemControls);
    _controls.Add(entryList);

    _controls.Add(new ContainerDescriptor("Pager", true, Pager.BuildMarkupTag));
    _controls.Add(new Link("BackLink", true));
  }
}

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

Верификатор/компилятор таких шаблонов может работать по следующему алгоритму:

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

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

И наконец, данный формат не налагает никаких ограничений на реализацию движка сайта. В следующей главе будет предложено решение основанное на технологии ASP.NET 2.0, но работать с этим форматом может и движок, написанный на java, php и прочих языках.

Посмотреть, как работает это решение в действии, и просмотреть предлагаемую абстрактную библиотеку элементов управления, вы можете, скачав SiteBuilder SDK и установив Templates из предлагаемых компонентов (остальные компоненты вам не понадобятся). Произвольную разметку на данный момент там поддерживают модули Blog и Guestbook.

Плюсы и минусы описываемого формата шаблонов дизайна

Подведем итоги предыдущего раздела. Сначала плюсы:

Теперь минусы. От них никуда не деться. Задача создания шаблонов дизайна – достаточно общая, чтобы иметь противоречивые требования. Данное решение удовлетворяет требованиям, описанным в начале этой статьи. Теперь поговорим о том, какие требования пострадали:

Реализация на базе ASP.NET 2.0

Технология ASP.NET 2.0 предоставляет механизм Application Theme, позволяющий задать значения группы свойств элементов управления, отвечающих за их визуальное представление. Этот механизм дает возможность изменить вид всех элементов управления, присутствующих на всех страницах сайта. Если научиться с помощью этого механизма менять разметку страницы, то решение будет получено. Стандартной поддержки такой возможности в классе System.Web.UI.Page нет. Но нечто очень похожее есть в новом элементе управления Login. Он содержит некую разметку по умолчанию. Однако при желании можно определить произвольную html-разметку с ASP.NET-элементами управления, используя свойство LayoutTemplate. Но что самое интересное – это свойство может быть переопределено в каждой Application Theme. То есть любая тема сайта может определить свою собственную html-разметку для всех Login форм.

ПРИМЕЧАНИЕ

Причем здесь реализована очень хорошая идея: от каждого элемента управления на этой форме не требуется быть конкретным классом. Достаточно поддерживать некоторый общий интерфейс. Например, поле ввода не обязано быть ASP.NET-элементом управления TextBox. Вместо него можно использовать любой класс, который реализует интерфейс IEditableTextControl. При этом некоторые элементы управления являются необязательными – например, разметка вполне может не определять флажок RememberMe.

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

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

Класс для поиска различных элементов управления по идентификатору и типу в произвольной разметке.
public class Container : WebControl, INamingContainer
{
  public Container()
  {
  }

  private ControlType FindControl<ControlType>(
    string id, bool required, params Type[] additionalTypeRestrictions)
    where ControlType : class
  {
    Control control = this.FindControl(id);
    ControlType ret = control as ControlType;
    if (ret != null)
    {
      if (additionalTypeRestrictions == null 
        || additionalTypeRestrictions.Length == 0
      )
        return ret;

      bool missedType = false;
      Type returnType = ret.GetType();

      foreach (Type type in additionalTypeRestrictions)
      {
        if (!type.IsAssignableFrom(returnType))
        {
          missedType = true;
          break;
        }
      }

      if (!missedType)
        return ret;
    }

    if (required)
    {
      string types = typeof(ControlType).FullName;

      if (additionalTypeRestrictions != null && additionalTypeRestrictions.Length > 0)
      {
        StringBuilder sb = new StringBuilder(types);

        foreach (Type type in additionalTypeRestrictions)
          sb.AppendFormat(", {0}", type.FullName);

        types += sb.ToString();
      }

      throw new InvalidOperationException(
        string.Format("Template is broken: missed required control "
          + "with id {0} and type(s): {1}.", id, types));
    }

    return null;
  }

  public ControlType FindRequiredControl<ControlType>(
    string id, params Type[] additionalTypeRestrictions)
    where ControlType : class
  {
    return FindControl<ControlType>(id, true, additionalTypeRestrictions);
  }

  public ControlType FindOptionalControl<ControlType>(
    string id, params Type[] additionalTypeRestrictions)
    where ControlType : class
  {
    return FindControl<ControlType>(id, false, additionalTypeRestrictions);
  }
}

Теперь можно определить сам элемент управления, допускающий произвольную разметку, которую, в свою очередь, можно подменить в конкретной Application theme:

public class PagePanel : CompositeControl
{
  private ITemplate _Template;
  [PersistenceMode(PersistenceMode.InnerProperty)]
  [Browsable(false)]
  [TemplateContainer(typeof(PagePanel))]
  public ITemplate Template
  {
    get { return _Template; }
    set
    {
      _Template = value;
      this.ChildControlsCreated = false;
    }
  }

  private Container _TemplateContainer;
  public Container TemplateContainer
  {
    get
    {
      EnsureChildControls();
      return _TemplateContainer;
    }
  }

  private TemplateWrapper _renderingWrapper;
  public TemplateWrapper RenderingWrapper
  {
    get
    {
      EnsureChildControls();
      return _renderingWrapper;
    }
  }

  public ControlType FindRequiredControl<ControlType>(
    string id, params Type[] additionalTypeRestrictions)
    where ControlType : class
  {
    return TemplateContainer.FindRequiredControl<ControlType>(
      id, additionalTypeRestrictions);
  }

  public ControlType FindOptionalControl<ControlType>(
    string id, params Type[] additionalTypeRestrictions)
    where ControlType : class
  {
    return TemplateContainer.FindOptionalControl<ControlType>(
      id, additionalTypeRestrictions);
  }

  protected override void CreateChildControls()
  {
    this.Controls.Clear();
    if (Template == null)
      throw new InvalidOperationException("Missed template definition.");

    _TemplateContainer = new Container();
    Template.InstantiateIn(_TemplateContainer);
    _renderingWrapper = new TemplateWrapper(_TemplateContainer);
    _renderingWrapper.ID = "RenderingWrapper";
    this.Controls.Add(_renderingWrapper);
  }

  public override void RenderBeginTag(HtmlTextWriter writer)
  {
  }

  public override void RenderEndTag(HtmlTextWriter writer)
  {
  }
}

Данный класс позволяет определить внутри себя произвольную разметку, а потом работать с ней с помощью методов FindOptionalControl и FindRequiredControl. Класс TemplateWrapper, использованный здесь – это наследник ASP.NET-элемента управления Panel. Содержимое страницы оборачивается этим классом, чтобы получить маленькие радости: кнопку по умолчанию, возможность ограничить при желании размеры содержимого и прочее.

В принципе, для данной статьи этот класс особого смысла не несет, но для полноты ниже приведен также и его код:

public class TemplateWrapper : Panel
{
  Container _container;

  public TemplateWrapper(Container container)
  {
    _container = container;
    Controls.Add(container);
  }

  public override System.Web.UI.Control FindControl(string id)
  {
    return _container.FindControl(id);
  }
}

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

<asp:Content ID="ContentStep" runat="server" 
             ContentPlaceHolderID="SomeContentID">
  <h1>Test page</h1>

  <asp:Label ID="WelcomeMessage" runat="server"/><br/>

  <asp:TextBox ID="Name" runat="server"/> 
  <asp:RequiredFieldValidator Display="Static" ID="NameIsRequired" 
                              runat="server" 
                              ControlToValidate="Name" 
                              ValidationGroup="SampleForm" />
  <asp:Button ID="SayHello" ForeColor="Green" 
              runat="server" ValidationGroup="SampleForm" />
</asp:Content>

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

<asp:Content ID="ContentStep" runat="server" 
             ContentPlaceHolderID="SomeContentID">
  <SiteBuilder:PagePanel ID="SomePanel" SkinID="PageName" runat="server">

    <h1>Test page</h1>

    <asp:Label ID="WelcomeMessage" runat="server"/><br/>

    <asp:TextBox ID="Name" runat="server"/> 
    <asp:RequiredFieldValidator Display="Static" 
                                ID="NameIsRequired" 
                                runat="server" />
    <asp:Button ID="SayHello" ForeColor="Green" runat="server"/>

  </SiteBuilder:PagePanel>
</asp:Content>

Все содержимое страницы было помещено внутрь элемента PagePanel, SkinID которого связан с именем страницы.

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

public partial class PageName : Page
{
  private ITextControl WelcomeMessage
  {
    get
    {
      return SomePanel.FindRequiredControl<ITextControl>("WelcomeMessage");
    }
  }

  private IEditableTextControl Name
  {
    get
    {
      return SomePanel.FindRequiredControl<IEditableTextControl>("Name");
    }
  }

  private IButtonControl SayHello
  {
    get { return SomePanel.FindRequiredControl<IButtonControl>("SayHello"); }
  }

  private BaseValidator NameIsRequired
  {
    get
    {
      return SomePanel.FindRequiredControl<BaseValidator>("NameIsRequired");
    }
  }

  protected override void OnInit(EventArgs e)
  {
    base.OnInit(e);
    SayHello.Click += new EventHandler(SayHello_Click);
  }

  protected void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack)
    {
      string validationGroup = "SampleForm";
      NameIsRequired.ControlToValidate = ((Control)Name).ID;
      NameIsRequired.ValidationGroup = validationGroup;
      NameIsRequired.Text = "*";
      SayHello.Text = "Say Hello";
      SayHello.ValidationGroup = validationGroup;
    }
  }

  void AddComment_Click(object sender, EventArgs e)
  {
    if (!Page.IsValid)
      return;

    WelcomeMessage.Text = "Hello, " + HttpUtility.HtmlEncode(Name.Text);
  }
}

Посмотрим, что изменилось:

К сожалению, дизайнер Web-форм Visual Studio Designer не в полной мере поддерживает такую реализацию PagePanel. Если данный фактор будет действительно существенным (что маловероятно), можно расширить класс PagePanel design-time поддержкой.

Используя этот подход, вы уже можете начать создание своего движка с поддержкой произвольной разметки. Надеюсь, прочитанная статья дала вам идеи, которые вы сможете использовать в своих проектах. Удачи и спасибо за внимание!


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