In the previous part
I talked about how to build the client control. In this part I'll show
how to put it all together to build an ASP.NET AJAX Enabled Server
Control. You can view the demo that demonstrate control usage here.
The
good thing about ASP.NET AJAX is that it supports fully programmable
interface for both Server and Client control. And make a connection
between both control. So you just need to put the control declaration
on the ASPX page and ASP.NET will make it work for you. On the other
hand, you have full access to the client APIs so that you can do some
manual calls to the client APIs as well.
Building the Server Control:
This
is an ASP.NET AJAX Extender control. Means it extends existing ASP.NET
control to enable ASP.NET AJAX on it. In this case this extender
extends ASP.NET Panel control, that is why it is called
CollapsiblePanelExtender. Here is an article to show you in details How To build Extender Controls.
Basically jQueryCollapsiblePanelExtender I am building here inherits directly from ExtenderControl. This required to implement 2 methods GetScriptDescriptors and GetScriptReferences which I am going to explore later. But now, I want to take your attention that I am using jQuery, and I wanted the developer to have the option to specify the path of the jQuery library. If he/she did not specify the path, the default path is used, which is the the one hosted by google at http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js. To do that I made a property and Call jQueryScriptPath that gets and sets jQuery javascript library path.
For
each property I defined in the client library, there is a mapped
property for it on the Server Side control. Same thing for events.
public string jQueryScriptPath
{
get { return (string)ViewState["jQueryScriptPath"]; }
set { ViewState["jQueryScriptPath"] = value; }
}
public string CollapseControlID
{
get { return (string)ViewState["CollapseControlID"]; }
set { ViewState["CollapseControlID"] = value; }
}
public string ExpandControlID
{
get { return (string)ViewState["ExpandControlID"]; }
set { ViewState["ExpandControlID"] = value; }
}
public string ImageControlID
{
get { return (string)ViewState["ImageControlID"]; }
set { ViewState["ImageControlID"] = value; }
}
public string ExpandedImage
{
get { return (string)ViewState["ExpandedImage"]; }
set { ViewState["ExpandedImage"] = value; }
}
public string CollapsedImage
{
get { return (string)ViewState["CollapsedImage"]; }
set { ViewState["CollapsedImage"] = value; }
}
public bool Collapsed
{
get { return (bool)(ViewState["Collapsed"] ?? false); }
set { ViewState["Collapsed"] = value; }
}
public bool SuppressPostBack
{
get { return (bool)(ViewState["SuppressPostBack"] ?? false); }
set { ViewState["SuppressPostBack"] = value; }
}
public string OnExpand
{
get { return (string)(ViewState["OnExpand"] ?? string.Empty); }
set { ViewState["OnExpand"] = value; }
}
public string OnExpanded
{
get { return (string)ViewState["OnExpanded"] ?? string.Empty; }
set { ViewState["OnExpanded"] = value; }
}
public string OnExpandComplete
{
get { return (string)ViewState["OnExpandComplete"] ?? string.Empty; }
set { ViewState["OnExpandComplete"] = value; }
}
public string OnCollapse
{
get { return (string)ViewState["OnCollapse"] ?? string.Empty; }
set { ViewState["OnCollapse"] = value; }
}
public string OnCollapsed
{
get { return (string)ViewState["OnCollapsed"] ?? string.Empty; }
set { ViewState["OnCollapsed"] = value; }
}
public string OnCollapseComplete
{
get { return (string)ViewState["OnCollapseComplete"] ?? string.Empty; }
set { ViewState["OnCollapseComplete"] = value; }
}
Maintaining Client State:
To maintain a client state, hidden field is used to store the
current state of the CollapsiblePanel. I copied this idea from the
AjaxControlToolkit. And actually it is totally make sense. This helps
to maintain CollapsiblePanel State across Postbacks. It worth to
mention that everything related to maintain client state is copied from
AjaxControlToolkit base class ExtenderControlBase. Below is the code
and it is comment for better explanation:
protected override void OnInit(EventArgs e)
{
//Create Client State Hidden Field and add to Controls collection
CreateClientStateField();
Page.PreLoad += Page_PreLoad;
base.OnInit(e);
}
protected override void OnLoad(EventArgs e)
{
if (!_loadedClientStateValues)
{
LoadClientStateValues();
}
base.OnLoad(e);
}
protected override void OnPreRender(EventArgs e)
{
//Must Call base class OnPreRender
base.OnPreRender(e);
//Saves Client State
SaveClientStateValues();
}
protected override void Render(HtmlTextWriter writer)
{
// Use ASP.NET's mechanism for ensuring Controls are within a form
// (since unexpected behavior can occur when ExtenderControls are not)
if (null != Page)
{
Page.VerifyRenderingInServerForm(this);
}
base.Render(writer);
}
private void Page_PreLoad(object sender, EventArgs e)
{
// Needs to happen sometime after ASP.NET populates the control
// values from the postback but sometime before Load so that
// the values will be available to controls then. PreLoad is
// therefore the obvious choice.
LoadClientStateValues();
}
/// <summary>
/// Creates or form ClientStateFiledID
/// </summary>
/// <returns>Client State Field Control ID</returns>
private string GetClientStateFieldID()
{
return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}_ClientState", ID);
}
/// <summary>
/// Creates HiddenField that is used to main client state
/// </summary>
/// <returns>ASP.NET HiddenField instance</returns>
private HiddenField CreateClientStateField()
{
// add a hidden field so we'll pick up the value
//
HiddenField field = new HiddenField();
field.ID = GetClientStateFieldID();
Controls.Add(field);
ClientStateFieldID = field.ID;
field.Value = this.Collapsed.ToString().ToLower();
return field;
}
/// <summary>
/// Loads client state and store it on ClientState Property.
/// It also fires ClientStateValuesLoaded if an event handler attached
/// </summary>
private void LoadClientStateValues()
{
if (!string.IsNullOrEmpty(ClientStateFieldID))
{
HiddenField hiddenField = (HiddenField)NamingContainer.FindControl(ClientStateFieldID);
if ((hiddenField != null) && !string.IsNullOrEmpty(hiddenField.Value))
{
ClientState = bool.Parse(hiddenField.Value);
}
}
if (null != ClientStateValuesLoaded)
{
ClientStateValuesLoaded(this, EventArgs.Empty);
}
_loadedClientStateValues = true;
}
/// <summary>
/// Saves Client State value into the HiddenField (ClientState Field)
/// </summary>
private void SaveClientStateValues()
{
HiddenField hiddenField = null;
// if we don't have a value here, this properties
// object may have been created dynamically in code
// so we create the field on demand.
if (string.IsNullOrEmpty(ClientStateFieldID))
{
hiddenField = CreateClientStateField();
}
else
{
hiddenField = (HiddenField)NamingContainer.FindControl(ClientStateFieldID);
}
if (hiddenField != null)
{
hiddenField.Value = ClientState.ToString().ToLower();
}
}
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool ClientState
{
get
{
return _clientState;
}
set
{
_clientState = value;
}
}
Implementing GetScriptDescriptors and GetScriptReferences:
GetScriptDescriptors Registers the ScriptDescriptor objects for the control. That would include properties and events. Just as the following:
protected override IEnumerable<ScriptDescriptor>
GetScriptDescriptors(System.Web.UI.Control targetControl)
{
ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor("jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender", targetControl.ClientID);
descriptor.AddProperty("ClientStateFieldID", this.ClientStateFieldID);
descriptor.AddProperty("CollapseControlID", this.CollapseControlID);
descriptor.AddProperty("ExpandControlID", this.ExpandControlID);
descriptor.AddProperty("ImageControlID", this.ImageControlID);
descriptor.AddProperty("ExpandedImage", Page.ResolveClientUrl(this.ExpandedImage));
descriptor.AddProperty("CollapsedImage", Page.ResolveClientUrl(this.CollapsedImage));
descriptor.AddProperty("Collapsed", this.Collapsed);
descriptor.AddProperty("SuppressPostBack", this.SuppressPostBack);
if (!String.IsNullOrEmpty(this.OnExpand))
{
descriptor.AddEvent("expanding", this.OnExpand);
}
if (!String.IsNullOrEmpty(this.OnExpanded))
{
descriptor.AddEvent("expanded", this.OnExpanded);
}
if (!String.IsNullOrEmpty(this.OnExpandComplete))
{
descriptor.AddEvent("expandComplete", this.OnExpandComplete);
}
if (!String.IsNullOrEmpty(this.OnCollapse))
{
descriptor.AddEvent("collapsing", this.OnCollapse);
}
if (!String.IsNullOrEmpty(this.OnCollapsed))
{
descriptor.AddEvent("collapsed", this.OnCollapsed);
}
if (!String.IsNullOrEmpty(this.OnCollapseComplete))
{
descriptor.AddEvent("collapseComplete", this.OnCollapseComplete);
}
yield return descriptor;
}
While
GetScriptReferences
Registers the script libraries for the control. In this case the
libraries would be the Client Library for this control I built in
part 1 and the
jQuery Library. The Client Control Library is an embedded resource while the
jQuery Library is defined using jQueryScriptPath property:
protected override IEnumerable<ScriptReference>GetScriptReferences()
{
string jqueryPath = jquery126Live;
if (this.jQueryScriptPath != null)
{
jqueryPath = Page.ResolveClientUrl(this.jQueryScriptPath);
}
ScriptReference[] scripts =
{ new ScriptReference(jqueryPath),
new ScriptReference("jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.js",
this.GetType().Assembly.FullName) };
return scripts;
}
This was all about the the Extender control. Now I'll move on how to use it.
Live Demo:
Below is the ASPX markup for the control usage. As you'll notice the usage is exactly the same as the original CollapsiblePanel:
- <cc1:jQueryCollapsiblePanelExtender ID="collapsiblePanelEx"
- TargetControlID="childPanel"
- CollapseControlID="parentLink"
- ExpandControlID="parentPanel"
- ImageControlID="imgExpandCollapse"
- CollapsedImage="~/images/plus.png"
- ExpandedImage="~/images/minus.png"
- Collapsed="false"
- OnCollapsed="collapsed"
- OnExpanded="expanded"
- runat="server"/>
<cc1:jQueryCollapsiblePanelExtender ID="collapsiblePanelEx"
TargetControlID="childPanel"
CollapseControlID="parentLink"
ExpandControlID="parentPanel"
ImageControlID="imgExpandCollapse"
CollapsedImage="~/images/plus.png"
ExpandedImage="~/images/minus.png"
Collapsed="false"
OnCollapsed="collapsed"
OnExpanded="expanded"
runat="server"/>
The demo I am going to present here is an old demo. But this time I'm using jQueryCollapsiblePanelExtender instead of the Original CollapsiblePanelExtender. Note that I am handling OnExpand/expanding client event to initiate AJAX request using PageMethod call, just like as I did in the old demo. Download the sample project to review the code and explore the control's code.