Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Action Tags

0.00/5 (No votes)
5 Oct 2003 1  
An approach to encapsulating functionality in server controls, separate from a user interface.

ActionTags

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 = "";

// AttachTo property - the id of the

// control whose event will trigger this action

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:

// constructor 

    public ActionTagControl() : base() {
        // default to the Click event; inheritors can change this

        // through their own constructors

            _triggerEvent = "Click";
    }

The base class also defines a TriggerEvent public property so a tag user may override the event if appropriate:

protected string _triggerEvent;

    // TriggerEvent property - the event which will trigger this action;

    // typically setting this would not be necessary as inheriting classes

    // will establish the default _triggerEvent in their constructors

    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:

// inheriting ActionTags must supply an EventAction method

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) {

    // is an AttachTo attribute specified?

    if (_attachTo.Length == 0) {
        string msg = "The 'AttachTo' attribute "
            "is required.  Specify a control"
            " in the AttachTo attribute.";
        throw new Exception(msg);
    }

    // attempt to locate the specified control            

    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:

// look for the control identified by sID

protected Control LocateControl(String sID) {
    Control c;
            
    // look in this control's naming container first

    c = this.FindControl(sID);
            
    // if not found, look in the naming

    // container of the parent of this control

    if (c == null && this.Parent != null) {
        c = this.Parent.FindControl(sID);
    }
            
    // if still not found then look in

    // the naming container for the Page itself

    if (c == null && this.Page != null) {
        c = this.Page.FindControl(sID);
    }

    // at this point we either found the

    // control or c is null; return it in either case

    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.

// attempt to get the attached control's

// triggering event through reflection

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:

// attempt to attach our EventAction to the appropriate control's 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;
            
    // parse the comma-delimited list of controls to hide

    if (_hide != "") {
        string[] hides = _hide.Split(',');
        for (int i=0; i<hides.Length; i++) {
            c = LocateControl(hides[i]);
            if (c != null) c.Visible = false;
        }
                
    }
            
    // parse the comma-delimited list of controls to show

    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:

// create a constructor to override the _triggerEvent value

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;

    // we'll override the OnInit method to locate each

    // panel (control) and throw an exception

    // if we can't find one

    // panels (controls) are identified

    // through the ListItem.Value property

    protected override void OnInit(EventArgs e) {
        // call the base method first

        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) {
    // loop through each of the items in the AttachedControl's item list

    // setting the selected panels' visibility to true or false

            
    Control c = this.AttachedControl;
    if (c != null) {
        ListControl lc = (ListControl) c;
        for (int i=0; i<lc.Items.Count; i++) {
            // set the panel's visibility

            _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;}
        }

        // create a constructor to override the _triggerAction value

        public Query()  : base() {
            _triggerEvent = "PreRender";
        }

        // the event action here is to execute

        // the query and bind the results

        protected override void EventAction(Object o, EventArgs e) {
            // get the connection and execute the query

            OleDbConnection con = new OleDbConnection(_connectionString);
            OleDbDataAdapter da = new OleDbDataAdapter();
            da.SelectCommand = new OleDbCommand(_sql, con);
            DataSet ds = new DataSet();
            da.Fill(ds);
            
            // bind the query results to the AttachedControl

            // we'll use reflection here to set the DataSource property;

            // an exception occurs if the target control does not have

            // a DataSource property.

            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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here