| |
Back to list of Articles
Basics of creating server controls. Part I. Rendering.
Author: Dmytry Rudenko aspnetman@aspnetmania.com
Server controls concept Microsoft offered in ASP.NET, raises web programmers possibilities to a magnificent level.
Now you can forget about diligent creating of separate parts of combined html/asp code in order to resolve typical tasks
(e.g. displaying data in a grid with sorting and pagination functionality)
and coping these code snippets to an appropriate spots on the page. It's enough to create only once
server control class implementing this functionality and you can use control you've got in any of
your projects (and even distribute it, for other programmers to enjoy its functionality).
What is a server control from the point of view of ASP.NET programmer? It is a class that derives
(directly or indirectly) from the System.Web.UI.Control (in fact this definition is not full, but quite enough for a web programmer).
This class provides basic functionality for control - for example, placing in a web form controls collections and rendering. It also
provides functionality for adding itself to the controls toolbar and running in design mode.
For UI server controls creation System.Web.UI.WebControls class is used basically. This class derives from System.Web.Ui.Control class.
A great number of features for server control visual representation were added to this class (e.g. Font, CssClass, etc) as well as rendering.
This article, first in series of articles I am planning to write about server controls creating, speaks about writing a code for rendering
server control. I will try to explain how to write a code generating html for server control and what methods to use.
I would like to note also, that control described in this article will be quite useful for every web programmer. It's a control that helps
to paginate. You can see it on the picture below:
This control is a table that has 3 cells to display current page info, previous/next buttons, and buttons to go to a specific page. It has following properties needed for rendering:
| Property |
Description |
| PageSize |
Number of rows on the page |
| RowsTotal |
Total number of rows in dataset |
| CurrentPageIndex |
Current page number |
| CurrentPageFormat |
Format for displaying current page info (left cell) |
Let's start creating control class with these properties declaration:
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;
}
}
Since I will not discuss saving data between postbacks and using ViewState, all properties are stored in private class properties.
Let's start with a control inherited from System.Web.UI.Control.
System.Web.UI.Control class uses 3 methods and one property to display a control. Property used - ClientID - client-side control identifier, unique in the page namespace. Methods are the following:
| Method |
Description |
| public void RenderControl(HtmlTextWriter writer) |
Outputs server control content to a provided HtmlTextWriter object.
If a server control's Visible property is set to true, it renders the server control content to the page. |
| protected virtual void Render(HtmlTextWriter writer) |
Calls RenderChildren method to render embedded controls. |
| protected virtual void RenderChildren(HtmlTextWriter writer) |
Calls Render Control method for all elements of Controls collection. |
In the table above functionality description is given, provided for these methods in System.Web.UI.Control class. While creating controls derived directly from System.Web.UI.Control,
it is necessary to re-define Render method for the purposes of rendering. Let's do it.
Html code for this server control is generated by calling HtmlTextWriter class methods.
First example of Render method implementation can be called "sweet&simple solution", in this case we will draw our table
almost in such a way as if we are using Response.Write method for this purpose. Never write html control code in such a way -
this code is given only as an example!
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>");
}
I think, there is no point in describing logic of this method implementation - everything is quite clear.
But there is point to say how is it correct to use HtmlTextWriter class.
HtmlTextWriter class provides a dozen of methods for proper html code generation.
The most important methods for server controls developers are given in the table below:
| Method |
Description |
| public virtual void RenderBeginTag(HtmlTextWriterTag); |
Renders opening tag of html element.
Tag type is set in HtmlTextWriterTag enumeration.
Also there is an option to set html element as a string.
|
| public virtual void AddAttribute(...); |
Adds attribute to stack.
All attributes added to the stack are assigned to the first html element rendered by calling RenderBeginTag method. |
| public virtual void
AddStyleAttribute(...);
|
Adds style attribute to stack.
All attributes added to the stack are assigned to the first html element rendered by calling RenderBeginTag method. |
| public override void Write(...); |
Writes the value of parameter given |
Now let's try to re-write our Render method using the table above. We should get approximately the following:
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();
}
Code became longer, but user advantages of this html code generation method outweigh yet.
First, as a result of this code execution we will get valid and "nice" html code. Second, while generating controls html code ASP.NET
is able to substitute automatically HtmlTextWriter class with its heirs depending on settings in section of machine.config file.
For example, if web form is requested by an old browser which is not HTML 4.0 compliant, HtmlTextWriter class will be substituted with
HtmlTextWriter32 class and HTML will be generated in full compliance with HTML 3.2 specification (e.g. many style attributes will be
substituted with html elements with similar functionality and instead of div element table element will be used).
So, try to write html generation code in a proper way and don't strive for seeming quickness.
Well, we have created the control, it can render itself, everything is fine, we are happy. But there is one big "but" -
there is no possibility to change styles of rendered control - for example, to change font or background color.
The reason of this is the following: System.Web.UI.Control class is designed for creating non-visual controls (e. g. title or xml controls ).
And for creating visualized controls System.Web.UI.WebControl class exists, which has basic set of properties for control style customization.
It also has extended set of methods for rendering. Let's review these methods and properties:
| Method/Property |
Description |
| public Style ControlStyle {get;} |
The ControlStyle property encapsulates all properties of the WebControl class that specify the appearance of the control. |
| public bool ControlStyleCreated {get;} |
Property indicating that control style has been created. |
| protected virtual HtmlTextWriterTag TagKey {get;} |
Html tag of the control |
| protected virtual string TagName {get;} |
String - html tag of the control. Is used when there is no appropriate value from HtmlTextWriterTag enumeration. |
| protected virtual Style CreateControlStyle(); |
The CreateControlStyle method is used to create the style object that is used internally to implement all style related properties. |
| public void ApplyStyle(Style s ); |
Applies style set in parameter to the control. All nonblank elements of style s are copied to the control, overwriting style elements of the control. |
| public void MergeStyle(Style s ); |
Applies style set in parameter to the control, but does not overwrite existing style elements of the control. |
| protected virtual void AddAttributesToRender(HtmlTextWriter writer ); |
Adds attributes and styles of the control to HtmlTextWriter. |
| public virtual void RenderBeginTag(HtmlTextWriter writer ); |
Renders opening html tag of the control. |
| protected virtual void RenderContents(HtmlTextWriter writer ); |
Renders control content (without opening and closing tags) |
| public virtual void RenderEndTag(HtmlTextWriter writer ); |
Renders closing html tag. |
How does WebControl renders itself using all these innovations? Quite easy - look at the code:
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();
}
As we can see from the code above, while creating a control using System.Web.UI.WebControls.WebControl, for html
code generation in the general case it is necessary to implement RenderContents method, not Render method.
If necessary, we need to override TagKey or TagName properties (WebControl uses tag by default) or RenderBeginTag/RenderEndTag methods.
Now, let's try to extend visual functionality of our control basing on all mentioned above. And let's make the following changes:
1. Add support of styles, specific for grid displaying.
2. Add styles for every cell of our control - info cell, previous/next buttons cell, and navigation buttons cell.
Let's start with control styles. First of all we need to override CreateControlStyle method:
protected override Style CreateControlStyle()
{
return new TableStyle();
}
Now we need to add table-specific properties into our control - CellSpacing, CellPadding, and HorizontalAlign.
Note, that these properties are stored in ControlStyle property, which is of TableStyle type for the control we are creating.
[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;
}
}
The most interesting part in this code is that we don't need to do anything else for its rendering.
WebControl.AddAttributesToRender method calls ControlStyle.AddAttributesToRender method which renders everything.
Now, let's add properties for control cells styles. These properties are of TableItemStyle type:
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;
}
}
Note, how to declare complex properties properly - always make them "read only". I would like to remind you once again,
that in this article we do not discuss saving state between postbacks, that is why all properties are stored in class private properties.
Now we need only to generate html code. To do it, we need to override TagKeyproperty and RenderContents method. Let's do it:
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();
}
The code above does not varies greatly from Render method code we reviewed in the previous example. There are only 2 differences:
Method generating table tag is not called (this tag is generated in RenderBeginTag and RenderEndTag methods using value of TagKey property).
While generating td tags appropriate styles are added to them using their AddAttributesToRender method calls.
That is it. The last one and the most accurate version of Pager server control rendering is ready.
In my next article problems of saving state between postbacks, postback handling and event generation in the control will be discussed.
You can download source code here.
Back to top
|