Introduction
This article demonstrates an approach for developing a library of server controls, called "action tags" here, separating the functionality of an action from its user interface. The tags themselves are objects inheriting from the System.Web.UI.Control
class, with an AttachTo
attribute that indicates the associated user interface control. For example, code to execute a query can be encapsulated as a <Query>
action tag, then attached to a DataGrid
or DropDownList
control. Or code that would be triggered from a Click
event could be encapsulated in a tag, then attached to a Button
, LinkButton
, or ImageButton
. This is a different approach than sub-classing a user interface control to add functionality, and can provide a consistent means of linking encapsulated code to a triggering event.
A base class ActionTagControl
will be described, defining how these action tags will function. Objects in the System.Reflection
namespace are used to inspect user interface controls for event and property information. The article will also show three working examples of inheriting classes, to create a <ShowAndHideAction>
tag, <Selector>
tag, and <Query>
tag.
Background
To some degree, this approach has been conceived with HTML scripters in mind. A couple of years ago I had the experience of migrating the department I had just joined, from ColdFusion to ASP, then to ASP.NET for web applications. In my brief time working with ColdFusion, I came to appreciate its appeal for scripters, using a tag approach consistent with HTML to provide functionality. There is a lot that a non-programmer can accomplish with that platform. Being a programmer, I prefer the power and flexibility provided with ASP.NET, but I ask the question: can ASP.NET be functional for non-programmers too, in the way ColdFusion is?
Take for example, the action of binding the results of a query to a DataGrid
or DropDownList
control. Although this is a simple operation without much code necessary, there is still some programming code required. Beyond the few lines of code, dealing with connection and dataset classes and page events still requires conceptual understanding better suited for programmers than HTML scripters. I can see the attraction of a ColdFusion platform, which provides a <Query>
tag to take care of these details for the scripter. ASP.NET should be a platform well-suited for a non-programmer if programmers develop similar tag libraries. This article offers one such approach.
The base class - ActionTagControl
All action tags in this library will be based on the abstract class ActionTagControl
. The base class provides a consistent means for assigning an action to a control. As the action tags will not provide a user interface of their own, the base class inherits from System.Web.UI.Control
rather than WebControl
. The ID of the triggering control - that is to say the control that triggers the action represented by the tag - is implemented as the string property AttachTo
:
protected string _attachTo = "";
public string AttachTo {
get {return _attachTo;}
set {_attachTo = value;}
}
The constructor for an ActionTagControl
inheriting class will typically set the triggering event; if none is specified, the base class establishes the Click
event as a default in its own constructor:
public ActionTagControl() : base() {
_triggerEvent = "Click";
}
The base class also defines a TriggerEvent
public property so a tag user may override the event if appropriate:
protected string _triggerEvent;
public string TriggerEvent {
get {return _triggerEvent;}
set {_triggerEvent = value;}
}
One abstract method is defined in the base class: EventAction
. The EventAction
method is the code that will be executed when the event is triggered on the attached control. This method shares the same signature as a System.EventHandler
delegate:
protected abstract void EventAction(Object o, EventArgs e) ;
The real work performed in the base class, and its usefulness, comes in its overriding of the Control.OnInit
event. It is here that the AttachTo
control ID is inspected and located. If no such control is found, an exception is thrown. An exception is also thrown if the control is found but lacks the appropriate triggering event. The first part of the OnInit
function handles these cases:
protected override void OnInit(EventArgs e) {
if (_attachTo.Length == 0) {
string msg = "The 'AttachTo' attribute "
"is required. Specify a control"
" in the AttachTo attribute.";
throw new Exception(msg);
}
Control c = LocateControl(_attachTo);
if (c == null) {
string msg = string.Format("A control with" +
" id='{0}' could not be found. Specify " +
"a control in the AttachTo attribute.", _attachTo);
throw new Exception(msg);
}
The LocateControl
method used in the OnInit
event is as follows:
protected Control LocateControl(String sID) {
Control c;
c = this.FindControl(sID);
if (c == null && this.Parent != null) {
c = this.Parent.FindControl(sID);
}
if (c == null && this.Page != null) {
c = this.Page.FindControl(sID);
}
return c;
}
I decided to write this LocateControl
function rather than just use the FindControl
method of the Control
class, to allow for more flexibility in the placement of the action tag on a web page. FindControl
will locate a control by ID in the same naming container as the control calling it. If the desired control exists on the page but outside the action tag's naming container, FindControl
will not identify it. LocateControl
is also defined as protected
rather than private
, to allow for its use by inheriting classes.
Returning to the OnInit
method - its next three lines demonstrate the use of System.Reflection
classes to get an object representing the triggering event itself.
System.Type t = c.GetType();
BindingFlags bf = BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic;
EventInfo ei = t.GetEvent(_triggerEvent, bf);
These three lines deserve further explanation. The BindingFlags
object allows options to be set which affect how members are searched through reflection. Once the control's type is obtained as a System.Type
object, its GetEvent
method is used to obtain a System.Reflection.EventInfo
object. The EventInfo
object provides access to event metadata and exposes the AddEventHandler
method. It is through the use of AddEventHandler
that the ActionTagControl
's EventAction
code becomes associated with the triggering event:
if(ei != null) {
ei.AddEventHandler(c, new System.EventHandler(this.EventAction));
}
else {
string msg = string.Format("The control '{0}' " +
"does not have the {1} event. " +
"Either specifiy a control with the " +
"{1} event in the 'AttachTo' attribute, " +
"or specify a different event in " +
"the 'TriggerEvent' attribute.",
_attachTo, _triggerEvent);
throw new Exception(msg);
}
At this point we have a base class that implements the association of an action tag with a triggering event of a user interface control. Inheriting classes need only override the EventAction
method now, to take advantage of this association.
Here is a simple but complete example of an action tag inheriting from ActionTagControl
, designed to be triggered through a Click
event. Because Click
is already established as the default triggering event in the base class, the subclass does not need to override that in its own constructor. All that is necessary is to implement the EventAction
method.
public class SimpleAction : ActionTagControl {
static int i;
protected override void EventAction(Object o, EventArgs e) {
i++;
this.Page.Response.Write(i);
}
}
As with any custom server control, the tag becomes useable on a web page through the <%@ Register %>
directive. The AttachTo
attribute is then assigned the ID of a Button
, LinkButton
, ImageButton
, or any other user interface control that supports the Click
event.
<%@ Register TagPrefix="actions"
Namespace="ActionTags" Assembly="ActionTags" %>
<html>
<head><title>SimpleAction</title></head>
<body>
<form runat="server">
<asp:Button id="btnTest" runat="server"
text="Click Me!" />
<actions:SimpleAction id="click1"
runat="server" AttachTo="btnTest" />
</form>
</body>
</html>
Inheriting class Example 1: ShowAndHideAction
A more practical example of a Click
event-based action tag is presented in the ShowAndHideAction
class. It defines attributes Show
and Hide
as comma-delimited strings, intended for listing controls that should be displayed or hidden respectively upon the triggering of the Click
event. The EventAction
method here parses these strings and sets each individual control's Visible
attribute to true
or false
accordingly.
protected override void EventAction(Object o, EventArgs e) {
Control c;
if (_hide != "") {
string[] hides = _hide.Split(',');
for (int i=0; i<hides.Length; i++) {
c = LocateControl(hides[i]);
if (c != null) c.Visible = false;
}
}
if (_show != "") {
string[] shows = _show.Split(',');
for (int i=0; i<shows.Length; i++) {
c = LocateControl(shows[i]);
if (c != null) c.Visible = true;
}
}
}
Note that this inheriting ActionTagControl
makes use of the protected method LocateControl
, which was defined in the base class.
The following is an example of a .aspx page that demonstrates the ShowAndHideAction
attached to LinkButton
controls:
<%@ Register TagPrefix="actions" Namespace="ActionTags"
Assembly="ActionTags" %>
<html>
<head><title>ShowAndHideAction - Sample 1</title></head>
<body>
<form runat="server">
<asp:LinkButton id="Hide" runat="server"
text="Hide Text Boxes" />
|
<asp:LinkButton id="Show" runat="server"
text="Show Text Boxes" />
<br /><br />
<asp:Textbox id="text1" runat="server" text="text1" />
<asp:Textbox id="text2" runat="server" text="text2" />
<asp:Textbox id="text3" runat="server" text="text3" />
<actions:ShowAndHideAction
id="hide1" runat="server" attachTo="Hide"
hide="text1,text2,text3" />
<actions:ShowAndHideAction
id="show1" runat="server" attachTo="Show"
show="text1,text2,text3" />
</form>
</body>
</html>
A more complex example of the use of the ShowAndHideAction
tag is provided with the source file showHide2.aspx.
Inheriting class Example 2: Selector
Another example of an ActionTagControl
inheritor is provided with the Selector
class. This action is designed to be triggered by a selection from a ListControl
rather than a button click. Each item in the list is associated with a Panel
(or other Control
); the visibility of a Panel
depends on whether or not it is selected in the list.
The SelectedIndexChanged
event is specified in the Selector
class' constructor, overriding the Click
event specified by the base:
public Selector() : base() {
_triggerEvent = "SelectedIndexChanged";
}
To ensure that the associated panels (or controls) are all available on the page, the OnInit
method is overridden with code to locate each. If one is not found, an exception is thrown. The base class' OnInit
method is still called to ensure the association of the action tag with the desired ListControl
.
private Control[] _panels;
protected override void OnInit(EventArgs e) {
base.OnInit(e);
Control c = this.AttachedControl;
if (c != null) {
ListControl lc = (ListControl) c;
_panels = new Control[lc.Items.Count];
for (int i=0; i<lc.Items.Count; i++) {
_panels[i] = LocateControl(lc.Items[i].Value);
if (_panels[i] == null) {
string msg =
string.Format("Selector is attached to {0}," +
" which has a ListItem value of {1}. " +
"A control with id='{1}' is not found.",
c.ID, lc.Items[i].Value);
throw new Exception(msg);
}
}
}
}
The EventAction
for this tag then simply iterates through the attached control's list items, setting the visibility of the associated panel accordingly.
protected override void EventAction(Object o, EventArgs e) {
Control c = this.AttachedControl;
if (c != null) {
ListControl lc = (ListControl) c;
for (int i=0; i<lc.Items.Count; i++) {
_panels[i].Visible = (lc.Items[i].Selected);
}
}
}
An example of the usage of the Selector
action tag is provided in the source code file panelSelector.aspx. Note that the AutoPostBack
attribute of the attached ListControl
should be set to true
for the event to be triggered.
Inheriting class Example 3: Query
The Query
action tag is provided as a final example. Its purpose is to execute a query against an OLEDB data source and bind the results to any user interface control that supports the DataSource
property. The Query
tag inherits from ActionTagControl
and defines two properties to support each, the identification of an OLEDB connection string and a SQL statement. Its constructor defines PreRender
as its triggering event - thus, the PreRender
event of a DataGrid
, Repeater
, or other DataSource
-supporting control is where the SQL statement is executed and results bound.
public class Query : ActionTagControl {
private string _connectionString = "";
private string _sql = "";
public string ConnectionString {
get {return _connectionString;}
set {_connectionString = value;}
}
public string Sql {
get {return _sql;}
set {_sql = value;}
}
public Query() : base() {
_triggerEvent = "PreRender";
}
protected override void EventAction(Object o, EventArgs e) {
OleDbConnection con = new OleDbConnection(_connectionString);
OleDbDataAdapter da = new OleDbDataAdapter();
da.SelectCommand = new OleDbCommand(_sql, con);
DataSet ds = new DataSet();
da.Fill(ds);
Type t = this.AttachedControl.GetType();
PropertyInfo pi = t.GetProperty("DataSource");
pi.SetValue(this.AttachedControl, ds, null);
this.AttachedControl.DataBind();
con.Close();
}
}
As in the base class, System.Reflection
classes are used here to probe the attached control for a DataSource
property whose value can then be set to the results of the executed query. In this case, a PropertyInfo
object is obtained and its SetValue
method called.
There are many ways in which this Query
action tag could be enhanced. For example, connection information could be obtained through a dedicated connections file, or as an application parameter through the web.config file. This example was kept simple to remain focused on the elements of reflection while still being functional.
This example .aspx page demonstrates the use of the Query
action tag to populate a DropDownList
control with data from a Microsoft Access table.
<%@ Register TagPrefix="actions"
Namespace="ActionTags" Assembly="ActionTags" %>
<html>
<head><title>Query Examples</title></head>
<body style="font-family: Tahoma;">
<form>
<asp:DropDownList id="dd1" runat="server"
DataTextField="Field1" DataValueField="Field2" />
</form>
</body>
</html>
<actions:Query id="myQuery" runat="server" attachTo="dd1"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data
Source=c:\inetpub\wwwroot\ActionTags\test.mdb"
sql="SELECT Field1, Field2 FROM Table1"
/>
The source file query.aspx shows an additional example using a DataGrid
.
Summary
This article demonstrates an alternative approach to sub-classing user interface controls for encapsulating reusable functionality: the development of a library of action tags. The implemented tags are based on an abstract ActionTagControl
class with an AttachTo
attribute, using the EventInfo
object of the System.Reflection
namespace to define a consistent means for associating functionality with a triggering event. For the programmer, this approach can result in the convenience of not having to subclass several controls (e.g. Button
, LinkButton
, and ImageButton
) for the same functionality. The development and distribution of such libraries may also serve to make ASP.NET a more practical platform choice for non-programmers.