Introduction
Each ASP.NET developer has
need, at least once, during developing of one or more applications, to create
custom functionalities and or design features, related to application purpose.
Indeed ASP.NET provides a very wide category of these controls, covering most
of functionalities and developers tastes. But what if a developer needs an
extra functionality, which ASP.NET controls cannot offer it? Or maybe somebody
else wants to be original and create own controls, to reuse them each time
need?
An answer could be: create
a custom control. ASP.NET provides a mechanism which allows creating custom
controls, as the developer defines it. In this way, developer can define
control structure, properties, its representation, giving a shape for an object
that control represents.
Custom controls, as a
server control, can have 2 representations: one, in aspx (or ascx) file from
application, which describes the structure of the control, and other in code
file (C#, VB etc.) which describes the functionality of the control. For
example, a classic ASP.NET server control, Label, has associated a class in
ASP.NET libraries, which defines the functionality of the control (members,
properties, behavior such as rendering mode etc.) and a representation in aspx
files from application, where the class is instantiated in object, as they are
defined in that page:
<asp:label ID="lblTest"
Runat="server" Text="This is a test label"></asp:label>
This control is
instantiated at runtime in an object of type Label and will generate a HTML
control:
<span id="lblTest">This
is a test label</span>,
and sent to the web browser, which
will display.
The question is: how is
generated a Label server control? Or, generally, how is generated every asp.net
control in one or more Html controls, to be sent to the web browser which made
the request. Let’s consider, for example a DataGrid control: a complex ASP.NET
control, with numerous modalities for defining data displaying, which will be
rendered as a HTML table control, containing a lot of simple HTML controls,
such as buttons, textboxes, images etc, calls of javascript code etc.,
depending of its defining structure from aspx files. How functions this
rendering mechanism, from a control to another?
The aspx definition of an
ASP.NET control is made in a markup language, having an xml structure. Thus,
the control definition is a collection of tags. The root tag is the definition
of the control itself, while the children tags will provide information about
control properties, children controls and so on (control metadata).
Each ASP.NET control has
associated a control builder class. This class describes the way the control
will be rendered. When the control starts to be parsed, the control builder
class provides the necessary information about how will be interpreted the
content of the control. Each tag will be interpreted in a form or another,
depending of tag type. For example, <asp:ListItem> tag of a
<asp:DropDownList> control will generate a option for
DropDownListControl:
If
(Tag = “ListItem”) Then
AddOptionToControl
Example
To take a closer look to
this mechanism, let’ create a custom control, a very simple tree. Our control
can be defined as a tree structure, and will print some text on the screen.
(Obviously, our tree is almost useless in real applications; it will be defined
here just to demonstrate how custom controls can be implemented).
Here is the markup language
definition of the SampleTree control
<st:SampleTree ID="stWorld" runat="server" Text="Earth">
<Level1 Name="Europe">
<Level2 text="France"></Level2>
<Level2 text="Great Britain"></Level2>
<Level2 text="Romania"></Level2>
</Level1>
<Level1 Name="Africa">
<Level2 text="Senegal"></Level2>
<Level2 text="Maroc"></Level2>
<Level2 text="Somalia"></Level2>
</Level1>
<Level1 Name="Asia">
<Level2 text="Japan"></Level2>
<Level2 text="China"></Level2>
</Level1>
</st:SampleTree>
As can be seen the tree has two levels, and has a property,
Name (for the first level) or Text (for second level). When will be rendered,
tree will generate the output
Let see the control
definition:
using System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Web.UI;
using
System.Web.UI.HtmlControls;
using
System.Web.UI.WebControls;
using
System.ComponentModel;
using
System.Collections;
namespace STControl
{
public class SampleTreeControlBuilder
: ControlBuilder
{
public
override Type
GetChildControlType(string tagName, IDictionary attributes)
{
if
(tagName.ToLower() == "level1")
return typeof(TreeLevel1);
return
null;
}
}
[ToolboxData("<{0}:SampleTree runat=server></{0}:SampleTree>")]
[ControlBuilder(typeof(SampleTreeControlBuilder))]
[ParseChildren(false)]
public class SampleTree :
WebControl
{
private
List<TreeLevel1>
level1Collection = new List<TreeLevel1>();
public
List<TreeLevel1>
Level1Collection
{
get
{
return level1Collection;
}
set
{
level1Collection = value;
}
}
private
string text;
public
string Text
{
get
{
return text;
}
set
{
text = value;
}
}
protected
override void
AddParsedSubObject(object obj)
{
if
(obj is TreeLevel1)
this.level1Collection.Add((TreeLevel1)obj);
}
protected
override void
CreateChildControls()
{
this.Controls.Clear();
HtmlGenericControl
divTree = new HtmlGenericControl("div");
Label
lblTreeText = new Label();
lblTreeText.Text = this.Text;
divTree.Controls.Add(lblTreeText);
foreach
(TreeLevel1 level1 in
this.level1Collection)
{
HtmlGenericControl divGroup = new HtmlGenericControl("div");
Label lblGroup = new
Label();
lblGroup.Text = "___" + level1.Name;
divGroup.Controls.Add(lblGroup);
foreach (TreeLevel2
level2 in level1.Level2Collection)
{
HtmlGenericControl divProp = new HtmlGenericControl("div");
Label lblProp = new
Label();
lblProp.Text =
"______" + level2.Text.ToString();
divProp.Controls.Add(lblProp);
divGroup.Controls.Add(divProp);
}
divTree.Controls.Add(divGroup);
}
this.Controls.Add(divTree);
}
}
public class TreeLevel1ControlBuilder
: ControlBuilder
{
public
override Type
GetChildControlType(string tagName, IDictionary attributes)
{
if
(tagName.ToLower() == "level2")
return typeof(TreeLevel2);
return
null;
}
}
[ControlBuilder(typeof(TreeLevel1ControlBuilder))]
[ParseChildren(false)]
public class TreeLevel1 :
WebControl
{
private
List<TreeLevel2>
level2Collection = new List<TreeLevel2>();
public
List<TreeLevel2>
Level2Collection
{
get
{
return level2Collection;
}
set
{
level2Collection = value;
}
}
private
string name;
public
string Name
{
get
{
return this.name;
}
set
{
this.name = value;
}
}
protected
override void
AddParsedSubObject(object obj)
{
if
(obj is TreeLevel2)
this.level2Collection.Add((TreeLevel2)obj);
}
}
public enum LevelType
{
Text = 1,
Collection = 2
}
public class TreeLevel2 :
WebControl
{
private
LevelType type;
public
LevelType Type
{
get
{
return this.type;
}
set
{
this.type = value;
}
}
private
string text;
public
string Text
{
get
{
return this.text;
}
set
{
this.text = value;
}
}
}
}
The
main class is SampleTree, which is derived by WebControl. Of course, our
functionality is based on WebControl functionality; we just add or modify some
things in some things in behavior of control generation. This class has
following attributes:
[ToolboxData("<{0}:SampleTree runat=server></{0}:SampleTree>")]
[ControlBuilder(typeof(SampleTreeControlBuilder))]
[ParseChildren(false)]
ToolboxData specifies which text will be added in aspx files when
control will be added by drag and drop from toolbar. This attribute has no role
in control generation.
ParseChildren attribute, when true, specifies that child tags must be map
with class properties. When false (as our case), this map will not be made;
thus, a custom mapping occurs, provided by associated ControlBuilder class.
ControlBuilder
is a very important attribute for customization of rendering control. It
specifies name of the class which will be used to render the control structure.
In this way we will override the default control builder class associated with
WebControl class; this thing allows us to provide our tags and associate them
with specific classes
In
SampleTreeControlBuilder, delegated to define parsing rules, we have overridden
method GetChildControlType. This method is automatically called for every child
tag encountered in control definition and gets as parameter the name of the
tag:
public override Type
GetChildControlType(string tagName, IDictionary attributes)
{
if
(tagName.ToLower() == "level1")
return typeof(TreeLevel1);
return null;
}
In this function,
we specified that, if name of the tag is “Level1”, return TreeLevel1 type,
which is a class defined below in the code. In this way, we defined the rule
which sais that
child tag >Level1< will be associated with an object of type TreeLevel1.
If any other child tag occurs in control definition, it will not be recognized
and will be ignored.
What
happened next? If the tag is a “known” one, will be added to a list in the
class:
protected override void
AddParsedSubObject(object obj)
{
if (obj is TreeLevel1)
this.level1Collection.Add((TreeLevel1)obj);
}
In
this way we will parse all the tags and add them as children objects in parent
control. This mechanism can be applied recursively, allows us to define as many
levels as we want (in our control, Level2 is processed in a similar way).
In the end, all we have to
do is to transform children objects in controls, for any level from control
definition. We will do that overriding function CreateChildControls(),
generating the control with expected content and purpose.
CustomTree.rar (23.74 kb)