In this tutorial we’ll examine the concept of script controls, how they differ from extender controls and how we can build a base abstract class which implements client state and dynamic loading of embedded CSS files. In my previous article titled ASP.NET 3.5 AJAX Extender Controls we examined AJAX extender controls and how they can be used to extend the functionality of a standard ASP.NET control, however, there is another side to the coin, that is, custom controls with AJAX script functionality. Such controls do not extend the functionality of an existing control but rather represent an entirely new control, which also implements AJAX script functionally. Before we examine any code, we’ll briefly analyze the two control types and discuss in greater detail the differences, after which we’ll build a base script control class that can be inherited from when we wish to implement custom script controls.

Extender vs. Script Controls

We know that an extender control inherits from the ExtenderControl base abstract class. We also know that the ExtenderControl base abstract class ensures that a ScriptManager control is present and registers script descriptors and script reference objects with the ScriptManager. The extender control also registers the derived class with the ScriptManager. The control is registered accordingly, that is, as an extender control.

By contrast, the ScriptControl base abstract class equally ensures that a ScriptManager control is present, and also registers the derived class with the ScriptManager. However, differing slightly, as a control which inherits from the ScriptControl base abstract class is considered a script control and the derived control is registered with the script manager accordingly. That is as a script control, as opposed to an extender control. What then are the differences between an extender control and a script control?

First and foremost, an extender control was designed to extend a control of a specific type. Hence the metadata attribute TargetControlType. An extender control is designed to extend the behavior of another control, for example, consider the FocusExtender built above, the textbox functions as expected of a textbox control, with however an extended behavior, that is, the background color changes indicating focus. A script control on the other hand, is an entirely new control, which can be said for any custom server control, however the script control also includes a client-side script object, that is, the client-side control object representing, that of the rendered script control markup and can thus manage the way the control functions and behaves.

The client-side script control inherits from the Sys.UI.Control base class, as opposed to an extender behavior, which inherits from the base class Sys.UI.Behavior. The following section examines the difference between an extender and a script control, from the viewpoint of client-side script.

Components, Behaviors and Controls

First and foremost, all client-side components must inherit from the Sys.UI.Component base class. This class provides the base functionality for the Control and Behavior classes, as well as any other objects whose lifetime should be managed by the ASP.NET AJAX infrastructure. While we’re not going to examine the properties and methods implemented by this class, we are going to examine why we may choose to inherit from this class, the behavior class or the control class.

As we’ve just stated, there are three different client-side objects we may construct – Components, Controls, and Behaviors. Components lie at the foundation of all client-side objects and is generally used to represent objects which have no UI representation, such as a timer object which may invoke methods at specific intervals, although is not visible on the page. As previously stated, the Sys.UI.Component class is the base class for both behavior and control objects.

Behavior objects on the other hand, as we’ve been discovering, extend the behavior of DOM elements, such as the focus extender control. Behavior objects should inherit from the Sys.UI.Behavior base class, which in turn inherits from the Sys.UI.Component base class.

Generally behavior controls don’t alter the markup of the associated DOM element, however, can create DOM objects. A single DOM element can and often consists of multiple behavior objects and an array of behavior objects can be retrieved using the Sys.UI.Behavior.getBehaviors() method, passing as an argument the element whose behavior objects we wish to retrieve.

Controls represent a DOM element, as opposed to extending the behavior of such an element. Controls often modify the markup of the DOM element, which it represents and alters its behavior, again as opposed to simply extending the behavior of the represented element. For example, consider a bulleted list constructed using ul and li elements, while generally such elements used in conjunction with each other will result in a bulleted list, a client control can completely alter the behavior of such elements, perhaps the result being a tabbed control, or a menu control. Finally, client controls inherit from the Sys.UI.Control base class.

Again, to identify the major differences, both behavior objects and control objects work with a given element, however behaviors alter the behavior of the target element while keeping the general structure intact. A control on the other hand represents an element and completely alters the behavior and structure, to, in essence, construct a new control.

One Class to Rule Them All

First and foremost, the ScriptControl base abstract class packaged with the AJAX Framework should be inherited from when implementing custom script controls, however this class is very simple and implements the IScriptControl interface, which simply defines the two methods that must be implemented, that is, the methods to return the controls script descriptor and script references. These methods function in a manner quite similar to that seen with extender controls, however rather than returning a ScriptBehaviorDescriptor object type the ScriptControlDescriptor object is returned from the GetScriptDescriptors() method. The GetScriptReference() method returns a collection of ScriptReference objects, as is the case with extender controls. Finally, the ScriptControl base abstract class also implements the WebControl class. The ScriptControl base abstract class has the following definition:

public abstract class ScriptControl : WebControl, IScriptControl
{
    // Fields
    private IPage _page;
    private IScriptManagerInternal _scriptManager;

    // Methods
    protected ScriptControl();
    internal ScriptControl(IScriptManagerInternal scriptManager, IPage page);
    protected abstract IEnumerable<ScriptDescriptor> GetScriptDescriptors();
    protected abstract IEnumerable<ScriptReference> GetScriptReferences();
    protected internal override void OnPreRender(EventArgs e);
    protected internal override void Render(HtmlTextWriter writer);
    IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors();
    IEnumerable<ScriptReference> IScriptControl.GetScriptReferences();

    // Properties
    private IPage IPage { get; }
    private IScriptManagerInternal ScriptManager { get; }
}

Notice that the ScriptControl base abstract class also overrides the OnPreRender() and Render() methods. It's within the OnPreRender() method that the derived class is registered with the pages ScriptManager control. Thus, during the rendering process, all script controls are iterated and the corresponding GetScriptDescriptors() and GetScriptReferences() methods are invoked. The script descriptors are registered with the ScriptManager within the Render() method. Simply put, the server-side architecture for script controls are pretty much the same as extender controls, say for the differences already noted.

The ScriptControl base abstract class looks great and is pretty easy to work with, so the question is, why develop our own base abstract class? The answer is simple, we wish to implement additional features not supported by the ScriptControl class, and the only reusable approach is to develop an additional base abstract class. The list below outlines the two features we're going to implement.

  • Client state
  • Registration of embedded CSS files

Client State

You've heard of view state and control state, however what we're going to discuss in this section is client state, that is, the state of a client control. This is important because our application model has heavily shifted from standard synchronous postback model to an asynchronous AJAX-enabled postback model. For example, consider a standard control which operates upon postbacks, that is, once a control is altered a request is then posted to the server which is then fulfilled server-side. As we know, when a postback occurs all client controls along with their values are gathered and sent to the server within the request body. The Framework then reconstructs all controls of the posted page. All controls which should manage postback data inherit from the IPostBackDataHandler and implement the LoadPostData() method. It’s within this method that the desired values are retrieved from the request body and are used to repopulate the server-side properties, thus reconstructing the server-side object to match that of the client-side state after any alterations may have occurred. For example, consider a ListBox with the AutoPostBack property set to true, once posted to the server, the response body contains the control with the newly selected value. The server-side control then retrieves the posted value and populates the SelectedIndex property, hence updating the server-side instance to match that of the client-side control state, this allows us to retrieve the selected index within our server-side logic and use it in any way we see fit.

Control state and view state cannot be altered client-side, thus a reason the control attributes are included within the request body. That is, as discussed, the properties and values found within the request body are retrieved from server-side code and the client and/or view state is updated on the server, thus persisting data across postbacks, as HTTP is stateless. However, consider that a control is altered from client-side code and a standard postback is performed. Any alterations to the state of the client-side control will not be persisted, that is, upon returning the response to the client, the state of the control will be returned to that found within view/control state. Hence the idea of client state.

The main difference between view/control state and client state come in the way client state data is handled and persisted; control state and view state are constructed within the server and the __VIEWSTATE hidden field populated with a hashed string containing the various control and view state properties. Our implementation of client state on the other hand won’t use the __VIEWSTATE hidden field; we’ll construct an entirely new hidden field, thus allowing us to alter the content of control state within our client code. The implementation of client state is nearly identical to the mechanism used when retrieving and populating control state, that is, we will build a load and save method which can either append data to client state or extract data from the client state hidden field. Our client-side object must also contain a client state load and save method which should be invoked when initialized. That said, we'll also build a new client-side base abstract class our client controls should inherit from.

Registration of Embedded CSS Files

The next feature our base abstract class will implement is the registration of embedded CSS files. For example, consider that you've embedded several CSS files within your custom controls assembly and you wish to register these files with the page, that is, include a link tag indicating the URL of the CSS file(s). While this can be done manually, building a reusable approach is considerably more attractive.

Our Base Script Control Class

To begin, our base class must inherit from the ScriptControl base abstract class, which acts much in the same fashion as the ExtenderControl base abstract class. With one major difference, it has been designed to be used by custom script controls rather than extenders. First and foremost the ScriptControl base abstract class only differs slightly from the ExtenderControl base class, as already discussed. Script controls needn’t extend another control, thus the target control ID property previously seen is no longer needed and the script descriptor object also differs, as opposed to the ScriptBehaviorDescriptor object previously used to define script descriptors we’ll make use of the ScriptControlDescriptor object. It’s important to note that as the first argument to the default constructor we must pass a string representation of the client-side control type, thus indicating to the AJAX framework the client-side object to initialize. This proves to be a problem at first glance, as we’re developing a base control object which also includes client-side functionality and thus descriptors. That said, how is it that we can supply a string representation of the control client-side control type?

The simple answer is to define a metadata attribute on all classes inheriting from our base script control class, thus allowing us to supply, as an argument to the ScriptControlDescriptor object, the string representation of the control type. We need only define a new class inheriting from the System.Attribute class and define the properties and constructor, as follows in the code snippet below.

class ClientScriptDescriptorAttribute : Attribute
{
    private string _componentType;
    public string ComponentType
    {
        get { return _componentType; }
        set { _componentType = value; }
    }

    public ClientScriptDescriptorAttribute(string type)
    {
        ComponentType = type;
    }

}

We can define our custom script control class with the above attribute to specify the client-side class that should be created when initialized.

Finally, our base script control class, as previously noted, must inherit from the ScriptControl base abstract class and implement the INamingContainer interface, which as you may, or may not know, creates namespaces for our children of our control. Lastly, we must also implement the IPostBackDataHandler interface enabling us to handle postback data, which is required to retrieve the client state object sent from the client as part of the request body. Our base class is defined as follows:

public abstract class ScriptControl : System.Web.UI.ScriptControl, INamingContainer, IPostBackDataHandler

Server-side Client State Implementation

As we're implementing the IPostbackDataHander interface we must implement the methods LoadPostData() and RaisePostDataChangedEvent(). Its within the LoadPostData() method that we can retrieve the client state object from the request body and populate a protected dictionary object with the content found in client state. To simplify the management of client state, we'll implement a protected property much in the same way the ViewState property is implemented in the Control class. The property is implemented as follows.

private Dictionary<string, object> _clientState;
protected Dictionary<string, object> ClientState
{
    get
    {
        if (_clientState == null)
            _clientState = new Dictionary<string, object>();

        return _clientState;
    }
}

The above property allows us to retrieve client state data in a manner very similar to retrieving view state data. Adding an object to client state is as simple as defining the key which should be set and the associated data. We'll examine this in a moment, however before we continue let us analyze the structure of the client state object. Again, as previously stated, client state is stored in a hidden field within the HTML markup, much like the __VIEWSTATE hidden field used by the ASP.NET Framework to store view and control state. However, our client-side implementation differs slightly as the data is stored in plain-text, that is, it's not hashed or encrypted, thus allowing for easy retrieval from client-side code. The method below will be invoked from within the PreRender event to serialize the client state property (the Dictionary shown in the listing above).

protected string SaveClientState()
{
    MemoryStream ms = new MemoryStream();
    DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(IDictionary));
    ser.WriteObject(ms, ClientState);

    string cs = Encoding.Default.GetString(ms.ToArray());

    ms.Close();
    return cs;
}

The SaveClientState() method shown above is invoked to serialize the ClientState Dictionary object into JSON text. The object is serialized as a JavaScript array of complex types. Consider the serialized output below.

[{"Key":"activeIndex", "Value":"5"}]

Given the above JSON text, when deserialized, the activeIndex client state property can be accessed from client-side code, as shown below.

desObj[0].Value

That is, assuming that the JSON text was deserialized to a variable named desObj. Note that the JSON serializer that shipped with AJAX 1.0 doesn't serialize the Dictionary object as an array, as shown above. If we wanted similar behavior we use the JSON serializer from 1.0, that is, the System.Web.Script.Serialization.JavaScriptSerializer object, as shown in the listing below.

protected string SaveClientState()
{
    System.Web.Script.Serialization.JavaScriptSerializer jsSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
    return jsSerializer.Serialize(ClientState);
}

The JSON serialized text would then look like that shown below. Note the omission of the JavaScript array (an array is enclosed in brackets []).

{"activeIndex":"5"}

Give the above JSON text, when deserialized, the activeIndex client state property can be accessed from client-side code, as shown below.

desObj.activeIndex

Okay, back on track! The SaveClientState() method, as previously noted, is invoked from within the pre render event, thus client state cannot be modified after the pre render event. The overridden OnPreRender() method is shown in the listing below.

protected override void OnPreRender(EventArgs e)
{
    // Write hidden field to page
    ScriptManager.RegisterHiddenField(this, ClientStateKey, SaveClientState());

    // This notifies the page that this control should handle postback data
    Page.RegisterRequiresPostBack(this);

    base.OnPreRender(e);
}

Notice that we make use of the ScriptManager method RegisterHiddenField() to register the hidden field with the page. By using the ScriptManager rather than the ClientScript object exposed by the Page object we receive partial-page postback compatibility. That is, if client state is altered on the server during a partial-page postback the hidden field will be updated to reflect any changes. If we were to use the ClientScript object exposed by the Page object, the changes would only apply during a full postback.

From the code listing above, note that the ClientStateKey property is used as the ID of the hidden field, this property is defined as follows.

protected string ClientStateKey
{
    get { return "__CLIENTSTATE_" + this.ClientID; }
}

You may have noticed that the OnPreRender() method defined above also notifies the Page object that our control should handle postback data, thus giving us the opportunity to retrieve the client state hidden field from the response body. The LoadPostData() method inherited from the IPostBackDataHandler interface is defined as follows.

public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
    if (!string.IsNullOrEmpty(postCollection[ClientStateKey]))
        LoadClientState(postCollection[ClientStateKey]);

    return false;
}

If the hidden field is found, the LoadClientState() method is invoked, passing the JSON serialized text. Remember, during a postback, the value of the client state hidden field will be part of the request body and is serialized as JSON text, thus we must deserialize the JSON text and reconstruct the ClientState object, as shown in the LoadClientState() method below.

protected void LoadClientState(string csField)
{
    Stream ms = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(csField));

    // Deserialize client state and re-populate ClientState object
    DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(Dictionary<string, object>));
    Dictionary<string, object> clientState = (Dictionary<string, object>)ser.ReadObject(ms);
    
    // Populate the client state object
    IDictionaryEnumerator ienum = clientState.GetEnumerator();
    while (ienum.MoveNext())
    {
        ClientState[ienum.Key as string] = ienum.Value;
    }
}

Note that if you were to use the older System.Web.Script.Serialization.JavaScriptSerializer object to serialize client state into JSON text, you should make use of the same object to deserialize the JSON text. Finally, note that in the code listing above we make use of the new DataContractJsonSerializer object. The client state is stored within a MemoryStream object and passed to the ReadObject() method which will deserialize the JSON text into a Dictionary object. We then iterate through the collection and repopulate the ClientState object with the persisted name/value pairs.

That's all there is to it! We've developed methods to load and save client state to a global protected object coincidentally named ClientState. Storing and retrieving properties from client state is as simple as view state. For example, consider that you wish to persist the active index of a tab control in client state, you may do so using the following code.

ClientState["activeIndex"] = 1;

We can retrieve the activeIndex property from client-side code, which we will discover in a few moments and also modify the activeIndex property from client-side code. When modified from client-side code, the modifications will be serialized and sent to the server within the request body, the LoadClientState() method seen above will be invoked to retrieve the modified activeIndex property and populate the ClientState object with the new value, thus persisting client state changes across postbacks.

Client-side Client State Implementation

Because client state must be manipulated from client-side code, we must also implement a base class that our client-side controls should inherit from. This base class is quite simple and implements a method to load client state from the HTML hidden field into a JavaScript object and also binds event handlers to the begin and end requests of a partial page update. The begin request of a partial page update is used to save the current client state into the HTML hidden field, which as discussed above, will be included within the HTTP request object. The method to save client state should be overridden by implementers, which we will discover in a few moments. Finally, the end request of a partial page update is used to deserialize client state into a private class property. The default constructor is shown below.

Type.registerNamespace('Apt.Controls');

Apt.Controls.ControlBase = function(element)
{
    Apt.Controls.ControlBase.initializeBase(this, [element]);
    
    this._clientStateField = null;
    this._clientState = null;
    
    this._onSubmitHandler = null;
    this._onPartialUpdateEnd = null;
}

The table below examines each property in detail.

Property Description
_clientStateField This property represents the HTML hidden field which stores client state.
_clientState This property will hold the deserialized JavaScript object.
_onSubmitHandler The on submit handler. The method represented by this delegate will be invoked when a postback, either asynchronous or synchronous occurs.
_onPartialUpdateEnd The method to be invoked when a partial page update has completed.

Below you'll find the initialize() and dispose() methods. Notice how we bind the begin and end request event handlers using the client-side PageRequestManager.

Apt.Controls.ControlBase.prototype =
{
    initialize : function()
    {
        Apt.Controls.ControlBase.callBaseMethod(this, 'initialize');
        
        this.loadClientState();
        
        this._onSubmitHandler = Function.createDelegate(this, this._onSubmit);
        this._onPartialUpdateEnd = Function.createDelegate(this, this._onUpdateEnd);
        
        if (typeof(Sys.WebForms)!== "undefined" && typeof(Sys.WebForms.PageRequestManager)!== "undefined")
        {
            Array.add(Sys.WebForms.PageRequestManager.getInstance()._onSubmitStatements, this._onSubmitHandler);
            Sys.WebForms.PageRequestManager.getInstance().add_endRequest(this._onPartialUpdateEnd);
        }
        else 
            $addHandler(document.forms[0], "submit", this._onSubmitHandler);
    },
    
    dispose : function()
    {
        if (typeof(Sys.WebForms)!== "undefined" && typeof(Sys.WebForms.PageRequestManager)!== "undefined")
        {
            if (this._onSubmitHandler)
            {
                Sys.WebForms.PageRequestManager.getInstance().remove_endRequest(this._onPartialUpdateEnd);
                this._onSubmitHandler = null;
            }
            if (this._onSubmitHandler)
            {
                Array.remove(this._pageRequestManager._onSubmitStatements, this._onSubmitHandler);
                this._onSubmitHandler = null;
            }
        }
        else
        {
            if (this._onSubmitHandler)
            {
                $removeHandler(document.forms[0], "submit", this._onSubmitHandler);
                this._onSubmitHandler = null;
            }
        }
        
        if (this._clientStateField)
            this._clientStateField = null;
        if (this._clientState)
            this._clientState = null;
            
        Apt.Controls.ControlBase.callBaseMethod(this, 'dispose');
    },

For the most part, everything should look relatively simple, that is, the partial page update begin and end requests are bound to event handlers as is the form tags submit event. The form tags submit event is handled only if an instance of the PageRequestManager  and Sys.WebForms isn't found, that is, partial page updates have been disabled using the ScriptManager control, in which case, a standard postback will occur and binding to the form tags submit event gives us an opportunity to save client state and prepare it for the journey to the web server within the HTTP request body. Finally, the event handler methods are shown in the code listing below.

_onSubmit : function()
{
    if (!this._clientStateField)
        return true;

    if (null != this.saveClientState())
    {
        this._clientStateField.value = this.saveClientState();
    }

    return true;
},

_onUpdateEnd : function()
{
    this.loadClientState();
    return true;
},

Notice that the _onUpdateEnd() method, as the name suggests, will be invoked upon the completion of a partial page update request, invokes the method loadClientState(). This method simply deserializes the content of the HTML hidden field into the property _clientState, as shown below.

loadClientState : function()
{
    if (this._clientStateField.value == '' || this._clientStateField.value == 'undefined')
        return;
    this._clientState = Sys.Serialization.JavaScriptSerializer.deserialize(this._clientStateField.value);
},

The saveClientState() method, invoked from the _onSubmit() method shown above, should be overridden in all derived classes. The reason being, to allow for all client state objects to be collected and serialized on a per application basis. The saveClientState() method implemented in our base class is shown below.

saveClientState : function()
{
    return null;
},

And, the overridden saveClientState() method for a tabs control is shown below.

saveClientState : function()
{  
    var state = {
        activeViewIndex:this.get_activeTabIndex()
    };
    return Sys.Serialization.JavaScriptSerializer.serialize(state);
},

The remaining portion of the base class is the get and set accessors for the properties discussed above, for the sake of completeness, the code is shown below.

get_clientStateField : function()
{
    return this._clientStateField;
},

set_clientStateField : function(value)
{
    if (this._clientStateField != value)
    {
        this._clientStateField = value;
    }
},

get_clientStatd : function()
{
    return this._clientState;
},

set_clientState : function(value)
{
    if (this._clientState != value)
    {
        this._clientState = value;
    }
}
}

Finally, our base class inherits from the Sys.UI.Control base class, as shown below.

Apt.Controls.ControlBase.registerClass('Apt.Controls.ControlBase', Sys.UI.Control);

Script Descriptors and Script References

Above we briefly examined the differences between the ExtenderControl and ScriptControl base abstract classes. We know that the ScriptControl class makes use of the ScriptControlDescriptor object, which again as we know requires a string representation of our control type. Above we’ve tackled this issue by defining a custom Attribute object which can be affixed to our classes and the a string representation of our client-side control type specified. Before we examine the usage of the attribute object, let us first examine the registration of script descriptors within our ScriptControlBase class.

Script descriptor objects are defined quite similar to what we’ve previously seen with extender controls, that is the GetScriptDescriptors() method inherited from the ScriptControl base abstract class should be overridden as shown below.

protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    if (ClientControlType == null)
        throw new Exception("ClientScriptDescriptorAttribute must be defined");

    ScriptControlDescriptor desc = new ScriptControlDescriptor(ClientControlType, this.ClientID);

    // Add client stated field
    desc.AddElementProperty("clientStateField", ClientStateKey);

    this.GetScriptDescriptors(desc);

    yield return desc;
}

Notice that our first order of business is to ensure that the ClientControlType property is present, if the value is null, the previously discussed attribute has not been defined and an exception will be thrown as a script descriptor object can’t be constructed without the knowledge of the client-side control type.

Finally notice that as an element property of the script descriptor we define the client state key, thus allowing our client-side code to retrieve the client state hidden field. Last but not least, our ClientControlType property is defined as follows:

public string ClientControlType
{
    get
    {
        ClientScriptDescriptorAttribute attribute = (ClientScriptDescriptorAttribute)TypeDescriptor.GetAttributes(this)[typeof(ClientScriptDescriptorAttribute)];
        if (attribute == null)
            return null;
        return attribute.ComponentType;
    }
}

The ClientScriptDescriptorAttribute object is retrieved from the controls metadata. Recall from the class definition above, the property ComponentType should be defined with the string representation of the client-side class type which can be used when declaring our script descriptor object.

Returning to the definition of our script descriptors, note that the method this.GetScriptDescriptors() is invoked passing as an argument the newly created ScriptControlDescriptor instance. This method is defined as an abstract method requiring our inherited objects to override this method to define additional script descriptor attributes. The identical approach has been taken with script reference objects as well. Both abstract methods are defined as follows and must be overridden from objects inheriting from this class.

protected abstract void GetScriptReferences(IList<ScriptReference> references);
protected abstract void GetScriptDescriptors(ScriptControlDescriptor descriptor);

Lastly, our script references are registered within the GetScriptReferences() method, just as previously seen extender controls. Within this method we register the JavaScript file which includes the definition of the client-side base control class. This method is defined as shown below.

protected override IEnumerable<ScriptReference> GetScriptReferences()
{
    List<ScriptReference> scriptReferences = new List<ScriptReference>();
    scriptReferences.Add(new ScriptReference("MiScriptControl.ControlBase.js", "MiScriptControl"));

    this.GetScriptReferences(scriptReferences);
    return scriptReferences;
}

Wrapping Up: Sample Tabs Control

To wrap up our discussion we'll build a very simple AJAX-enabled tabs control. The control is actually take from my book ASP.NET AJAX Programming Tricks and again, is a very simple client state enabled tabs control. The control inherits from our base script control class developed above, as client state is a required attribute. For example, consider that the user has selected a specified tab and a standard postback occurs, without client state, the tab will revert to its original state, however utilizing client state, we can persist the state across standard postbacks. Pretty cool, huh? Let's get to it, shall we?

Aside from the active tab index persisting across postbacks via client state, the following other design requirements should be taken into account when designing our tab control.

  • Each tab panel should be an independent object, that is, each tab, while children of our parent tab container, should function independently of one another.
  • Each tab panel should have a content template property allowing users of the tab control to define the content of each tab either programmatically or declaratively.
  • The tab container should track the active tab index and make use of client state to persist the index across postbacks and allow for alterations to the property from within client-side code.

Server-side Implementation

As we’ve defined above, each tab panel should be independent of the one another, thus we need two different controls, the Tabs control, which acts as the parent and tracks the active tab index and the TabPanel control which consists of the tab header and that tabs content. To begin, we’ll examine the Tabs control.

The Tabs control will make use of a ControlBuilder object, which allows us to identify an object as a child of another declaratively based on a friendly name rather than declaring the entire ASP.NET markup tag. That is, <TabPanel /> can be used to define a TabPanel control rather than <apt:TabPanel runat=”server” />. The ControlBuilder class is defined as follows:

public class TabPanelControlBuilder : ControlBuilder
{
    public override Type GetChildControlType(string tagName,
        System.Collections.IDictionary attribs)
    {
        if (tagName.Equals("tabpanel", StringComparison.OrdinalIgnoreCase))
            return typeof(TabPanel);

        return null;
    }
}

If the tag name equals tabpanel, the TabPanel object type is returned. Our Tabs class is defined with the following metadata attributes.

[MI.ClientScriptDescriptor("Apt.Controls.TabsControl")]
[ControlBuilder(typeof(TabPanelControlBuilder))]
[ParseChildren(false)]
public class Tabs : MI.ScriptControl

Notice the ClientScriptDescriptor metadata attribute which we examined above, recall that this attribute allows us to indicate the client-side control object.

All TabPanel objects will be parsed and stored within the following List object.

private List<TabPanel> _panels;
public List<TabPanel> Panels
{
    get
    {
        if (_panels == null)
            _panels = new List<TabPanel>();
        return _panels;
    }
}

Pretty simple, let's move on. The ActiveViewIndex property, shown below, makes use of client state to persist the active tab index. The property is defined below.

public int ActiveViewIndex
{
    get
    {
        int currIndex = ((ClientState.ContainsKey("activeViewIndex") == false) ? -1 : (int)ClientState["activeViewIndex"]);

        if (Panels.Count == 0)
            return -1;
        return currIndex;
    }
    set
    {
        if (Panels.Count == 0)
            ClientState["activeViewIndex"] = value;
        else
        {
            int currIndex = ((ClientState.ContainsKey("activeViewIndex") == false) ? -1 : (int)ClientState["activeViewIndex"]);

            if (value > Panels.Count || value < 0)
                throw new ArgumentOutOfRangeException("value");

            if (currIndex > -1)
                Panels[currIndex].Active = false;
            ClientState["activeViewIndex"] = value;
            Panels[value].Active = true;
        }
    }
}

Notice the get block. An attempt is made to first retrieve the client state object for our tab index, if no client state object exists, the integer value of -1 is stored into the currIndex property, otherwise the value of the tab index is returned from the client state object. Notice that when the tab index is set, assuming the index is not larger than the maximum number of panels added to the Panels List object, the Active property of the panel is set to true and the previously active tab is deactivated. This is used when the tab panel is rendered, an active tab is visible and a non-active tab is hidden using CSS. We’ll examine the TabPanel object in detail in a few moments.

The remaining portion of the Tabs control is actually quite simple and consists of nothing more than some basic logic for the tabs control to function as expected.  Recall that above one of our requirements was that each tab panel was to function independently of one another, however the Tabs container object is to manage the active tab index, thus for a tab panel to change its state to active the container object must be notified so that the currently active tab can be disabled. This can be accomplished using a property of the TabPanel object which will also be sent client-side via a script descriptor. This object is the Owner property and is assigned to each TabPanel object after successfully parsing and adding the object to our Controls collection. This is done within the AddedControl() method, which is invoked after a control has been added to the child collection.

protected override void AddedControl(Control control, int index)
{
    ((TabPanel)control).SetOwner(this);
    ((TabPanel)control).Width = this.Width;
    ((TabPanel)control).Height = this.Height;
    base.AddedControl(control, index);
}

Next up on our list is to register the script file with the ScriptManager. This is done by overriding the GetScriptReferences() method defined within the MI.ScriptControl class, examined above.

protected override void GetScriptReferences(IList<ScriptReference> references)
{
    references.Add(new ScriptReference("TabsControl.js"));
}

Last, but hardly least, is the rendering of the Tabs control (our container object):

protected override void RenderContents(HtmlTextWriter output)
{
    output.AddAttribute(HtmlTextWriterAttribute.Class, "aptTabHead");
    output.RenderBeginTag(HtmlTextWriterTag.Ol);
    foreach (TabPanel pan in this.Panels)
        pan.RenderTabHead(output);
    output.RenderEndTag();

    base.RenderContents(output);
}

Each tab header will be included within an Ol HTML tag. Each header should be rendered separately from the primary control, thus a RenderTabHead() method has been defined within the TabPanel class. This method simply renders the specified Title within an Li tag. Lastly, the base RenderContents() method is invoked which will render all child objects, which just so happen to be the TabPanel controls. Recall that the content of each tab is also defined within the TabPanel controls. That said, the TabPanel control is defined, as shown below.

[MI.ClientScriptDescriptor("Apt.Controls.TabPanel")]
[ParseChildren(true)]
public class TabPanel : MI.ScriptControl

Each tab header is rendered with a unique ID. This ID is the ID of the panel with the string "_tabHead" appended to the ID, thus making it easy for us to include the ID within the script descriptor so that we may manipulate the tab header from client-side code to apply the hover effects, onclick event handlers, etc. The RenderTabHead() method is defined as follows:

public void RenderTabHead(HtmlTextWriter writer)
{
    writer.AddAttribute(HtmlTextWriterAttribute.Id, ClientID + "_tabHead");
    writer.RenderBeginTag(HtmlTextWriterTag.Li);
    writer.Write(Title);
    writer.RenderEndTag();
}

Finally, the tab is either hidden or displayed based on the value of the Active property. Recall that this property is defined from within the Tabs control. If the panel is to be hidden, the CSS attributes are applied to the content div as defined within the AddAttributesToRender() method, shown below.

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
    writer.AddAttribute(HtmlTextWriterAttribute.Class, "aptTabContent");
    if (!Active)
    {
        writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "none");
        writer.AddStyleAttribute(HtmlTextWriterStyle.Visibility, "hidden");
    }

    base.AddAttributesToRender(writer);
}

Finally, the GetScriptDescriptors() method is defined, as shown below.

protected override void GetScriptDescriptors(ScriptControlDescriptor descriptor)
{
    descriptor.AddElementProperty("tabHead", ClientID + "_tabHead");
    descriptor.AddComponentProperty("tabOwner", _owner.ClientID);
}

Client-side Implementation

The client-side component of our Tabs control is actually quite simple and has only a few tasks. One of which to manage the active tab index and a list of panel controls associated with itself. However, let’s begin slightly different, let us examine the TabPanel client-side object prior to the container, Tabs object.

Our first order of business within the TabPanel object is to register itself with its owner (the container object), recall that it’s the owner that manages the tab index and the activation and deactivation of the different tab panels. The registration occurs within the initialize() method and is as simple as retrieving an instance of the owner object and adding itself to an array. Recall that the owner element was identified within the script descriptor and can thus be retrieved using the client-side get_tabOwner() method. The tabs are managed from within the Tabs client-side object via an array retrievable via the get_tabs() method, see the following code:

var owner = this.get_tabOwner();

// Determine tab index
this._tabIndex = owner.get_tabs().length;

// Register tab with the owner
Array.add(owner.get_tabs(), this);

The owner object will then have access to the panel via its array. Notice that the the index for the current panel can be retrieved by getting the length of the tabs array. The initialization method in its entirety can be found below.

initialize : function()
{
    Apt.Controls.TabPanel.callBaseMethod(this, 'initialize');
    
    var owner = this.get_tabOwner();
    
    // Determine tab index
    this._tabIndex = owner.get_tabs().length;
    
    // Register tab with the owner
    Array.add(owner.get_tabs(), this);
    
    // Apply click and mouse over events to the tab head
    $addHandlers(this._tabHead, {
        click:this._tabHead_onClick$delegate,
        mouseover:this._tabHead_onmouseover$delegate,
        mouseout:this._tabHead_onmouseout$delegate
    });
    
    // Apply css class to the tab head
    Sys.UI.DomElement.addCssClass(this._tabHead, "aptTabPanel");
    if (this._tabIndex == this._tabOwner.get_activeTabIndex())
        Sys.UI.DomElement.addCssClass(this._tabHead, "aptActiveTab");
    else
        Sys.UI.DomElement.addCssClass(this._tabHead, "aptDefaultTab");
}

Notice that the relevant event handlers are bound to the tab header, that is, the mouse over, mouseout and onclick events. Also notice that the active tab index can be retrieved from the owner, thus allowing us to assign the proper CSS class to this panels header. That is, if this panel is active, apply the active CSS style, otherwise the default style is applied to the tab header.

The event handler methods are quite simple and defined as follows:

_tabHead_onClick : function()
{
    this.get_tabOwner().set_activeTab(this);
},

_tabHead_onmouseover : function()
{
    // Apply CSS Class
    Sys.UI.DomElement.addCssClass(this._tabHead, "aptTabHover");
},

_tabHead_onmouseout : function()
{
    Sys.UI.DomElement.removeCssClass(this._tabHead, "aptTabHover");
},

Notice that during an onclick event, the set_activeTab() method of the owner object is invoked. This is done as the owner manages the current tab index and can deactivate the current tab, activate the new tab and alter the active tab index accordingly. This being said, each panel object must also define the methods which will be invoked from its owner, these methods are the activate() and deactivate() methods and are defined as follows:

activate : function()
{
    // Set Css Class
    Sys.UI.DomElement.removeCssClass(this._tabHead, "aptDefaultTab");
    Sys.UI.DomElement.addCssClass(this._tabHead, "aptActiveTab");
    
    // Display content
    this.get_element().style.visibility = 'visible';
    this.get_element().style.display = '';
},

deactivate : function()
{
    // Remove Active css and add default css class
    Sys.UI.DomElement.removeCssClass(this._tabHead, "aptActiveTab");
    Sys.UI.DomElement.addCssClass(this._tabHead, "aptDefaultTab");
    
    // Hide the tab content
    this.get_element().style.display = 'none';
    this.get_element().style.visibility = 'visible';
},

Activation and deactivation of a tab panel is rather simple and involves assigning the correct CSS class to the header and adjusting the display and visibility CSS attributes of the content object, as shown above.

Let's jump over to the TabsControl object and examine the class in its entirety.

Apt.Controls.TabsControl = function(element)
{
    Apt.Controls.TabsControl.initializeBase(this, [element]);
    
    this._tabIndex = null;
    this._tabs = null;
}

Apt.Controls.TabsControl.prototype =
{
    initialize : function()
    {
        Apt.Controls.TabsControl.callBaseMethod(this, 'initialize');
    },
    
    dispose : function()
    {
        Apt.Controls.TabsControl.callBaseMethod(this, 'dispose');
    },
    
    set_activeTab : function(tab)
    {
        var i = Array.indexOf(this.get_tabs(), tab);
        this.set_activeTabIndex(i);
    },
    
    saveClientState : function()
    {  
        var state = {
            activeViewIndex:this.get_activeTabIndex()
        };
        return Sys.Serialization.JavaScriptSerializer.serialize(state);
    },
    
    get_tabs : function()
    {
        if (this._tabs == null)
            this._tabs = [];
        return this._tabs; 
    },
    
    set_activeTabIndex : function(value)
    {
        // Disable the currently active tab
        this.get_tabs()[this.get_activeTabIndex()].deactivate();
        
        if (value > -1 && value < this.get_tabs().length)
        {
            // Activate the new tab
            this.get_tabs()[value].activate();
        }
        this._tabIndex = value;
    },
    
    get_activeTabIndex : function()
    {
        if (this._tabIndex == null)
            return this._clientState.activeViewIndex;
        
        return this._tabIndex;
    }
}

Apt.Controls.TabsControl.registerClass('Apt.Controls.TabsControl', Apt.Controls.ControlBase);

The TabsControl class doesn't require much explanation, however take a look at the saveClientState() method, notice how the JavaScript object is constructed and the currently active tab index is set. The object is then serialized and the saveClientState() method of the Apt.Controls.ControlBase class will store the serialized content into the HTML hidden field, thus persisting the tab state.

Using the Tabs Control

The Tabs control can be used just as you would any other, however, prior to using the control, it must be registered with the page (or application). The Default.aspx file which makes use of the control is shown below.

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register Assembly="__code" Namespace="Apt.Web.Controls" TagPrefix="apt" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
    <link href="Tabs.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager runat="server" id="Scriptmanager1"></asp:ScriptManager>
        
        <apt:Tabs ID="tab1" runat="server" Width="400px" Height="200px">
            <apt:TabPanel ID="TabPanel1" runat="server" Title="Tab Number 1">
                <ContentTemplate>
                    Heres tab number 1
                </ContentTemplate>
            </apt:TabPanel>
            <apt:TabPanel ID="TabPanel2" runat="server" Title="Tab Number 2">
                <ContentTemplate>
                    Click this: <asp:Button ID="btn1" runat="server" Text="Test"/>
                </ContentTemplate>
            </apt:TabPanel>
        </apt:Tabs>
    </form>
</body>
</html>

First and foremost, notice that the control is registered with the page. The __code assembly is identified, as the custom control is placed within the App_Code folder of our application. Secondly, notice that the Tabs.css file is manually referenced from the Default.aspx file, as previously stated, this should be done for us within our controls base abstract class. We'll develop this feature in the second part of this tutorial, stay tuned.

Finally, the screen shot below demonstrates the control in action.

image

Conclusion

In this tutorial we examined the ScriptControl base abstract class packaged with the ASP.NET AJAX Framework. We also examined the idea of client-state and one of many different ways we could implement client state. We know that client state is a method of persisting the state of AJAX-enabled controls, that is, controls that are altered within JavaScript code, yet the state should be persisted during postbacks. In part 2 of this tutorial we'll examine a method of registering CSS files with the ASP.NET page from our custom controls, rather than requiring the control user to register the CSS file(s) manually within their pages.

For more detailed information check out my books ASP.NET AJAX Programming Tricks and ASP.NET 3.5 AJAX Pocket Guide.

Download all source files