Концепция
серверных элементов управления, предложенная 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 элемента
управления, но это тема для отдельной статьи.
В следующей статье будут рассмотрены вопросы сохранения состояния
между постбеками, обработка постбека и генерация событий элементом
управления. |