|
Концепция серверных элементов управления, предложенная Microsoft в
ASP.NET, поднимает возможности веб программистов на невиданную доселе
высоту. Теперь можно забыть о кропотливом создании отдельных кусков сешанного
html/asp кода для решения типовых задач (например вывода данных в таблицу с
возможностью сортировки и постраничного вывода) и копирования этих кусков кода в
нужные места страниц. Достаточно всего лишь один раз создать класс серверного
элемента управления, реализующий данную функциональность и скомпилировать
его в сборку и можно использовать получившийся сэлемент управления в любых своих
проектах (и даже распространять его дабы и другие программисты могли насладиться
его функциональностью :)).
Что же такое серверный элемент управления с точки зрения ASP.NET
программиста? Это класс, родителем которого (явным или неявным) является класс
System.Web.UI.Control (на самом деле это несколько не так, но для веб
программиста данного определения хватит с головой). Данный класс представляет
базовую функциональность для элемента управления -например размещение в
коллекции серверных элементов управления веб формы и отрисовку. Также он
предоставляет функциональность для своего добавления в панель элементов
управления и работы в дизайн режиме.
Для создания видимых сервеных элементов управления в основном имспользуется
класс System.Web.UI.WebControls.WebControl - наследник класса
System.Web.Ui.Control. В этот класс добавлено множество свойств для визуального
представления серверного элемента управления (например Font, CssClass, etc), а
также добавлена дополнительная функциональность (в том числе и для
отрисовки).
Данная статья, первая из запланированных серии статей, посвященных созданию
серверных элементов управления, посвящена как раз таки созданию кода для
отображения серверного элемента управления. В ней я постараюсь рассказать как
правильно писать код генерации html для элемента управления и какие методы
принимают в этом участие.
Чтобы читателю было интересней сообщу также заранее, что элемент управления,
который начнет создаваться в этой статье, будет полезен любому веб программисту.
Это пейджер, предназначенный для помощи в постраничном выводе данных в табличных
элементах управления. Примерный внешний вид его читатель может увидеть на
рисунке:

Данный элемент управления представляет собой таблицу, содержащую 3 ячейки для
отображения информации о текущей страницы, кнопок для переходов вперед-назад и
кнопок для переходов на конкретную страницу. У него есть следующие, нужные при
рисовании, свойства:
| Свойство |
Описание |
| PageSize |
Размер страницы в строках |
| RowsTotal |
Строк всего в источнике данных |
| CurrentPageIndex |
Текущий номер страницы |
| CurrentPageFormat |
Формат для отображения информации о текущей странице (левая
ячейка) |
Начнем создавать класс элемента управления с описания этих свойств:
private int pageSize = 50;
private int rowsTotal = 0;
private int currentPageIndex = 1;
private string currentPageFormat = "Page {0} of {1}";
[Bindable(true),
Category("Data"),
DefaultValue("50")]
public int PageSize
{
get
{
return pageSize;
}
set
{
pageSize = value;
}
}
[Bindable(true),
Category("Data"),
DefaultValue("0")]
public int RowsTotal
{
get
{
return rowsTotal;
}
set
{
rowsTotal = value;
}
}
[Bindable(true),
Category("Data"),
DefaultValue("1")]
public int CurrentPageIndex
{
get
{
return currentPageIndex;
}
set
{
currentPageIndex = value;
}
}
[Bindable(true),
Category("Appearance"),
DefaultValue("<b>Page</b> {0} of {1}")]
public string CurrentPageFormat
{
get
{
return currentPageFormat;
}
set
{
if(value.IndexOf("{0}") == -1 || value.IndexOf("{1}") == -1)
throw new ArgumentException("Invalid Current Page Format string");
currentPageFormat = value;
}
}
Так как в данной статье я не рассматриваю вопросы
сохранения данных между постбеками и работы с ViewState, то все свойства
хранятся в приватных полях класса.
Теперь начнем создания нашего элемента управления. И первые пробы
проведем на элементе управления, унаследованном от System.Web.UI.Control. Но для
начала немного теории.
Класс System.Web.UI.Control использует при отображении элемента
управления три метода и одно свойство. Используемое свойство это ClientID -
клиентский дентификатор элемента управления, уникальный в пространстве
идентификаторов страницы. Методы же следующие:
| Метод |
Описание |
| public void RenderControl(HtmlTextWriter writer) |
Проверяет включено ли отображение элемента управления, и если включено
- вызывает метод Render |
| protected virtual void Render(HtmlTextWriter writer) |
Вызывает метод RenderChildren для отрисовки вложенных элементов
управления |
| protected virtual void RenderChildren(HtmlTextWriter writer) |
Для каждого элемента управления из коллекции Controls данного элемента
управления вызывает его метод RenderControl. |
В данной таблице приведено описание функциональности, заложенной в эти
методы в классе System.Web.UI.Control. При создании элементов управления,
напрямую наследуемых от System.Web.UI.Control, для отрисовки необходимо
переопределить метод Render. Сделаем это.
Генерация html кода для серверного элемента управления происходит с помощью
вызова методов класса HtmlTextWriter.
Первый пример реализации метода Render можно назвать "решением в лоб" -
в нем мы будем рисовать нашу таблицу почти так же, как если бы мы рисовали ее с
помощью вызовов Response.Write. Никогда не пишите html код элемента
управления таким образом - этот кусок кода приведен только для примера!
protected override void Render(HtmlTextWriter output)
{
output.Write("<table align=\"center\" width=\"100%\" cellspacing=\"2\" cellpadding=\"2\">");
int pagesTotal = RowsTotal % PageSize == 0 ? RowsTotal / PageSize : RowsTotal / PageSize + 1;
output.Write("<tr><td align=\"left\">" + currentPageFormat + "</td>", CurrentPageIndex, pagesTotal);
output.Write("<td noWrap align=\"center\">");
if(CurrentPageIndex > 1)
output.Write("<a href=\"#\"><< Previous</a> ");
if(CurrentPageIndex < pagesTotal)
output.Write("<a href=\"#\">Next >></a> ");
output.Write("</td>");
output.Write("<td noWrap align=\"right\">");
int[,] pageRanges = new int[3, 2]
{
{1, 2},
{CurrentPageIndex - 2, CurrentPageIndex + 2},
{pagesTotal - 1, pagesTotal}
};
if (pageRanges[0, 1] + 1 >= pageRanges[1, 0])
{
if(pageRanges[0, 1] < pageRanges[1, 1])
pageRanges[0, 1] = pageRanges[1, 1];
pageRanges[1, 0] = -1000;
}
if((pageRanges[1, 0] != pageRanges[1, 1]) && (pageRanges[1,1] + 1 >= pageRanges[2,0]))
{
if (pageRanges[2, 0] > pageRanges[1, 0])
pageRanges[2, 0] = pageRanges[1, 0];
pageRanges[1, 0] = -1000;
}
if (pageRanges[0, 1] + 1 >= pageRanges[2, 0])
{
pageRanges[0, 1] = pageRanges[2, 1];
pageRanges[2, 0] = -1000;
}
output.Write ("<b>Pages:</b> ");
for(int rangeIndex = 0; rangeIndex <= 2; rangeIndex++)
{
if(pageRanges[rangeIndex, 0] != -1000)
{
if(rangeIndex != 0)
output.Write (". . . ");
int pgIndex = pageRanges[rangeIndex, 0];
do
{
if(pgIndex == CurrentPageIndex)
output.Write(String.Format("<b>{0}</b> ", pgIndex.ToString()));
else
output.Write("<a href=\"#\">" + pgIndex.ToString() + "</a> ");
pgIndex++;
}
while (pgIndex <= pageRanges[rangeIndex,1]);
}
}
output.Write("</td></tr></table>");
}
Описывать логику работы данного метода я думаю нет
смысла - все и так достаточно прозрачно. А вот о чем есть смысл поговорить, так это о том, как правильно использовать класс HtmlTextWriter.
Класс HtmlTextWriter представляет с десяток методов для генерации правильного
html кода. Наиболее важные для разработчиков серверных элементов управления
методы приведены в таблице:
| Метод |
Описание |
| public virtual void RenderBeginTag(HtmlTextWriterTag); |
Записывает открывающий тег html элемента. Тип тега
задается из перечисления HtmlTextWriterTag. Также есть возможность
задавать html элемент строкой. |
| public virtual void
RenderEndTag(); |
Записывает закрывающий тег html элемента. Каждый
тег, открытый с помощью вызова метода RenderBeginTag, должен быть закрыт с
помощью RenderEndTag |
| public virtual void AddAttribute(...); |
Добавляет атрибут в стек. Все добавленные в стек
атрибуты присваиваются к первому html элементу, записанному с помощью
вызова метода RenderBeginTag. |
public virtual void
AddStyleAttribute(...); |
Добавляет стилевой атрибут в стек. Все добавленные
в стек атрибуты присваиваются к первому html элементу, записанному с
помощью вызова метода RenderBeginTag. |
| public override void Write(...); |
Записывает значение передаваемого
параметра. |
Теперь попробуем переписать наш метод Render используя приведенную выше
таблицу. Должно получиться примерно следующее:
protected override void Render(HtmlTextWriter output)
{
int pagesTotal = RowsTotal % PageSize == 0 ? RowsTotal / PageSize : RowsTotal / PageSize + 1;
output.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "2");
output.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "2");
output.AddAttribute(HtmlTextWriterAttribute.Align, "center");
output.AddStyleAttribute(HtmlTextWriterStyle.Width, "100%");
output.RenderBeginTag(HtmlTextWriterTag.Table);
output.RenderBeginTag(HtmlTextWriterTag.Tr);
output.AddAttribute(HtmlTextWriterAttribute.Align, "left");
output.RenderBeginTag(HtmlTextWriterTag.Td);
output.Write(currentPageFormat, CurrentPageIndex, pagesTotal);
output.RenderEndTag();
output.AddAttribute(HtmlTextWriterAttribute.Align, "center");
output.AddAttribute(HtmlTextWriterAttribute.Wrap, "nowrap");
output.RenderBeginTag(HtmlTextWriterTag.Td);
if(CurrentPageIndex > 1)
{
output.AddAttribute(HtmlTextWriterAttribute.Href, "#");
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write("<< Previous");
output.RenderEndTag();
output.Write(" ");
}
if(CurrentPageIndex < pagesTotal)
{
output.AddAttribute(HtmlTextWriterAttribute.Href, "#");
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write("Next >>");
output.RenderEndTag();
}
output.RenderEndTag();
output.AddAttribute(HtmlTextWriterAttribute.Align, "right");
output.AddAttribute(HtmlTextWriterAttribute.Wrap, "nowrap");
output.RenderBeginTag(HtmlTextWriterTag.Td);
int[,] pageRanges = new int[3, 2]
{
{1, 2},
{CurrentPageIndex - 2, CurrentPageIndex + 2},
{pagesTotal - 1, pagesTotal}
};
if (pageRanges[0, 1] + 1 >= pageRanges[1, 0])
{
if(pageRanges[0, 1] < pageRanges[1, 1])
pageRanges[0, 1] = pageRanges[1, 1];
pageRanges[1, 0] = -1000;
}
if((pageRanges[1, 0] != pageRanges[1, 1]) && (pageRanges[1,1] + 1 >= pageRanges[2,0]))
{
if (pageRanges[2, 0] > pageRanges[1, 0])
pageRanges[2, 0] = pageRanges[1, 0];
pageRanges[1, 0] = -1000;
}
if (pageRanges[0, 1] + 1 >= pageRanges[2, 0])
{
pageRanges[0, 1] = pageRanges[2, 1];
pageRanges[2, 0] = -1000;
}
output.RenderBeginTag(HtmlTextWriterTag.B);
output.Write ("Pages:");
output.RenderEndTag();
output.Write(" ");
for(int rangeIndex = 0; rangeIndex <= 2; rangeIndex++)
{
if(pageRanges[rangeIndex, 0] != -1000)
{
if(rangeIndex != 0)
output.Write (". . . ");
int pgIndex = pageRanges[rangeIndex, 0];
do
{
if(pgIndex == CurrentPageIndex)
{
output.RenderBeginTag(HtmlTextWriterTag.B);
output.Write(pgIndex.ToString());
output.RenderEndTag();
output.Write(" ");
}
else
{
output.AddAttribute(HtmlTextWriterAttribute.Href, "#");
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write(pgIndex.ToString());
output.RenderEndTag();
output.Write(" ");
}
pgIndex++;
}
while (pgIndex <= pageRanges[rangeIndex,1]);
}
}
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
}
Кода стало несколько больше, но преимущества
использования данного метода генерации html кода все таки перевешивают. Во
первых в результате выполнения этого кода будет получен правильный и "красивый"
html код ("красивый" в данном контексте означает отформатированный). А во вторых
при генерации html кода элементов управления ASP.NET умеет автоматически
подставлять вместо класса HtmlTextWriter его наследников в зависимости от
установок в секции <browserCaps> файла machine.config. Например если веб
форму запрашивает старый браузер, не понимающий HTML 4.0, класс HtmlTextWriter
будет подменен классом HtmlTextWriter32 и будет сгенерирован html, полностью
соответствующий спецификации HTML 3.2 (например многие стилевые атрибуты будут
заменены на подобные по функциональности html элементы, а вместо элемета div
будет использован элемент table). Так что старайтесь писать правильно код
генерации html и не гонитесь за мнимой быстротой.
Итак элемент управления создан, рисовать он себя умеет, все красиво, все
довольны. Но есть одно большое "но" - нет возможности изенить стили выводимого
элемента управления - например поменять шрифт или цвет фона. И все это потому,
что класс System.Web.UI.Control предназначен для создания невизуальных элементов
управления (например элемента title или xml). А для того, чтобы создавать
отображаемые элементы цправления, предназначен класс System.Web.UI.WebControl,
уже имеющий базовый набор свойств для стилевого оформления элемента управления.
А заодно и расширенный набор методов, отвечающих за отрисовку. Рассмотрим эти
методы и свойства:
| Метод/свойство |
Описание |
| public Style
ControlStyle {get;} |
Класс, содержащий свойства стилевого оформления элемента
управления |
| public bool
ControlStyleCreated {get;} |
Свойство, индикатор того, что стиль элемента управления создан. |
| protected virtual HtmlTextWriterTag
TagKey {get;} |
html тег элемента управления. |
| protected virtual string
TagName {get;} |
Строка - html тег элемента управления. Используется когда
нет соответствующего значения из перечисления HtmlTextWriterTag. |
| protected virtual Style
CreateControlStyle(); |
Метод создания свойства стилевого оформления документа.
|
| public void
ApplyStyle(Style
s ); |
Применяет стиль, указанный в параметре, к элементу управления. При
этом из стиля s в стиль элемента управления переносятся все непустые
значения, переписывая соответствующие значения в стиле элемента
управления. |
| public void
MergeStyle(Style
s ); |
Применяет стиль, указанный в параметре, к элементу управления, но при
этом не переписывает существующие значения в стиле элемента
управления. |
| protected virtual void
AddAttributesToRender(HtmlTextWriter
writer ); |
Добавляет атрибуты и стили из элемента управления в
HtmlTextWriter. |
| public virtual void
RenderBeginTag(HtmlTextWriter
writer ); |
Выводит откывающий html тег для элемента управления |
| protected virtual void
RenderContents(HtmlTextWriter
writer ); |
Выводит содержимое элемента управления (без открывающего и
закрывающего html тегов) |
| public virtual void
RenderEndTag(HtmlTextWriter
writer ); |
Выводит закрывающий html
тег. |
Как же WebControl себя рисует с учетом данных
нововведений? А достаточно просто - взгляните на код:
protected virtual void Render(HtmlTextWriter writer) {
RenderBeginTag(writer);
RenderContents(writer);
RenderEndTag(writer);
}
public virtual void RenderBeginTag(HtmlTextWriter writer) {
AddAttributesToRender(writer);
if (TagKey != HtmlTextWriterTag.Unknown)
writer.RenderBeginTag(TagKey);
else
writer.RenderBeginTag(TagName);
}
protected virtual void RenderContents(HtmlTextWriter writer) {
base.Render(writer);
}
public virtual void RenderEndTag(HtmlTextWriter writer) {
writer.RenderEndTag();
}
Как видно из приведенного кода при создании элемента
управления из System.Web.UI.WebControls.WebControl для генерации html кода
необходимо в общем случае использовать метод RenderContents, а не метод Render.
Также при необходимости нужно пеоеопределить свойства TagKey или TagName (по
умолчанию WebControl использует тег <span>) или методы RenderBeginTag/RenderEndTag.
Попробуем теперь расширить функциональность отображения нашего элемента
управления на основе всего вышеизложенного. И внесем в него следующие
изменения:
- Добавим работу со стилями, специфичными для
отображения таблицы.
- Добавим стили для каждой из ячеек элемента
управления - ячейки информации, ячейки кнопок "вперед-назад" и ячейки
навигационных кнопок.
Итак начем со стилей элемента управления. Первым делом
необходимо переопределить метод CreateControlStyle:
protected override Style CreateControlStyle()
{
return new TableStyle();
}
Теперь необходимо добавить в элемент управления
свойства, специфичные для таблицы - CellSpacing, CellPadding и
HorizontalAlign. Обратите внимание на то, что эти свойства сохраняются в в свойстве ControlStyle, которое в создаваемом элементе управления имеет тип TableStyle.
[Bindable(true),
Category("Appearance"),
DefaultValue("-1")]
public
int CellSpacing {
get
{
if(ControlStyleCreated)
return ((TableStyle) ControlStyle).CellSpacing;
return -1;
}
set
{
((TableStyle) ControlStyle).CellSpacing = value;
}
}
[Bindable(true),
Category("Appearance"),
DefaultValue("-1")]
public int CellPadding {
get
{
if(ControlStyleCreated)
return ((TableStyle) ControlStyle).CellPadding;
return -1;
}
set
{
((TableStyle) ControlStyle).CellPadding = value;
}
}
[Bindable(true),
Category("Appearance"),
DefaultValue("NotSet")]
public HorizontalAlign HorizontalAlign {
get
{
if(ControlStyleCreated)
return ((TableStyle) ControlStyle).HorizontalAlign;
return HorizontalAlign.NotSet;
}
set
{
((TableStyle) ControlStyle).HorizontalAlign = value;
}
}
Самое интересное во всем этом коде то, что для его отрисовки ничего больше делать не нужно. При вызове метода WebControl.AddAttributesToRender в нем вызывается метод ControlStyle.AddAttributesToRender, который и выводит все введенные данные.
Теперь добавим свойства для стилей ячеек элемента управления. Эти свойства
будут иметь тип TableItemStyle:
private TableItemStyle infoCellStyle;
[Bindable(true),
Category("Style"),
DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
PersistenceMode(PersistenceMode.InnerProperty)]
public TableItemStyle InfoCellStyle
{
get
{
if (infoCellStyle == null)
infoCellStyle = new TableItemStyle();
return infoCellStyle;
}
}
private TableItemStyle prevNextCellStyle;
[Bindable(true),
Category("Style"),
DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
PersistenceMode(PersistenceMode.InnerProperty)]
public TableItemStyle PrevNextCellStyle
{
get
{
if (prevNextCellStyle == null)
prevNextCellStyle = new TableItemStyle();
return prevNextCellStyle;
}
}
private TableItemStyle navBtnsCellStyle;
[Bindable(true),
Category("Style"),
DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
PersistenceMode(PersistenceMode.InnerProperty)]
public TableItemStyle NavBtnsCellStyle
{
get
{
if (navBtnsCellStyle == null)
navBtnsCellStyle = new TableItemStyle();
return navBtnsCellStyle;
}
}
Обратите внимание на то, как правильно нужно объявлять сложные свойства - всегда делайте их "только для чтения". И еще раз напоминаю, что в данной статье мы не рассматриваем сохранение состояние между постбеками, поэтому все свойства сохраняются в приватных переменных класса.
Теперь осталось только сгенерировать html код. Для этого необходимо
переопределить свойство TagKey и метод RenderContents. Сделаем это:
protected override HtmlTextWriterTag TagKey
{
get { return HtmlTextWriterTag.Table; }
}
protected override void RenderContents(HtmlTextWriter output)
{
int pagesTotal = RowsTotal % PageSize == 0 ? RowsTotal / PageSize : RowsTotal / PageSize + 1;
output.RenderBeginTag(HtmlTextWriterTag.Tr);
if(infoCellStyle != null)
infoCellStyle.AddAttributesToRender(output);
output.RenderBeginTag(HtmlTextWriterTag.Td);
output.Write(currentPageFormat, CurrentPageIndex, pagesTotal);
output.RenderEndTag();
if(prevNextCellStyle != null)
prevNextCellStyle.AddAttributesToRender(output);
output.RenderBeginTag(HtmlTextWriterTag.Td);
if(CurrentPageIndex > 1)
{
output.AddAttribute(HtmlTextWriterAttribute.Href, "#");
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write("<< Previous");
output.RenderEndTag();
output.Write(" ");
}
if(CurrentPageIndex < pagesTotal)
{
output.AddAttribute(HtmlTextWriterAttribute.Href, "#");
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write("Next >>");
output.RenderEndTag();
}
output.RenderEndTag();
if(navBtnsCellStyle != null)
navBtnsCellStyle.AddAttributesToRender(output);
output.RenderBeginTag(HtmlTextWriterTag.Td);
int[,] pageRanges = new int[3, 2]
{
{1, 2},
{CurrentPageIndex - 2, CurrentPageIndex + 2},
{pagesTotal - 1, pagesTotal}
};
if (pageRanges[0, 1] + 1 >= pageRanges[1, 0])
{
if(pageRanges[0, 1] < pageRanges[1, 1])
pageRanges[0, 1] = pageRanges[1, 1];
pageRanges[1, 0] = -1000;
}
if((pageRanges[1, 0] != pageRanges[1, 1]) && (pageRanges[1,1] + 1 >= pageRanges[2,0]))
{
if (pageRanges[2, 0] > pageRanges[1, 0])
pageRanges[2, 0] = pageRanges[1, 0];
pageRanges[1, 0] = -1000;
}
if (pageRanges[0, 1] + 1 >= pageRanges[2, 0])
{
pageRanges[0, 1] = pageRanges[2, 1];
pageRanges[2, 0] = -1000;
}
output.RenderBeginTag(HtmlTextWriterTag.B);
output.Write ("Pages:");
output.RenderEndTag();
output.Write(" ");
for(int rangeIndex = 0; rangeIndex <= 2; rangeIndex++)
{
if(pageRanges[rangeIndex, 0] != -1000)
{
if(rangeIndex != 0)
output.Write (". . . ");
int pgIndex = pageRanges[rangeIndex, 0];
do
{
if(pgIndex == CurrentPageIndex)
{
output.RenderBeginTag(HtmlTextWriterTag.B);
output.Write(pgIndex.ToString());
output.RenderEndTag();
output.Write(" ");
}
else
{
output.AddAttribute(HtmlTextWriterAttribute.Href, "#");
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write(pgIndex.ToString());
output.RenderEndTag();
output.Write(" ");
}
pgIndex++;
}
while (pgIndex <= pageRanges[rangeIndex,1]);
}
}
output.RenderEndTag();
output.RenderEndTag();
}
Приведенный код не очень сильно отличается от кода метода Render, рассмотренного в предыдущем примере. Таких отличий всего 2:
- Не вызывается метод для генерации тега table (этот тег генерится в методах
RenderBeginTag и RenderEndTag на основании значения свойства TagKey.
- При генерации тегов td к ним добавляются соответствующие стили с помощью
вызовов их методов AddAttributesToRender.
Все. Последняя (и наиболее правильная) версия рисования серверного элемента
управления Pager готова. Конечно есть и еще один вариант создания этого метода -
с помощью заполнения коллекции Controls элемента управления, но это тема для
отдельной статьи.
В следующей статье будут рассмотрены вопросы сохранения состояния между
постбеками, обработка постбека и генерация событий элементом
управления.
|