Contents
Introduction
When you open up Visual Studio 2008 to create a project, you will notice that it has two new web templates designed specifically for building AJAX controls: ASP.NET AJAX Server Control and ASP.NET AJAX Server Control Extender. You'll also find an old friend, the ASP.NET Server Control project template.
What are the differences between the Server Control, the ASP.NET AJAX Server Control, and the ASP.NET AJAX Extender, and when should each be used?
At first glance, it would seem that the ASP.NET Server Control differs from the other two controls in that it doesn't support AJAX. This isn't completely true, however, and in the first part of this tutorial, I will demonstrate just how far you can go in developing an AJAX-enabled control based on the Server Control alone. While the ASP.NET Server Control does not provide direct access to AJAX scripts, it can implement AJAX scripts encapsulated in other controls such as the UpdatePanel
, or the AJAX Extensions Timer
Control, to provide AJAX functionality. For control developers who are not all that keen on delving into the intricacies and pitfalls of JavaScript, the Server Control offers an excellent and clean development path.
The AJAX Server Control and the AJAX Server Control Extender differ from the regular ASP.NET Server Control by coupling themselves with JavaScript files, and allowing mapping between properties of a control class and properties of a JavaScript class. When you need functionality not provided by other AJAX Server controls, or simply want to customize your control using client-side script in order to avoid the ASP.NET control life-cycle, then this is the best option.
Finally, while the AJAX Server Control Extender is primarily used to add behavior (that is, JavaScript) to other controls on your ASP.NET page, the AJAX Server Control is a self-contained control in which any client-side script you write will apply, for the most part, only to the control itself, or to its children. In other words, an AJAX Extender will be aware of other controls on your page, while an AJAX Server Control will not.
Of some interest is the fact that the ASP.NET AJAX Server Control template, like the ASP.NET Server Control template, implements a ScriptControl
class that derives from System.Web.UI.WebControls.WebControl
, while the ASP.NET AJAX Server Control Extender template implements an ExtenderControl
class that derives directly from System.Web.UI.Control
. This means that using the first two kinds of templates, your control will include some built-in properties like Enabled
, Height
, and Width
, while this is not true of the Extender Control. For all practical purposes, however, this is not a significant difference. For a somewhat fuller treatment of the distinction between the WebControl
and Control
classes, please see Dino Esposito's article on the topic at MSDN.
A good way to look at the three types of controls we are discussing, then, is in terms of strategies which incrementally add developer features to your custom control, while preserving the features of the earlier controls. If you are only interested in adding AJAX functionality through child controls, then the ASP.NET Server Control is your best option. If you need to include some custom client-script to your control, then you should use the ASP.NET AJAX Server Control. If you additionally need to make your custom control aware of another control on your page, in order to interact with it, then the ASP.NET AJAX Server Control Extender should be used.
Of course, given that an Extender Control can do everything the other two control types can do, you always have the option of using Extenders for all your AJAX control development -- and many people do. However, if you are the type of developer who likes to use only the right tools for the right situation, then it behooves you to put some consideration into which base class is most appropriate for your needs.
In that vein, this tutorial will lead you through the construction of three different controls, based on the ASP.NET Server Control, the ASP.NET AJAX Server Control, and the ASP.NET AJAX Server Control Extender, respectively. Each subsequent control will include and extend the functionality of the previous control.
This tutorial will also attempt to construct something generally useful, a session timeout watcher. Most strategies for handling session timeout involve reactive solutions which check the state of the session upon a user event, and then perform some task, such as a page redirect, if the session has expired. Passive solutions are common because of what I like to think of as a version of Heisenberg's Uncertainty Principle as applied to the web. There is no way to look at the session object surreptitiously, to see if it still exists, without extending its lifetime. And so, we wait for the user to do something, and then either redirect, if the session has expired, or do nothing. The idea here is that since the user is extending the session lifespan anyways, if it has not already expired, we can piggy-back on this event to do our own prodding of the session object.
The problem with a passive solution is that it can be somewhat startling for the user of your website to be taken to a new page when they are trying to complete whatever they were in the middle of when the stepped away for a cup of coffee. A kinder, gentler way to handle session expiration would be to anticipate when the session is about to timeout, and then take some action on the user's behalf. In this case, the user will return to a session expired page, and (hopefully) know exactly what just happened to him.
Given the mild complexity of this sort of control, I will also have the opportunity to illustrate various useful techniques and gotchas involved in building an AJAX-enabled control without excessive contrivance on my part. The intent of this tutorial is not only to provide you with the basics of how to develop an ASP.NET AJAX control, but also to provide you with helpful pointers on building your own complex solutions. I ask for your forbearance in the event that, in my attempts to accomplish one of these goals, I undermine the other, making this tutorial either too easy or too opaque, and not always finding the happy medium between the two.
Note: This tutorial and all source code is built on the RTM version of Visual Studio 2008, rather than the VS 2008 beta. I had trouble opening up my beta projects using the RTM version, and would imagine that the reverse is also true.
I. The ASP.NET Server Control
In writing a proactive session timeout watcher control, it is necessary to anticipate what consumers of the control might like to do in the event of a session expiration. One possibility is that the user will want to automatically redirect to another web page, either a friendly page explaining what has just happened, or perhaps to a login page. Additionally, the consumer of this custom control may simply want to display a popup that does not require a redirect, but rather leaves the user on their current page. A third option is that the consumer wants the session to be extended, so that the session never dies as long as a web page is open. Fourth, the developer who consumes our session timeout control may want to handle the session timeout herself.
Our provisional list of features includes:
- Page Redirect
- Popup
- Extend Time
- Custom Event Handler
In addition, the session timeout watcher will need:
- to know how long the session is set to last, as well as
- be aware of every time the session timeout is re-extended because of a page postback. Additionally, it will need to,
- be able to respond to the session expiration in an AJAX manner -- that is, without unnecessarily causing a full page postback.
We will accomplish this by consuming the UpdatePanel
and Timer
controls, which now come with the 3.5 Framework and were previously included as part of AJAX Extensions, in our own custom control.
Begin by creating a new ASP.NET Server Control project called SessionTimeoutTool. This will generate both a project and a solution for us. Add a second ASP.NET Web project to the solution called TestTimeoutTool. Open the web.config file for TestTimeoutTool and add a sessionState
element in order to set the session timeout period. For the purposes of testing this control, it is advisable to set this attribute to something small. Two minutes works for me.
<system.web>
<sessionState timeout="2" mode="InProc"/>
</system.web>
This establishes the control development environment.
Rename the default class in the SessionTimeoutTool project by right clicking on the default class in the Solution Explorer and renaming the file from ServerControl1.cs to TimeoutWatcherControl. The IDE will take care of renaming your class for you. The TimeoutWatchControl
class comes with the Text
property and the RenderContents
already implemented for you by the IDE. You may delete these. You may also safely remove the ToolboxData
and DefaultProperty
attributes that decorate your class declaration. This will leave us with a rather Spartan class.
namespace SessionTimeoutTool
{
public class TimeoutWatcherControl : WebControl
{
}
}
Having gotten through the preliminaries, we can now start to build our control. We need to create an enum to keep track of the various timeout options our control will support. We will also expose the enum as a property that can be configured in ASP.NET markup.
private mode _timeoutMode = mode.CustomHandler;
public enum mode
{
PageRedirect,
PopupMessage,
ExtendTime,
CustomHandler
}
public mode TimeoutMode
{
get { return _timeoutMode; }
set { _timeoutMode = value; }
}
We need to add public properties for a path to the redirect page, if that is the mode the consumer wants to use, a popup message, as well as a a popup CSSClass
, and we need to have an event we can throw in case the consumer wants to handle the timeout herself. We also require private fields for the timeout interval as well as two child controls we need in order to implement our custom control, and two read only variables that will be used to convert between milliseconds (used by the Timer
control) and seconds (the unit of measure for the session timeout).
The UpdatePanel
and Timer
controls will be leveraged to enable AJAX functionality in our custom control without any actual client-scripting on our part. Instead, the Timer
control will take care of implementing the window.setInterval
method for us, which creates a JavaScript counter, while the UpdatePanel
, by registering itself with the ScriptManager
, will provide us with a placeholder in the DOM that we can update as needed. And this will be the last time I talk about client-scripting in this section, since the point of building a custom control in this way, using AJAX components that encapsulate the client-script for us, is that we do not have to worry about how those components do what they do.
private string _redirectPage;
private string _message;
private string _popupCSSClass = string.Empty;
public event EventHandler Timeout;
private int _interval = 1000;
private readonly int MINUTES = 60000;
private readonly int SECONDS = 1000;
protected System.Web.UI.Timer _sessionTimer = null;
private UpdatePanel _timeoutPanel = null;
public string RedirectPage
{
get { return _redirectPage; }
set { _redirectPage = value; }
}
public string TimeoutMessage
{
get { return _message; }
set { _message = value; }
}
public string PopupCSSClass
{
get { return _popupCSSClass; }
set { _popupCSSClass = value; }
}
You will also need to add a reference to the System.Web.Extensions
assembly in the Server Control project, since it contains the UpdatePanel
and Timer
controls we will use.
Now, it is time to implement the onLoad
handler for the control and, in doing so, fulfill the three requirements we outlined above regarding what a proactive session timeout watcher needs to be able to do. First, we said that the timeout watcher must know what the session lifespan is set to. We can retrieve this information programmatically from the Session
object itself by pulling the Session
object out of the HttpContext
class. If the session timeout period is set through the web.config file, then this is the value that will be returned, in minutes. If it is not set, then the default value of 20 minutes will be returned. The System.Web.UI.Timer
class reads time in milliseconds, however, so the Session.Timeout
value needs to be converted from seconds to milliseconds using our readonly MINUTES
variable.
The second requirement, that our control be aware of any resets of the session timeout, is met by putting our code in the OnLoad
method. The OnLoad
method will be called whenever there is a full or partial postback of the page that hosts the TimoutWatcherControl
. Normally, the ASP.NET lifecycle will actually call the OnLoad
handlers for each child control before it calls the OnLoad
handler of the host page itself.
In consuming the TimeoutWatcherControl
, our ideal user will most likely want to place the control on a MasterPage
rather trying to place a new instance of the control on each individual page. In this case, just because it is good to know, the normal lifecycle appears to call OnLoad
handlers in the following order:
- controls on the Master Page,
- controls in the Content Page,
- the Master Page, and
- the Content Page.
Finally, we said we want the TimeoutWatcherControl
to handle timeouts and postbacks in an economical manner. This is accomplished by adding an UpdatePanel
to our control, and a Timer
control to that UpdatePanel
. (For now, we will put in stub methods for the construction code for these two components.)
By design, when an AJAX Extensions Timer
control is placed inside an UpdatePanel
, it will automatically know to update that panel when it runs out of time. This works out well for us, since we want to be able change the content of the UpdatePanel
when the timer determines that our session has expired.
In nesting our controls, you will notice that we do not use the UpdatePanel
's Controls
property. This is because the UpdatePanel
content actually goes into a template object called the ContentTemplateContainer
rather than the Controls
property itself and, in fact, trying to add to the Controls
property will generate an exception. Here is our code, so far:
protected override void OnLoad(EventArgs e)
{
System.Web.SessionState.HttpSessionState state
= HttpContext.Current.Session;
_interval = state.Timeout * MINUTES;
UpdatePanel timeoutPanel = GetTimeoutPanel();
System.Web.UI.Timer sessionTimer = GetSessionTimer();
sessionTimer.Interval = _interval;
timeoutPanel.ContentTemplateContainer.Controls.Add(
sessionTimer);
this.Controls.Add(timeoutPanel);
}
You can automatically create methods from our two method placehoders by right clicking on the method calls and selecting "Generate Method Stub". (While this is not actually a new feature, I must admit, with some embarrassment, that I never noticed it before VS 2008.)
The code should look pretty straightforward, so it is worth mentioning that there is something very strange going on here. First, we are recreating the _sessionTimer
object and placing it in our UpdatePanel
on every postback, whether it is a full postback or a partial one. Why doesn't this create an error as we try to load multiple Timer
objects into our page?
A partial explanation involves the fact that it is being loaded into an UpdatePanel
that has its UpdateMode
set to "Always
". Because the UpdatePanel
is set to update itself "Always
" rather than conditionally, all the data inside the content template of the panel will be cleared out on every partial and full page postback. This works out well for us, since we want to turn off the timing mechanism on each postback (which happens to also reset the session timeout), and here we accomplish that by disposing of the previous timer and creating a new one which starts its countdown all over again using the full length of the session timeout lifespan. With each postback, then, we fortuitously manage to reset our own internal clock, and wait afresh for the session to expire.
But, here's a second mystery. Why doesn't recreating the UpdatePanel
on each postback cause problems, since the UpdatePanel
container falls outside of its content template? The answer here is a bit strange, and requires us to think a bit differently about state in web applications. Prior to ASP.NET AJAX, it was normal to think of the HTML content of an ASP.NET page as the most ephemeral layer of the ASP stack. It could always be changed through various client-scripting techniques, while the underlying code-behind class remained unchanged. In turn, the code-behind class could be destroyed and recreated on postbacks, but behind that, the Session
object would always remain steady and well-grounded. Like a Neo-platonic theory of software lifecycle, it was normal to think of permanence in a web page as emanating from the server, and gradually being dissipated by code-behind and finally actual HTML markup.
With ASP.NET AJAX, however, this model no longer holds. Partial page postbacks can force us to iterate through the OnLoad and other methods of the code-behind page without making a single change to the physical web page. In the new model, both the page, presented in the browser, and the session preserve state, while our code-behind is the least stable element of the stack.
While markup is rendered as a div
tag for the UpdatePanel
and registered with the ScriptManager component on the first full postback of the page, on each subsequent partial postback, we are required to create a new code object that will correspond to the markup for our panel, which hasn't actually changed at all. As an experiment, you can try setting a different ID for the UpdatePanel
on each partial postback. You'll notice that the code throws an exception if the ID of the UpdatePanel
we create does not correspond to the ID of the UpdatePanel
we originally instantiated. My understanding of what is going on behind the scenes is admittedly foggy at best, but such novelties in working with the ASP.NET AJAX lifecycle certainly throws a new twist into web development.
Here is the code we have just been discussing. While I have thrown in a null check for the UpdatePanel
, which is simply good programming practice, the _updatePanel
variable in this code fragment actually always returns a null value. Note also that we have added an event handler for the Timer
's Tick
event, which is fired when the timer finally finishes its countdown:
protected void SessionTimer_Tick(object sender, EventArgs e)
{
}
private System.Web.UI.Timer GetSessionTimer()
{
if (null == _sessionTimer)
{
_sessionTimer = new System.Web.UI.Timer();
_sessionTimer.Tick +=
new EventHandler<EventArgs>(SessionTimer_Tick);
_sessionTimer.Enabled = true;
_sessionTimer.ID = this.ID + "SessionTimeoutTimer";
}
return _sessionTimer;
}
private UpdatePanel GetTimeoutPanel()
{
if (null == _timeoutPanel)
{
_timeoutPanel = new UpdatePanel();
_timeoutPanel.ID = this.ID + "SessionTimeoutPanel";
_timeoutPanel.UpdateMode =
UpdatePanelUpdateMode.Always;
}
return _timeoutPanel;
}
To finish up this control, we just need to handle the different cases for what should happen when we think the session has expired. Here, to simplify the code, I will once again insert some place holder calls:
protected void SessionTimer_Tick(object sender, EventArgs e)
{
switch (TimeoutMode)
{
case mode.ExtendTime:
break;
case mode.PageRedirect:
DisableTimer();
Redirect(RedirectPage);
break;
case mode.PopupMessage:
DisableTimer();
BuildPopup();
break;
case mode.CustomHandler:
default:
DisableTimer();
OnTimeout();
break;
}
}
Handling the ExtendTime
mode is probably the easiest solution to implement. Since our Timer
, nested inside the UpdatePanel
, automatically invokes a partial postback, which in turn automatically extends the session timeout, we just have to make sure that this occurs before the session actually expires. We can accomplish this back in the OnLoad
handler by setting our internal timer to expire at some specified time before the session expires -- let's say 45 seconds. We'll modify our OnLoad
handler to look like this:
System.Web.UI.Timer sessionTimer = GetSessionTimer();
if (TimeoutMode == mode.ExtendTime)
sessionTimer.Interval = _interval - (45 * SECONDS);
else
sessionTimer.Interval = _interval;
The Redirect
method is also pretty easy to implement. We will simply pass the RedirectPage
property to our method and then use the Response
object to redirect to the specified page. To make things a little more interesting, though, we will also use the System.Web.VirtualPathUtility
to parse the redirectPage
parameter. This allows our custom control to support the tilde ("~") at the beginning of a URL string, and lets our control users apply the tilde to specify an app relative path.
private void Redirect(string redirectPage)
{
if (!string.IsNullOrEmpty(redirectPage))
{
Context.Response.Redirect(
VirtualPathUtility.ToAbsolute(
redirectPage));
}
}
For the popup message, we want to create a floating DIV
and inject it into our UpdatePanel
content template. We also want to find a way to disable our control, since we do not want multiple popup controls to appear if the end-user is away for a long time. This is a bit involved, since we need to be able to access the current session timer in order to disable it, as well as save the fact that we have disabled the timer between postbacks, so it doesn't just turn itself on the next postback. Calling on the principle that we discussed above, that is that the page markup is actually more permanent than the code-behind page, it turns out that we can actually preserve the timer's enabled state in the page viewstate object. This ensures that if we disable the timer, it will stay disabled when the page undergoes either a partial or a full page postback.
We probably also want the timer to re-enable itself on a non-postback page initialization. Fortunately, the viewstate object comes back as a new object on non-postbacks, and since we have set the TimerEnabled
property to default to true
, a non-postback page view always creates an enabled timer control. This also happens to work when TimoutWatcherControl
is hosted in a MasterPage
, rather than a regular web form.
public bool TimerEnabled
{
get
{
object timedOut = ViewState[this.ID + "TimedOutFlag"];
if (null == timedOut)
return true;
else
return Convert.ToBoolean(timedOut);
}
set
{
GetSessionTimer().Enabled = value;
ViewState[this.ID + "$TimedOutFlag"] = value;
}
}
private void DisableTimer()
{
this.TimerEnabled = false;
}
To fully implement the DisableTimer()
method, a final change needs to be made to our GetSessionTimer()
, which was originally written to set the internal timer's Enabled
property to true
. Instead, we will now pull this value from the viewstate.
_sessionTimer.Enabled = this.TimerEnabled;
Now, we just need to retrieve the current UpdatePanel
and add a floating DIV
to it. We accomplish this by building a simple Panel
control that is set to position: absolute and has a z-index. This Panel
contains both the popup message set in the TimoutWatcherControl
's markup, as well as a button to start the timer again. We start the timer again by hooking up an event handler to the floating DIV
's OK button.
void but_Click(object sender, EventArgs e)
{
this.TimerEnabled = true;
}
private void BuildPopup()
{
UpdatePanel p = GetTimeoutPanel();
Panel popup = new Panel();
AddCSSStylesToPopupPanel(popup);
popup.Height = 50;
popup.Width = 125;
popup.Style.Add("position", "absolute");
popup.Style.Add("z-index", "999");
AddMessageToPopupPanel(popup, TimeoutMessage);
EventHandler handlePopupButton = new EventHandler(but_Click);
AddOKButtonToPopupPanel(popup, handlePopupButton);
p.ContentTemplateContainer.Controls.Add(popup);
}
Finally, in order to throw a timeout event to the control's host page, we implement the OnTimeout()
method.
public event EventHandler Timeout;
protected void OnTimeout()
{
if (Timeout != null)
Timeout(this, EventArgs.Empty);
}
You can now build this solution to make the TimoutWatcherControl
available in your toolbox.
In order to test the control, add a new WebForm to the TestTimeoutTool project. Drag into it an AJAX Extensions ScriptManager
, an UpdatePanel
, and a Button
. Finally, drag the TimeoutWatcherControl
onto the form. Your markup should look something like this when you are done:
<div>
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
This page first loaded at <%= DateTime.Now.ToLongTimeString() %>.
<asp:UpdatePanel ID="UpdatePanel1"
runat="server" UpdateMode="Conditional">
<ContentTemplate>
This panel refreshed at <%= DateTime.Now.ToLongTimeString() %>.
<br /><asp:Button Text="Refresh Panel" ID="Button1"
runat="server"/>
</ContentTemplate>
</asp:UpdatePanel>
</div>
The point of this test is to make sure that when a partial update occurs in the UpdatePanel
, our TimeoutWatcher
re-extends its internal timeout just as the session extends its timeout period. We can test out the PageRedirect
option by creating a new WebForm called SessionExpired.aspx and hard-coding it as a property of the TimeoutWatcherConrol
in the markup.
And, mutatis mutandis, after about two minutes from the time in the UpdatePanel
, you should be redirected to the page specified in the RedirectPage
parameter.
There's only one potential problem with our custom control. Since the AJAX Extensions Timer
causes a postback when its counter runs down, we are, in effect, creating a new session object in order to notify the user that the old session object has expired. In some sense, we have simply re-invoked the Uncertainty Principle mentioned above. A cleaner solution would implement our various timeout events without using postbacks at all. In order to arrive at this cleaner solution, however, we will need to build an ASP.NET AJAX Server Control.
II. The ASP.NET AJAX Server Control
What do you get when you create a new AJAX Server Control project?
For the most part, it looks very similar to the ASP.NET Server Control, though it doesn't come with a default implementation of the Text
property. Instead, you will find two new methods associated with AJAX behavior: GetScriptDescriptors()
and GetScriptReferences()
. I will discuss these at some length in a bit. A new AJAX Server Control project also comes with an automatic reference to the System.Web.Extensions
assembly, as well as a JScript file and a resource file, both named TimeoutWatcherBehavior. You, typically, will want to rename these files, or just get rid of them and create your own. Finally, and not so obviously, your AssemblyInfo file will contain special instructions to make your script file available as a resource; these instructions need to be modified if you rename your script.
For this section of the tutorial, I need you to create a new ASP.NET AJAX Server Control project. Since we want to extend the current implementation, rather than replace it, you will want to save all the code you have written so far (everything between the class declaration and the final close bracket the class) over to the toolbox. You may also want to save the using
-namespace directives from the TimeoutWatcherControl
.
For simplicity, I'm going to use the same project name, SessionTimeoutTool, for this new AJAX Server Control, which requires me to remove the current project with that name and move it to a new location, before creating the new one. If you want to simply use a different project name, this is fine, be sure to remain cognizant of the minor naming discrepancies that will result from this between your code and the code I will be describing in this section of the tutorial.
The first thing we want to do in our new control project is rename all the default files: ServerControl1.cs, TimeoutWatcherBehavior.js, and TimeoutWatcherBehavior.resx. These files are named this way by default, no matter the name of your project.
Let's rename ServerControl1.cs to TimeoutWatcherAjaxControl.cs. VS generously takes care of renaming our class declaration and constructor for us. Let's also rename the JScript file as TimoutWatcherBehavior.js. Sadly, VS is a bit more miserly here, and we will have to open the JScript file and rename our methods and initializers a bit more manually. Do an Edit | Find and Replace | Quick Replace to switch all instances of "ClientControl1" with "TimeoutWatcherBehavior". You should do this for the entire project, rather than just this file, in order to make sure the cs file also gets updated with the correct JScript file reference.
Here is what the JScript file should look like after the changes, if you have built it with the SessionTimeoutTool project name. If not, it should use the name of your specific project as a namespace.
Type.registerNamespace("SessionTimeoutTool");
SessionTimeoutTool.TimeoutWatcherBehavior = function(element) {
SessionTimeoutTool.TimeoutWatcherBehavior.initializeBase(this, [element]);
}
SessionTimeoutTool.TimeoutWatcherBehavior.prototype = {
initialize: function() {
AjaxServerControl1.TimeoutWatcherBehavior.callBaseMethod(this, 'initialize');
},
dispose: function() {
SessionTimeoutTool.TimeoutWatcherBehavior.callBaseMethod(this, 'dispose');
}
}
SessionTimeoutTool.TimeoutWatcherBehavior.registerClass(
'SessionTimeoutTool.TimeoutWatcherBehavior', Sys.UI.Control);
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
There are four interesting sections in this JScript file. The first is the call to Type.Namespace
, a new language feature included in the ASP.NET AJAX Library to avoid name collisions and make JavaScript a bit more mature. In an AJAX control, you typically want to use your project name as your namespace.
Skipping over the body of the code (we will come back to this), there is a registerClass
method. This is a method implemented by the Microsoft AJAX Library (sometimes called the AJAX Framework) which establishes your JavaScript as a class. Even more interesting than this, the registerClass
parameters after the class name allow you to specify any other AJAX classes your custom class inherits. We will not be exploring this language feature in this tutorial, but it is worth noting because it demonstrates the lengths Microsoft has gone to in order to make JavaScript behave like an object-oriented language.
The last line, which begins "if (typeof(Sys) !== 'undefined')
", needs to be present at the bottom of every script file. It basically just informs the ScriptManager
that the file has finished being processed. It is a kludge for Safari browsers, which do not have a native way to indicate that a client-script file has completed loading.
Going back to the body of the code, the final thing worthy of note is how your JScript class is split between a main function and a prototype function. This is the prototype convention promoted by Microsoft for building AJAX behaviors. It is a bit of a mix of native JavaScript functionality, repurposed to make JavaScript behave in a more object-oriented manner, and the Microsoft AJAX Library.
Here are some cursory pointers on this programming convention:
- First, splitting your code between a main body and a prototype basically gives you the flexibility of the partial class model employed in C# programming. The prototype property can also be thought of as a way to extend your base code. You typically want to put your initialization code and any local variables in your main body. Any methods or properties of your object should go into the prototype.
initializeBase
and callBaseMethod
are language enhancements of the MS AJAX Library, as are the initialize
and dispose
methods. Your base class, in this case, happens to be Sys.UI.Control
. - Class level variables are, by convention, preceded by an underscore and then a Camel-cased variable name. Keep in mind, though, that this is only a convention. There are no secrets in JavaScript, and even your class level variables are accessible by external code.
- Property constructors are preceded by "get_" and "set_".
- All methods, including your class declaration, follow the paradigm function name, colon, function declaration (e.g.,
myFunction: function(){}
). - JavaScript methods can generally be thought of as static methods. In order to support the notion of class instances, there needs to be a way to indicate that you are using a field or property or method of an instantiated object, rather than static accessors. The MS AJAX Library provides the
this
keyword for this purpose. In your custom code, you want to use this early, and you want to use it often. - The MS AJAX Library supports delegates and event handlers. We will discuss how to implement these later in the tutorial, but it is important to be aware that they are part of your arsenal as you develop AJAX classes.
- Debugging: unlike in C# or VB, where you can debug simply by setting a breakpoint in your source, debugging JavaScript is a bit more involved. This is because client-script, in .NET, has to be processed in order to generate a result script. This result script, however, is not available until runtime. In order to work around this, you will need to place a call to the JavaScript debug object, like this:
Sys.Debug.fail("")
, somewhere early in your instantiation code in order to force a breakpoint. This will force the IDE to break in the result code as it is being processed. At that point, when you have access to the result script, you will be able to start setting breakpoints in your script, just as with any other language in Visual Studio.
Once the namespace, class name, and filename for the script file have been determined, you must also change the resource file name to match the script file: in this case, it should be renamed to TimeoutWatcherBehavior.resx. The way we are using it, the resource file basically serves as a place holder, letting the assembly know that there is a resource by that name. That name can now be used in order to set our script file up as a web resource.
Go into the properties of the .js file, and set its Build Action property to Embedded Resource.
Now, go into the project Properties folder, and open AssemblyInfo.cs for editing. It is here that we will set up metadata to make the JScript file available as a resource, and then as a web resource. As a web resource, it will be automatically referenced from a dynamic location through the ScriptResource.axd, rather than through a file location.
If you have had a chance to examine the AjaxControlToolkit, a separate assembly of custom AJAX controls and extenders, you will notice that it handles web resources by applying custom attributes to each custom control class, specifying the script resource that are coupled with the class. This is possible because the Toolkit has implemented internal code that uses reflection to automatically hook up scripts as web resources. It's all rather cool.
However, we will be writing this control without reference to third-party tools such as the Toolkit base classes. Instead, we will try to only use what Visual Studio 2008 provides.
In the AssemblyInfo file, you should find two assembly
attributes referencing your JScript file. If they aren't there, you may need to add them. For the TimeoutWatcherAjaxControl
, they should look something like this:
[assembly: System.Web.UI.WebResource("SessionTimeoutTool.TimeoutWatcherBehavior.js",
"text/javascript")]
[assembly: System.Web.UI.ScriptResource("SessionTimeoutTool.TimeoutWatcherBehavior.js",
"SessionTimeoutTool.TimeoutWatcherBehavior", "SessionTimeoutTool.Resource")]
What we really want is to turn our script into a script resource, but in order to do this, we also have to mark it as a web resource in the assembly. Declaring it as a web resource only requires using the WebResourceAttribute
declaration, as above, and referencing the file name, along with the file type ("text/javascript").
We use the ScriptResourceAttribute
to register the file as a script resource that can be accessed through the ScriptResource.axd handler. The first parameter is the script file name, which should be the same as in the WebResource declaration. The second parameter is the name of the resource file, which, as I indicated above, is a placeholder. The third parameter is a type -- in this case, the type is a resource in our project namespace.
Each parameter should include the namespace of the control project. Since we have placed our embedded resource in the project root, the namespace and filename are enough to identify it. If you want to place the script file in a subfolder, then you will need to specify it in the AssemblyInfo file by namespace, subfolder[s], and filename. For instance, if we had placed the TimeoutWatcherBehavior.js and resx files under a subfolder called common, our metadata entry would look like this:
[assembly: System.Web.UI.WebResource("SessionTimeoutTool.Common.TimeoutWatcherBehavior.js",
"text/javascript")]
[assembly: System.Web.UI.ScriptResource("SessionTimeoutTool.Common.TimeoutWatcherBehavior.js",
"SessionTimeoutTool.Common.TimeoutWatcherBehavior", "SessionTimeoutTool.Resource")]
Even though we have added the necessary metadata to identify our script file as a Script Resource, we still need to make sure it gets instantiated. This is done back in the GetScriptReferences()
method (one of the two methods we inherit from the ScriptControl
base class) of our custom control.
To implement the GetScriptReferences()
method, all we need to do is add the following line of code:
yield return new ScriptReference("SessionTimeoutTool.TimeoutWatcherBehavior.js",
this.GetType().Assembly.FullName);
Again, if the resource is in a subfolder, then the subfolder name will need to be included when you specify the resource name. Behind the scenes, this yield
statement ensures that, at some point, a script reference like:
<script src="/ScriptResource.axd?d=8O8TXUV..." type="text/javascript"></script>
will be inserted into our web page, making our JavaScript file available to our code, though in a somewhat obfuscated (and arguably more secure) manner. This is actually all that this method is used for.
The other method of the ScriptControl
base class which needs to be overridden is GetScriptDescriptors()
. This method also generates code in our resultant web page. It basically generates a call to the MS AJAX Library specific $create()
method, for instance:
$create(SessionTimeoutTool.TimeoutWatcherBehavior,
{"interval":120000,"message":"Timed out"},
null, null, $get("TimeoutWatcherControl1"));
which instantiates our JavaScript behavior class. This method is a bit more complicated, because it can be used to modify the generated $create()
method by adding additional properties (such as the "interval
" property in the sample above) and even give them an initial value. This simplest implementation in C# would look like this:
ScriptControlDescriptor descriptor =
new ScriptControlDescriptor("SessionTimeoutTool.TimeoutWatcherBehavior",
this.ClientID);
yield return descriptor;
The final step in coupling our custom control with our JavaScript behavior class is to set some property values in the generated $create()
method. We already know that we need to pass the interval of the session timeout to our client script, which has no other way of ascertaining this. We also will want to pass a popup message text to the client script, as well as the functionality (e.g., PopupMessage
, PageRedirect
) the consumer requests. All these are already available in the code we wrote previously. Finally, we want a way to indicate whether client-script will be used, or the server-side code we have already written will be used.
Drag all of our previous code, saved to the toolbar, into the TimeoutWatcherAjaxControl
class. Fortunately, all of this code will work in a class derived from ScriptControl
just as well as it does in one derived from WebControl
.
We will now allow the user to determine whether they want to use server-side code, which starts a new empty session when the current session expires, or pure client-side code, which does not. This is accomplished with a class level variable, a new enum, and a public property:
public enum ScriptMode
{
ServerSide,
ClientSide
}
private ScriptMode _scriptMode = ScriptMode.ServerSide;
public ScriptMode RunMode
{
get { return _scriptMode; }
set { _scriptMode = value; }
}
We should modify our OnLoad
event so an UpdatePanel
is only created and added to the current control if the consumer chooses the server-side option.
if (RunMode == ScriptMode.ServerSide)
{
UpdatePanel timeoutPanel = GetTimeoutPanel();
...
this.Controls.Add(timeoutPanel);
}
In the GetScriptDescriptors()
itself, we can now add the following properties to our descriptor: interval
, timoutMode
, redirectPage
, and message
. These will let our AJAX class know the session lifespan, the way the consumer wants a timeout to be handled, the page to redirect to, and the message to be shown in a popup if a popup is requested.
protected override IEnumerable<ScriptDescriptor>
GetScriptDescriptors()
{
if (RunMode == ScriptMode.ClientSide)
{
ScriptControlDescriptor descriptor =
new ScriptControlDescriptor("SessionTimeoutTool.TimeoutWatcherBehavior",
this.ClientID);
descriptor.AddProperty("interval", _interval);
descriptor.AddProperty("timeoutMode", _timeoutMode);
descriptor.AddProperty("message", _message);
descriptor.AddProperty("redirectPage",
string.IsNullOrEmpty(_redirectPage) ? "" :
VirtualPathUtility.ToAbsolute(_redirectPage));
yield return descriptor;
}
}
AddProperty
basically allows us to pass server values to our client-script. Keep in mind that the code here is not aware in any way of the contents of our JavaScript code. Instead, the descriptor simply provides instructions on how to emit a $create
call into our web page, with our properties hard-coded into it. The emitted script is then called when all client-scripts have finished loading; it instantiates our AJAX object (by leveraging the MS AJAX Framework), and then initializes the client-side object's properties the way it has been instructed to in the descriptor
object.
If you try to run this code now, however, you should get an exception message of some sort, since we still have to script these properties in our TimeoutWatcherBehavior
class.
Creating properties in a JavaScript behavior class is similar to creating one in C#. The main difference is in where you place your code and the fact that you have to use the this
keyword everywhere. Your class level variables go in your main JavaScript class, while your property accessors go into the prototype function. Since the $create
call emitted by our custom control is looking for an "interval
" property, we need to script a get_interval
method and a set_interval
method. Notice that "interval" is lowercase here, just as it is in the property name we are trying to map. We will need to do the same for timoutMode
, redirectPage
, and message
. Note in the code sample below that, in the prototype definition, all methods are followed by a comma, except the last method in the prototype.
SessionTimeoutTool.TimeoutWatcherBehavior = function(element) {
SessionTimeoutTool.TimeoutWatcherBehavior.initializeBase(this, [element]);
this._interval = 1000;
this._message = null;
this._timeoutMode = null;
}
SessionTimeoutTool.TimeoutWatcherBehavior.prototype = {
initialize: function() {
SessionTimeoutTool.TimeoutWatcherBehavior.callBaseMethod(this
, 'initialize');
},
get_interval: function() {
return this._interval;
},
set_interval: function(value) {
this._interval = value;
}
...
}
While the new intellisense for JavaScript is generally very nice, a minor annoyance is that code in your prototype will not recognize variables in your main class. Thus, when you begin typing "this.", "_interval" is not one of the values that intellisense will suggest to you. It is a bit odd that this isn't supported in intellisense when, at the same time, this style of coding is also encouraged by Microsoft.
You may remember that timeoutMode
is being passed in as an enum. In order to make it intelligible in our client-side code, we need a way to translate this value into something familiar. We know that enums are really integers beneath the covers, so we could just try to keep in mind that a timoutMode
value of 0 is a page redirect, a value of 1 is a popup message, and so on.
For readability, however, we are better off scripting a client-side enum for this. Client-side enums are yet one more feature supported by the MS Atlas Library. The code for the client-side enumerator should be placed just before the "typeof(Sys) !== 'undefined'
" line. I'll place the enumerator from our custom control next to the JavaScript version so you can see the similarities:
public enum mode
{
PageRedirect,
PopupMessage,
ExtendTime,
CustomHandler
}
SessionTimeoutTool.Mode = function(){};
SessionTimeoutTool.Mode.prototype =
{
PageRedirect: 0,
PopupMessage: 1,
ExtendTime: 2,
CustomHandler: 3
}
SessionTimeoutTool.Mode.registerEnum("SessionTimeoutTool.Mode");
In order to track the session timeout in client-side code, we need to create an internal timer for our JavaScript class. To this end, add an additional instance variable to the main class, _timer
:
this._timer = null;
This will be used to handle our internal timer. We could try to handle the window.setInterval
call by ourselves in order to set a timer. However, in this case, we are going to use a timer class, which I believe I originally found on Bertrand LeRoy's blog, but which can also be found in the AjaxControlToolkit.
Adding a new script file to our project and making it available to the TimeoutWatcherBehavior
class only requires that we go through the same steps we did to make our behavior class into a ScriptResource.
- Set the script file's Build Action to "Embedded Resource".
- Add a resource file with the same name (i.e., Timer.resx).
- Tag the script file as both a WebResource and a ScriptResource in AssemblyInfo.cs.
- Add a
yield
statement for it in the GetScriptReferences()
method of your custom control class so that a ScriptResource.axd reference will be created for it.
Here is the Sys.Timer
code, with license, which effectively wraps the window.setInterval
method:
Sys.Timer = function() {
Sys.Timer.initializeBase(this);
this._interval = 1000;
this._enabled = false;
this._timer = null;
}
Sys.Timer.prototype = {
get_interval: function() {
return this._interval;
},
set_interval: function(value) {
if (this._interval !== value) {
this._interval = value;
this.raisePropertyChanged('interval');
if (!this.get_isUpdating() && (this._timer !== null)) {
this._stopTimer();
this._startTimer();
}
}
},
get_enabled: function() {
return this._enabled;
},
set_enabled: function(value) {
if (value !== this.get_enabled()) {
this._enabled = value;
this.raisePropertyChanged('enabled');
if (!this.get_isUpdating()) {
if (value) {
this._startTimer();
}
else {
this._stopTimer();
}
}
}
},
add_tick: function(handler) {
this.get_events().addHandler("tick", handler);
},
remove_tick: function(handler) {
this.get_events().removeHandler("tick", handler);
},
dispose: function() {
this.set_enabled(false);
this._stopTimer();
Sys.Timer.callBaseMethod(this, 'dispose');
},
updated: function() {
Sys.Timer.callBaseMethod(this, 'updated');
if (this._enabled) {
this._stopTimer();
this._startTimer();
}
},
_timerCallback: function() {
var handler = this.get_events().getHandler("tick");
if (handler) {
handler(this, Sys.EventArgs.Empty);
}
},
_startTimer: function() {
this._timer = window.setInterval(Function.createDelegate(this,
this._timerCallback), this._interval);
},
_stopTimer: function() {
window.clearInterval(this._timer);
this._timer = null;
}
}
Sys.Timer.descriptor = {
properties: [ {name: 'interval', type: Number},
{name: 'enabled', type: Boolean} ],
events: [ {name: 'tick'} ]
}
Sys.Timer.registerClass('Sys.Timer', Sys.Component);
if (typeof(Sys) !== 'undefined')
Sys.Application.notifyScriptLoaded();
Here is the metadata information in AssemblyInfo:
[assembly: WebResource("SessionTimeoutTool.Timer.js", "text/javascript")]
[assembly: ScriptResource("SessionTimeoutTool.Timer.js",
"SessionTimeoutTool.Timer", "SessionTimeoutTool.Resource")]
And, here is the modified GetScriptReferences()
implementation. Note that we add a new yield
statement for each Script Resource we want to make available.
protected override IEnumerable<ScriptReference>
GetScriptReferences()
{
if (RunMode == ScriptMode.ClientSide)
{
yield return new ScriptReference("SessionTimeoutTool"
+ ".TimeoutWatcherBehavior.js"
, this.GetType().Assembly.FullName);
yield return new ScriptReference("SessionTimeoutTool.Timer.js"
, this.GetType().Assembly.FullName);
}
}
The Sys.Timer
class can be instantiated by using the new
keyword. This should be done in the initialize
method of the prototype function. Next, we need to add a handler for the tick
event of the Sys.Timer
object. Though the syntax is a bit different, the AJAX Library delegates work much the same way they do in C#. Use the createDelegate()
of the AJAX Library Function
class to create a delegate which points to a method in the TimeoutWatcherBehavior
class, named tickHandler
. For now, tickHandler
will not be doing much, but we will fill it out in a bit. Pass this delegate to the add_tick
method of our Sys.Timer
object.
The next thing we want to do is to make sure our internal timer resets itself every time a page reloads. We were able to do this in C# by handling the OnLoad
event. We can do something similar here using an event exposed by the MS AJAX Library. The Library exposes a function called Sys.Application.add_load
which calls any delegates passed to it every time the web page does either a full or partial update. To take advantage of this, we just need to create a delegate for a class method, setTime
, that will reset the timer and pass this delegate to the add_load
function.
Finally, in our set_Time
method, we just need to turn the timer off, reset it with the _interval
value we received from our custom control, and start it running again.
initialize: function() {
SessionTimeoutTool.TimeoutWatcherBehavior.callBaseMethod(this
, 'initialize');
this._timer = new Sys.Timer();
tickHandlerDelegate = Function.createDelegate(this, this.tickHandler);
this._timer.add_tick(tickHandlerDelegate);
setTime = Function.createDelegate(this,this.setTimer);
Sys.Application.add_load(setTime);
},
tickHandler: function(){
},
setTimer:function()
{
if(this._timer)
{
this._timer.set_enabled(false);
this._timer.set_interval(this.get_interval());
this._timer.set_enabled(true);
}
},
...
dispose: function() {
SessionTimeoutTool.TimeoutWatcherBehavior.callBaseMethod(this
, 'dispose');
if (this._timer) {
this._timer.dispose();
this._timer = null;
}
$clearHandlers;
}
To complete this section of the code, we need to make sure that when our TimoutWatcherBehavior
class gets disposed, we also call the internal timer object's dispose
method. Additionally, we will call $clearHandlers
for safe measure. This is a generic method to disconnect all event handlers we may have hooked up inside our class.
All that remains for us to do is to handle each possible value of the timeoutMode
property in our tickHandler
method.
We'll start by creating a skeleton implementation for tickHandler
, with some stub methods.
tickHandler: function(){
if(this._timeoutMode == SessionTimeoutTool.Mode.PageRedirect)
{
this.pageRedirect();
return;
}
if(this._timeoutMode == SessionTimeoutTool.Mode.PopupMessage)
{
this.popup();
return;
}
if(this._timeoutMode == SessionTimeoutTool.Mode.ExtendTime)
{
this.extendTime();
return;
}
if(this._timeoutMode == SessionTimeoutTool.Mode.CustomHandler)
{
this.customHandler();
return;
}
},
Both the pageRedirect
and popup
methods are pretty easy to implement, especially since we will simply be using a JavaScript alert
to handle the popup request. Just be sure to disable the timer before the popup, otherwise we will end up getting multiple alert windows.
pageRedirect: function(){
window.location = this._redirectPage;
},
popup: function(){
this._timer._stopTimer();
if(this._message == null)
{
alert("The session has expired.");
}
else
{
alert(this._message);
}
},
The astute reader will remark that in this section of the tutorial, we have not actually used any real AJAX functionality, up to this point. "Real" AJAX involves using the XMLHttpRequest
object to interact with the server from JavaScript. Even following the broader definition of AJAX as implemented in the AJAXControlToolkit, that is, either communication with Server objects or manipulation of the page DOM, we have still failed to do anything particularly AJAXesque. So far, we have only encapsulated JavaScript in a Server Control and managed to pass some information obliquely from the server to our client-script.
In order to implement the extendTime
method, however, we will need to talk to the Server. The technique we employ next, moreover, should serve as a template for any real AJAX you might want to do in your own projects.
The basic solution is pretty simple. The MS AJAX Library supports calls to web services from JavaScript. So, all we need to do is create a web service that extends the session by writing a value to it.
In your control project, add a reference to the assembly System.Web.Services
. Next, we will want to add a web service to the project. Unfortunately, the template for a web service does not show up under an AJAX Server Control project, so you will need to create it for the test project, if you have it open, and drag it over to the control project. You can use the default name for this service (for reasons to be explained later). Make sure the service is decorated with the ScriptService
attribute, which allows it to be called from a client-script. Create a method to extend the session, called ExtendSessionTimeout
, and be sure to mark it as a service that requires access to the current session, by marking it with this tag: WebMethod(EnableSession = true)
. The web service should end up looking something like this:
using System.ComponentModel;
using System.Web.Services;
using System.Web.Services.Protocols;
namespace SessionTimeoutTool
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
[System.Web.Script.Services.ScriptService]
public class WebService1 : System.Web.Services.WebService
{
[WebMethod(EnableSession = true)]
public void ExtendSessionTimeout()
{
Session["extendTimeout"]=true;
}
}
}
The standard way to use ASP.NET AJAX to call a web service requires adding a reference to our service in the ScriptManager
. This is not very useful for us, since the user's ScriptManager
is in a different assembly, and we do not have access to it. Fortunately, ASP.NET AJAX also provides the Sys.Net.WebServiceProxy.invoke
method, which allows us to make web service calls without needing such a reference. The call to our service would look like this:
var webRequest = Sys.Net.WebServiceProxy.invoke("WebService1.asmx"
, "ExtendSessionTimeout", false, null
, null, null, "User Context");
But here's the rub. In .NET, web services are implemented using *.asmx pages that reference underlying web service classes. The AJAX function that calls the code is run in the assembly that implements our custom control. The WebService1.asmx that the AJAX function calls, however, does not exist in that assembly. If we try to run our code as it stands now, we will throw an exception since the file cannot be found. There is also no way to set up an asmx page as a resource file in order to expose it in the client assembly.
One solution would be to make the consumer of our control implement the WebService1.asmx in their own project. While this would work, it is rather un-cool. It would be preferable to have a self-contained AJAX Server Control that is able to find its own internal web service no matter where it is used.
HttpHandlers
offer a less invasive solution. Implementing an HttpHandlerFactory
requires the control consumer to make a small modification of his web.config file, but this is still preferable to forcing him to implement a whole class to our specifications. The purpose of an HttpHandlerFactory
is to provide instructions on how certain file extensions are handled by the web server. In effect, they can be configured in the web.config file to provide an alias that can then be mapped to a class or web object which we specify in our HttpHandlerFactory
. The trick, then, is to find a way to implement an HttpHandlerFactory
to return our internal web service when it is called using JavaScript from any client of our custom control.
First, let's set up our TimeoutWatcherBehavior
class to call this alias. We can write our extendTime
function like this:
extendTime: function(){
this.callWebService();
},
callWebService: function()
{
var webRequest = Sys.Net.WebServiceProxy.invoke(
"SessionTimeoutTool.asmx"
, "ExtendSessionTimeout"
, false
, null
, this.succeededCallback
, this.failedCallback
, "User Context");
},
succeededCallback: function(result, eventArgs)
{
if(result !== null)
alert(result);
},
failedCallback: function(error)
{
alert(error);
},
Specifications for this AJAX Library function can be found here. Even though our web method does not return a value, I have included stub methods for the callback functions, for reference. If you do not need callbacks, the success and fail parameters can be null
. There is also a final optional parameter, which I have left out of the reference code above, that sets a timeout for the call. The AJAX Library documentation says that it can be set to null
, but this actually causes an exception to be thrown. If you do not need a timeout, you should just leave off the parameter.
The project that consumes our control will need to include an HttpHandler
element for our web service alias. Whenever the web server receives a call to our alias, "SessionTimeoutTool.asmx", from an assembly that references our TimeoutWatcherAjaxControl
, we will redirect the call to an HttpHandlerFactory
called "SessionTimeoutTool.SessionTimeoutHandlerFactory
" (which we have yet to write).
<system.web>
<httpHandlers>
<add verb="*" path="SessionTimeoutTool.asmx"
type="SessionTimeoutTool.SessionTimeoutHandlerFactory"
validate="false"/>
...
</httpHandlers>
</system.web>
Writing our HttpHandlerFactory
class requires a bit of black magic. Microsoft message boards are full of entries by Microsoft employees saying that you simply cannot call a web service in one project from another project. This isn't true, of course, but it is tricky. Hugo Batista offers a solution in his blog. Unfortunately, this solution was obviated when, in .NET 3.5, the WebServiceHandlerFactory
was replaced with the ScriptHandlerFactory
as the main class for handling calls to files with an asmx extension. The ScriptHandlerFactory
, which is found in the System.Web.Extensions
assembly, delegates regular web service calls to the WebServiceHandlerFactory
. Calls to web services from JavaScript, however, are implemented through the RestHandlerFactory
. The RestHandlerFactory
, moreover, allows us to pass a type definition for our web service, rather than requiring us to pass a path to an *.asmx page.
There is one additional element of complexity, however. All the HttpHandlerFactory
classes in System.Web.Extensions
are scoped internal
. In order to use classes, consequently, we have to use some Reflection. A solution for doing all this is provided by Robertjan Tuit in a CodeProject article. His solution is simply brilliant, but apparently under-appreciated. I highly encourage you to give him your fives. I had to read through several Chinese hacker sites using Google translator to even figure out what exactly he was doing. Basically, he has used a reflection tool to peer into the ScriptHandlerFactory
in order to figure out what it is doing, and re-implemented the whole thing in order to pass a web service type reference to it rather than an *.asmx file path.
I have streamlined his code a bit, since it is intended only to handle web service calls from JavaScript, and only for one specific web service. I've also added a little additional code, based on Reflection on the RestHandlerFactory
implementation, in order to pass session information.
The implementation is pretty generic, and copy-and-paste ready. You will be able to re-use it, as-is, in your future projects. The only thing that ever changes is the value of the web service class, which is set in the webServiceType
variable. Here is the complete code to be added to the SessionTimeoutTool project:
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Reflection;
using System.Web.Services.Protocols;
using System.Web.Script.Services;
using System.Web.SessionState;
namespace SessionTimeoutTool
{
class SessionTimeoutHandlerFactory: IHttpHandlerFactory
{
#region IHttpHandlerFactory Members
IHttpHandlerFactory factory = null;
Type webServiceType = typeof(WebService1);
public IHttpHandler GetHandler(HttpContext context, string requestType
, string url, string pathTranslated)
{
Assembly ajaxAssembly = typeof(GenerateScriptTypeAttribute).Assembly;
factory = (IHttpHandlerFactory)System.Activator.CreateInstance(
ajaxAssembly.GetType(
"System.Web.Script.Services.RestHandlerFactory"));
IHttpHandler restHandler = (IHttpHandler)System.Activator.CreateInstance(
ajaxAssembly.GetType("System.Web.Script.Services.RestHandler"));
ConstructorInfo WebServiceDataConstructor = ajaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData").GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance
, null, new Type[] { typeof(Type), typeof(bool) }, null);
MethodInfo CreateHandlerMethod = restHandler.GetType().GetMethod(
"CreateHandler", BindingFlags.NonPublic | BindingFlags.Static,
null, new Type[] { ajaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData"),
typeof(string) }, null);
IHttpHandler originalHandler =
(IHttpHandler)CreateHandlerMethod.Invoke(restHandler,
new Object[]{ WebServiceDataConstructor.Invoke(
new object[] { webServiceType, false }),
context.Request.PathInfo.Substring(1)
});
Type t = ajaxAssembly.GetType(
"System.Web.Script.Services.ScriptHandlerFactory");
Type wrapperType = null;
if (originalHandler is IRequiresSessionState)
wrapperType = t.GetNestedType("HandlerWrapperWithSession"
, BindingFlags.NonPublic | BindingFlags.Instance);
else
wrapperType = t.GetNestedType("HandlerWrapper"
, BindingFlags.NonPublic | BindingFlags.Instance);
return (IHttpHandler)System.Activator.CreateInstance(
wrapperType, BindingFlags.NonPublic | BindingFlags.Instance
, null, new object[] { originalHandler, factory }, null);
}
public void ReleaseHandler(IHttpHandler handler)
{
factory.ReleaseHandler(handler);
}
#endregion
}
}
The last thing we need to do is to make sure that our code does not wait until the session has already expired before calling the extendTime
method. As with our TimeoutWatcherControl
, we will configure the TimeoutWatcherAjaxControl
to set the internal timer to fire off 45 seconds early when the ExtendTime
option is selected. We will do this in the setTimer
method of our behavior class.
setTimer:function()
{
if(this._timer)
{
this._timer.set_enabled(false);
if(this._timeoutMode == SessionTimeoutTool.Mode.ExtendTime)
this._timer.set_interval(this.get_interval()- 45000);
else
this._timer.set_interval(this.get_interval());
this._timer.set_enabled(true);
}
Inside our test project, we can test this functionality using the same markup we used in order to test the page redirection option. This markup for our custom control will look like this:
<cc1:TimeoutWatcherAjaxControl
ID="TimeoutWatcherAjaxControl1"
TimeoutMode="ExtendTime"
RunMode="ClientSide"
runat="server" />
We will also add a third UpdatePanel
to the test page that will check to see if the session is still alive by checking for a variable we add to the Session
object. When the variable does not exist, which is the case on the first page hit, and if the session expires, the panel will tell us that the session is new; otherwise, it will return false
.
<asp:UpdatePanel ID="UpdatePanel2" runat="server">
<ContentTemplate>
<div style="border: medium solid Yellow; padding: 5px; width:400px;">
At <%= DateTime.Now.ToLongTimeString() %>
this session is brand new:
<%= Session["old"]==null?"true":"false" %>.
<% Session["old"] = "someValue"; %>
<br /><asp:Button Text="Check Session" ID="Button2" runat="server"/>
</div>
</ContentTemplate>
</asp:UpdatePanel>
Clicking the "Check Session" button will extend the session, since it causes a partial postback, so be sure to wait a good two minutes (or whatever length you have set your session timeout to) after the last page update before clicking it.
The technique described above is key to creating an AJAX control that can call server-side code from client-side code. As far as I know, Microsoft has not provided any other way to build true AJAX functionality into a server control, which is a shame. But, at least, we have a work-around.
We still need to script the customHandler
method. There are several ways of doing this. One option is to expose additional properties in our server control to pass a web service and method name. We could then use the WebServiceProxy.invoke
method described above to call this web service and run our user's code.
This type of functionality is already available through the ServerModeTimeout
event, however, and there is no sense in simply finding a different way to do the same thing here. Instead, we will expose a new property that allows the user to pass custom JavaScript to our control, and we will execute it when the session expires.
Add a new property called CustomHandlerJScript
to the TimeoutWatcherAjaxControl
C# class.
private string _customHandlerJScript;
public string CustomHandlerJScript
{
get { return _customHandlerJScript; }
set { _customHandlerJScript = value; }
}
Pass this to the TimeoutWatcherBehavior
JavaScript class in the GetScriptDescriptors
method.
descriptor.AddProperty("customHandlerJScript", _customHandlerJScript);
Now, in our TimeoutWatcherBehavior
, add an accessor to receive this value:
SessionTimeoutTool.TimeoutWatcherBehavior = function(element) {
...
this._customHandlerJScript = null;
}
SessionTimeoutTool.TimeoutWatcherBehavior.prototype = {
...
get_customHandlerJScript:function()
{
return this._customHandlerJScript;
},
set_customHandlerJScript:function(value)
{
this._customHandlerJScript = value;
},
...
}
Inside the customHandler
function, we will simply use the classic JavaScript function eval
to execute the script passed in by the control's consumer.
customHandler: function(){
this._timer._stopTimer();
eval(this.get_customHandlerJScript());
},
To test this functionality, the markup for our TimeoutWatcherAJAXControl
should look like this:
<cc1:TimeoutWatcherAjaxControl
ID="TimeoutWatcherAjaxControl1"
TimeoutMode="CustomHandler"
RunMode="ClientSide"
CustomHandlerJScript="alert('this is the custom handler');"
runat="server" />
and if all goes well, after about two minutes, you should see this:
Our ASP.NET AJAX Server Control is complete.
III. The ASP.NET AJAX Server Control Extender
As is indicated by its name, the ASP.NET AJAX Server Control Extender is just the AJAX Server Control plus a little something extra. You will recall that the class declaration for our AJAX behavior class takes a parameter called element
. Though we did not discuss this, the element passed in can be accessed throughout our JavaScript code with a call to this.get_element()
. The value of the element
parameter, in turn, is passed to the behavior class inside our server control's GetScriptDescriptors
method. It is the second parameter, there, of ScriptControlDescriptor
's constructor.
ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
"SessionTimeoutTool.TimeoutWatcherBehavior", this.ClientID);
In the AJAX Server Control, we simply pass the ID of the custom control, and the get_element()
function in our behavior class uses this to get a DOM element. In an AJAX Server Control Extension, however, we pass the ID of another control on our web page.
In an Extension Control, we can then use this id
to hook into the DOM element for another page control and add hook into its methods to provide custom behaviors. In effect, this gives us two different ways to add AJAX functionality to a server-side control. We can use the AJAX Server Control Template and implement, for instance, a TextBox
control with some JavaScript attached. Alternatively, we can create a standalone set of behaviors that are then attached to a regular TextBox
control.
This is, essentially, the only important difference between an AJAX Server Control and an AJAX Server Control Extension: whether the JavaScript associated with a custom control applies to itself or applies to an external control. The Extender model, however, is much more flexible, since with it, you can go into a pre-existing application and simply start adding behaviors to your pre-existing controls, rather than having to start replacing each of them with your own AJAX-customized control. Extensions also have the added benefit of allowing you to add multiple behaviors, from multiple Server Control Extensions, to a single control. You might think of this as a way to allow any control to inherit from multiple base classes, whereas the AJAX Server Control only allows you to inherit from one.
The Server Control we built above had a rather humble implementation of the popup functionality. What would be much more cool is if we allowed the user to point to an external panel, and in our implementation, we turned it into a floating DIV
. We can do this by turning our Server Control into an Extension Control.
There are two ways to go about creating our TimeoutWatcherAjaxControlExtension
. We could do what we did above and create an entirely new project based on the ASP.NET AJAX Server Control Extension template, then copy all of our code over to it. But the differences between the regular AJAX Control and the AJAX Extender are rather minor, so I'm going to opt for simply creating a new class based on the TimoutWatcherAJAXControl
and just making a few adjustments to it. This will obviate our having to copy all the *.js files and AssemblyInfo
settings into the new project (though you can certainly choose to do this, if you like).
Doing it my way, simply create a new class file called TimeoutWatcherAjaxControlExtender.cs. Copy all of the AJAX Control code we wrote above into it. The Extension Control inherits from ExtenderControl
rather than ScriptControl
, so we will need to make that change. Also, the class declaration takes an attribute that specifies what kind of control we intend to extend. In our case, this will be a Panel
control. I have commented out the original class declaration so you can see the differences.
[TargetControlType(typeof(Panel))]
public class TimeoutWatcherAjaxControlExtender: ExtenderControl
{
Next, the GetScriptDescriptors
method has a different signature in an Extender class. It takes a control as a parameter. When we create a new ScriptBehaviorDescriptor
object in our method implementation, we simply need to pass the id
of our target control rather than the ID of the Server Control.
protected override IEnumerable<ScriptDescriptor>
GetScriptDescriptors(Control targetControl)
{
if (RunMode == ScriptMode.ClientSide)
{
ScriptControlDescriptor descriptor =
new ScriptControlDescriptor("SessionTimeoutTool."
+ "TimeoutWatcherBehavior", targetControl.ClientID);
...
Those are the only changes we really need to make. JavaScript behavior classes have the same structure whether you are building a Server Control or an Extender, so we can actually just reuse the class we wrote in the previous section. Because we inherit from the ExtenderControl
class, our custom Extender also automatically exposes a new property called TargetControlID
, which the Extender Control's consumer will use in his markup to identify the Panel
control that will be turned into a floating DIV
.
Normally, you would now use the get_element()
function inside your JavaScript prototype to hook into the Panel
's properties and events in order to add new behaviors. You would combine it with the AJAX Library $addHandlers
method to capture a DOM event and then pass it your own custom function, like this:
ControlNamespace.ClientControl1.prototype = {
initialize: function() {
$addHandlers(this.get_element(),
{ 'click' : this._onClick,
},
_onClick: function()
{
alert("clicked");
},
The AJAX Control Toolkit already contains a really good Popup Extender JavaScript class, however, so we will take a shortcut and use that behavior class rather than trying to script up our own. Additionally, it will afford us an opportunity to see how to pull out JavaScript classes from third-party assemblies and use them in our own Control Extenders.
To use the Toolkit, we will need to add the ACT assembly to our bin directory and then add a reference to it. You can get the assembly either from the sample project for this tutorial, or by downloading it from the Microsoft website.
We do not need to add any entries to the AssemblyInfo
class in order to use ACT scripts, since they are already tagged as resources in the ACT assembly. All we need to do is to make sure they get instantiated as *.axd resources, and are accessible through the ScriptResource.axd
path. In the GetScriptReferences
method, add three additional yield
statements in order to make the ACT's PopupBehavior
class accessible. One is for the PopupExtender
itself, while the other two are for some base classes that the PopupBehavior
requires in order to run properly.
yield return new ScriptReference("AjaxControlToolkit"+
".ExtenderBase.BaseScripts.js", "AjaxControlToolkit");
yield return new ScriptReference("AjaxControlToolkit" +
".Common.Common.js", "AjaxControlToolkit");
yield return new ScriptReference("AjaxControlToolkit" +
".PopupExtender.PopupBehavior.js", "AjaxControlToolkit");
We can now instantiate the PopupBehavior
class from our own custom JavaScript behavior class. Add a new variable called this._popupBehavior
to the main class. Then, in the prototype's initialize
routine, set it to a new PopupBehavior
instance by using the AJAX Library $create
function.
this._popupBehavior = $create(AjaxControlToolkit.PopupBehavior
, {"id":this.get_id()+'PopupBehavior'}
, null
, null
, this.get_element());
Our popup can now be rewritten so that all it does is to turn off the internal timer and call the PopupBehavior
class' show()
method.
popup: function(){
this._timer._stopTimer();
this._popupBehavior.show();
},
This control can be tested by adding a Panel
control to a web form and setting its id
as the TargetControlID
of the Extender in markup.
<asp:Panel ID="timeoutPanel" runat="server"
style="display:none;
text-align: center; width:200px; background-color:White;
border-width:2px; border-color:Black; border-style:solid;
padding:20px;">
This session timed out.
<br /><br />
<center>
<asp:Button ID="ButtonOk" runat="server" Text="OK" />
</center>
</asp:Panel>
<cc1:TimeoutWatcherAjaxControlExtender
TargetControlID="timeoutPanel"
ID="TimeoutWatcherAjaxControlExtender1"
TimeoutMode="PopupMessage"
RunMode="ClientSide"
runat="server" />
One warning, however. An Extender Control requires that a TargetControlID
be set, whether it is used in your control or not. It must also be of the type specified in the TargetControlType
attribute used to decorate the class declaration. This means that a user of our Extender will have to set up a dummy Panel
for the TargetControlID
if they want to use any of the functionality other than a PopupMessage
, which isn't particularly graceful. To make things a little more convenient, if still not perfect, I'm going to change the TargetControlType
attribute to a Control
instead of a Panel
, which at least will give the consumer the ability to point to any control on the page when the Popup
option isn't selected.
This completes our Extender control, and the last section of this tutorial. It is my hope that this tutorial has given you the skills and insights required to build your own advanced controls.
JavaScript is always going to be hairy, and the various attempts to clean it up and make it more OOP-like occasionally resemble like slapping lipstick on a pig. However, it really is much cleaner than it used to be, and with the help of the Visual Studio 2008 AJAX Server Control and AJAX Extender templates, we now have the option of hiding much of this code inside custom controls, so that developers who have no interest in client-scripting need never look at it, but can still benefit from it.
The last thing you may want to know, at the end of this rather long tutorial, is how to add an icon for your control to the Toolbox. You actually just need to add a bitmap or icon file to your project and set its Build Action to "Embedded Resource". Then, add these two Toolbox attributes to your class declaration, placing your class name where it is required. In this example, I am using a Catbert icon.
[TargetControlType(typeof(Control))]
[System.Drawing.ToolboxBitmap(typeof(TimeoutWatcherAjaxControlExtender)
, "Catbert.ico")]
[ToolboxData("<{0}:TimeoutWatcherAjaxControlExtender runat="server">
</{0}:TimeoutWatcherAjaxControlExtender>")]
public class TimeoutWatcherAjaxControlExtender: ExtenderControl
{...}
The icon will not show up if your control is referenced as a project reference. It will only show up if you compile your control and then reference its assembly.