Control Designers...
Designers control the UI and functionality of an ASP.NET control in the design-time environment.� Each ASP.NET
server control is linked to a control designer through the DesignerAttribute class, and can render completely
different HTML in the design-time environment versus the runtime environment.
By: Brian Mains
Date: January 3, 2005
Download the code.
When ASP.NET first came out, it meant a drastic impact on the way developers created web applications. A whole
new architecture and major changes to the language made ASP much better than its predecessors (depending on who
you talk to). Though not only did it mean changes to software and hardware, it also meant changes to the
developer tools, such as Visual Studio environment. Visual Studio .NET is more functional and customizable
than it you may think; developers can create custom editors, add-ins, wizards, and implement separate
functionality and UI at design-time than at runtime. In my opinion, Visual Studio .NET is a very powerful
product, and shouldn't be taken lightly when choosing development tools.
When an ASP.NET control is dragged and dropped from the toolbox onto the screen, the HTML rendered for the
control in Visual Studio .NET is specified by a designer class. ASP.NET server controls actually implement a
separate control designer class through the Designer attribute designation. For most controls, a direct
rendering of the control is all that is needed; however, other controls need more design-time functionality.
A designer has many functions, such as modifying the syntax rendered in the HTML, modifying the design of the
control in the Visual Studio .NET designer, notifying the user of an error in the designer, persisting inner
content, etc.
A good example of custom designer functionality is the DataGrid control. This control renders as a table with
many rows and cells, all generated at runtime. However, a specific designer for the control affects how the
control interacts at design time. For example, the DataGrid renders five rows of data. By adjusting the paging
settings and specifying a number of rows per page, the DataGrid automatically redraws itself to the appropriate
number of rows needed. In addition, the columns are automatically generated by default (Column0, Column1, and
Column2 headings appear in the grid initially); however, by turning off the auto-generation feature and adding
a series of columns, the DataGrid redraws itself with the correct set of columns in the designer. Without a
designer, the DataGrid wouldn't be as easy to implement.
The DataGrid example is a more complicated example. For example, a DataGrid control supports templates and can
accept dragging and dropping of controls within that template. In addition, template-editing is easily
achievable by selecting the template to edit. Also, DataGrid supports databinding, which is another advanced
topic in itself, especially in the designer. These are more advanced topics that will be discussed later.
Again, each ASP.NET server control is linked to a control designer through the DesignerAttribute class. The
designer attribute looks like:
VB.NET:
<Designer(GetType(System.Web.UI.Design.ControlDesigner))> _
Public Class MyControl
...
End Class
C#:
[Designer(typeof(System.Web.UI.Design.ControlDesigner))]
public class MyControl {
...
}
If you inherit from an existing control, there is a designer associated with that control even if you don't
assign one. The base designer for server controls is System.Web.UI.Design.ControlDesigner, although more
detailed designers are available in the framework. The designer defines a base set of methods that can be
overridden to implement your custom functionality in the designer. The following is a summary of the more
common methods:
GetDesignTimeHtml - This method returns an HTML string representing the control in the design-time environment. If certain functionality is not desired in the Visual Studio .NET IDE (such as not having validation controls appear), you can leave out those controls within the designer but still have that core functionality in the control. The design-time HTML does not affect the runtime environment.
GetErrorDesignTimeHtml - This method returns a string containing information about the error occurring in the designer. Use the CreatePlaceHolderDesignTimeHtml method to create a standard gray box around the error information.
GetEmptyDesignTimeHtml - This method returns a string that contains HTML for an empty control (if nothing has been returned from the Render method).
GetPersistInnerHtml � This method controls the inner HTML rendered for the control. An example would be a drop-down list; this control renders many <asp:ListItem> elements within it, representing the items in the list. The content within it is completely customizable and must be parsed later at runtime to retrieve its values.
CreatePlaceHolderDesignTimeHtml - This method creates a gray placeholder box around the control, similar to the error messages that appear when an error occurs in the control.
When rendering the content of the control through GetDesignTimeHtml, the base class will invoke the Render
method of the control. However, the output could be something completely different than the control itself.
For example, a control that renders a textbox could actually have a designer that renders a hyperlink instead.
In Visual Studio .NET, a hyperlink would appear, but when the page is compiled and ran, the textbox would
appear in the browser. It's up to the developer to ensure something meaningful in the designer.
Let's look at an example of designers for controls. The following is a declaration of the comparison control.
This control is a WebControl that defines a ListItemCollection type property named Items. This control also
overrides the RenderContents method to render the items to the browser in the form of an unordered list. Note
the Designer attribute that defines the designer class type ComparisonControlDesigner.
A ControlBuilder attribute is declared in a similar manner as the Designer attribute. A control builder
controls the parsing logic used when parsing the control. The control builder in this instance only allows
ListItem's as child elements of the control. Control builders are another bigger topic and will be discussed
more thoroughly in a follow-up article; for now, all you need to understand is that a control builder affects
the parsing logic and can determine what is or isn't allowable for a control�s content. The AddParsedSubObject
object may be new, but I'll discuss that method later in the article.
[
ControlBuilder(typeof(CCBuilder)),
Designer(typeof(ComparisonControlDesigner)),
ParseChildren(false)
]
public class ComparisonControl : WebControl
{
private ListItemCollection _items = new ListItemCollection();
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
PersistenceMode(PersistenceMode.InnerDefaultProperty)
]
public ListItemCollection Items {
get {
return _items;
}
set {
_items = value;
}
}
protected override void AddParsedSubObject(object obj) {
if (obj is ListItem) {
ListItem item = obj as ListItem;
_items.Add(item);
}
else
base.AddParsedSubObject(obj);
}
protected override void RenderContents(HtmlTextWriter writer) {
//Don�t render anything if no items exist
if (this.Items.Count == 0)
return;
writer.RenderBeginTag(HtmlTextWriterTag.Ul);
foreach (ListItem item in _items) {
writer.RenderBeginTag(HtmlTextWriterTag.Li);
writer.Write(item.Text);
writer.RenderEndTag();
}
writer.RenderEndTag();
}
}
|
For the first example of the designer, this class will override the GetDesignTimeHtml method to render the list
control; however, a title will appear at the top, all within a DIV element. This DIV element will define some
styles to "jazz up" the control. Notice the call to base.GetDesignTimeHtml(), which retrieves the HTML
generated from the control�s Render method.
The GetEmptyDesignTimeHtml method is called whenever the control's Render method returns no HTML content. In
the RenderContents method, if there were no items in the collection, a null string was returned. This in effect
will trigger the GetEmptyDesignTimeHtml method. Oftentimes too logic will appear in the GetDesignTimeHtml
method to return an empty string if the control reference is null or if a property value hasn�t been provided
(like items in the collection). When nothing is rendered, the GetEmptyDesignTimeHtml method will generate
"Add content" in the designer.
GetErrorDesignTimeHtml returns the full error message to the designer. You may have seen the default error for
controls stating �Error Creating Control,� because of some issue with the control code; this overridden method
implementation states a more descriptive error text. The CreatePlaceHolderDesignTimeHtml method receives a
string parameter for the text to render a placeholder for. The placeholder is a gray box that text appears in,
exactly as the standard error message.
public class ComparisonControlDesigner : System.Web.UI.Design.ControlDesigner {
public override string GetDesignTimeHtml() {
try {
//Render the contents of the control, but add a header
return @"<div style='background-color:goldenrod;color:navy;font-weight:bold;'>
This is my custom designer<br><br>" + base.GetDesignTimeHtml() + "</div>";
}
catch (Exception e) {
return this.GetErrorDesignTimeHtml(e);
}
}
protected override string GetEmptyDesignTimeHtml() {
//Return empty text in a placeholder
return this.CreatePlaceHolderDesignTimeHtml("Add items to the list.");
}
protected override string GetErrorDesignTimeHtml(Exception e) {
//Return error text in a placeholder
return this.CreatePlaceHolderDesignTimeHtml("The control could not be loaded:<br>" + e.ToString());
}
public override string GetPersistInnerHtml() {
StringWriter writer = new StringWriter();
HtmlTextWriter html = new HtmlTextWriter(writer);
ComparisonControl control = this.Component as ComparisonControl;
if (control != null) {
foreach (ListItem item in control.Items) {
html.WriteBeginTag("item");
html.WriteAttribute("value", item.Value);
html.WriteAttribute("selected", item.Selected.ToString());
html.Write(HtmlTextWriter.TagRightChar);
html.Write(item.Text);
html.WriteEndTag("item");
}
}
return writer.ToString();
}
}
|
GetPersistInnerHtml has a specific function for this control. The control will emit the following control
syntax:
<cc1:ComparisonControl id="ComparisonControl5" runat="server">
<item value="1" selected="False">1</item>
<item value="2" selected="False">2</item>
<item value="3" selected="False">3</item>
<item value="4" selected="False">4</item>
</cc1:ComparisonControl>
|
GetPersistInnerHtml renders the <item> inner elements. The designer class has a Component property that
contains the reference to the control that the designer is for, which is used to retrieve the Items collection.
GetPersistInnerHtml loops through each ListItem in the collection, and renders an <item> tag with a value
and selected attribute, as well as the text in the element base. If the class wasn't a standard class in the
framework, each HTML attribute would map to a specific property, based on the persistence mode. Because the
ListItem defines the text property to be the inner property, the text value is retrieved from inside the
element. This is established through the PersistenceMode attribute.
This is where the AddParsedSubObject comes into play. The GetPersistInnerHtml "gives" the items to the control
(in the HTML definition), whereas the AddParsedSubObject �receives� the objects from the control (recompiles the
items back into the collection at runtime). AddParsedSubObject checks the type of the object passed into it.
If the type is of ListItem, then the object is added to the list. What makes that determination is the control
builder. The control builder has a GetChildControlType which parses the child elements. When it is parsed, the
control builder verifies that the children are <item> tags and returns the ListItem type. Otherwise, the
base class makes the determination. The designer controls how the content is generated within the control's
tags, the control builder checks the type at compile time, and the AddParsedSubObject method regenerates the
object at runtime.
public class ComparisonControl : WebControl
{
..
protected override void AddParsedSubObject(object obj) {
if (obj is ListItem) {
ListItem item = obj as ListItem;
_items.Add(item);
}
else
base.AddParsedSubObject(obj);
}
..
}
public class CCBuilder : ControlBuilder
{
public override Type GetChildControlType(string tagName, System.Collections.IDictionary attribs) {
if (string.Compare(tagName, "item", true) == 0)
return typeof(ListItem);
return base.GetChildControlType(tagName, attribs);
}
}
|
The next designer example renders an HTML table showing the Text and Value properties in a tabular format.
This is implemented in the GetDesignTimeHtml method. This method uses StringWriter and HtmlTextWriter objects
to return the HTML for a server control. The key is the RenderControl method. This method renders the server
control's HTML as a string to an HtmlTextWriter. If you notice at the beginning of the code, the StringWriter
is passed to the HtmlTextWriter in the constructor; the StringWriter returns the final control HTML string
using its ToString method.
This class defines the same implementation of the GetEmptyDesignTimeHtml, GetErrorDesignTimeHtml, and
GetPersistInnerHtml methods. This example illustrates that the design-time representation doesn't have to be
even close to the runtime representation.
public class ComparisonControlDesigner2 : System.Web.UI.Design.ControlDesigner {
public override void Initialize(IComponent component) {
if (!(component is ComparisonControl)) {
throw new ArgumentException("Component must be a Comparison Control, type 2", "Component");
}
base.Initialize(component);
}
public override string GetDesignTimeHtml() {
try {
//Component stores reference to the control
ComparisonControl control = this.Component as ComparisonControl;
//If control isn't null
if (control != null && control.Items.Count > 0) {
StringWriter writer = new StringWriter();
HtmlTextWriter html = new HtmlTextWriter(writer);
Table table = new Table();
table.BorderWidth = new Unit("1px");
table.BorderStyle = BorderStyle.Solid;
foreach (ListItem item in control.Items) {
TableRow row = new TableRow();
row.Cells.Add(new TableCell());
row.Cells[0].Text = item.Text;
row.Cells.Add(new TableCell());
row.Cells[1].Text = item.Value;
table.Rows.Add(row);
}
table.RenderControl(html);
return writer.ToString();
}
//Control reference empty; return empty text
return this.GetEmptyDesignTimeHtml();
}
catch (Exception e) {
return this.GetErrorDesignTimeHtml(e);
}
}
protected override string GetEmptyDesignTimeHtml() {
return this.CreatePlaceHolderDesignTimeHtml("Add items to the list.");
}
protected override string GetErrorDesignTimeHtml(Exception e) {
return this.CreatePlaceHolderDesignTimeHtml("The control could not be loaded:<br>" + e.ToString());
}
public override string GetPersistInnerHtml() {
StringWriter writer = new StringWriter();
HtmlTextWriter html = new HtmlTextWriter(writer);
ComparisonControl control = this.Component as ComparisonControl;
if (control != null) {
foreach (ListItem item in control.Items) {
html.WriteBeginTag("item");
html.WriteAttribute("value", item.Value);
html.WriteAttribute("selected", item.Selected.ToString());
html.Write(HtmlTextWriter.TagRightChar);
html.Write(item.Text);
html.WriteEndTag("item");
}
}
return writer.ToString();
}
}
|
You may notice the addition of the Initialize method. This method checks the type of the Component parameter,
which should be of type ComparisonControl. If not the proper type, an ArgumentException is thrown stating that
the type is wrong.
About the Code
The test page to view designer functionality is the Ex01.aspx page in the web site attached. The code for the
controls and designers are stored in the DesignerExampleLibrary project, in the Ex01 folder. To test each
designer with the ComparisonControl, switch the designer attribute statements between these two:
Designer(typeof(ComparisonControlDesigner))
Designer(typeof(ComparisonControlDesigner2))
Recompile the project. The changes should take effect immediately in the design-time environment.
You may download the code here.