KiMoGiGi 技术文集

不在乎选择什么,而在乎坚持多久……

IT博客 首页 联系 聚合 管理
  185 Posts :: 14 Stories :: 48 Comments :: 0 Trackbacks
Refrence From ..

ASP.NET AJAX Controls and Extenders Tutorial

By James Ashley


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?

templates

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 intracacies 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 msdn2.microsoft.com.

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 interesting 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 forebearance 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:
  1. Page Redirect
  2. Popup
  3. Extend Time
  4. Custom Event Handler

In addition, the session timeout watcher will need 1) to know how long the session is set to last, as well as 2) be aware of every time the session timeout is re-extended because of a page postback. Additionally, it will need to 3) 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 control 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 update panel, 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.

Collapse
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 the 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: 1. controls on the Master Page, 2. controls in the Content Page, 3. the Master Page and 4. 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 update panel to our control, and a timer control to that update panel. (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 update panel, 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 update panel when the timer determines that our session has expired.

In nesting our controls, you will notice that we do not use the update panel's Controls property. This is because Update Panel 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)
{
//retrieve session timeout period from server
System.Web.SessionState.HttpSessionState state
= HttpContext.Current.Session;
//convert minutes to milliseconds
_interval = state.Timeout * MINUTES;
//create new ajax components
UpdatePanel timeoutPanel = GetTimeoutPanel();
System.Web.UI.Timer sessionTimer = GetSessionTimer();
sessionTimer.Interval = _interval;
//add timer to update panel
timeoutPanel.ContentTemplateContainer.Controls.Add(
sessionTimer);
//add update panel to timeout watcher control
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 update panel 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 update panel that has its UpdateMode set to "Always." Because the update panel 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 update panel on each postback cause problems, since the update panel 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 Neoplatonic theory of software lifecycle, it was normal to think of permanance 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 update panel and registered with the ScriptManager component on the first full postback of the page, on each subsequent partial postback, we are required to creating a new code object that will correspond to the markup for our panel, which hasn't actually changed at all. As an expirament, you can try setting a different ID for the update panel on each partial postback. You'll notice that the code throws an exception if the id of the update panel we create does not correspond to the id of the update panel 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 update panel, 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:

Collapse
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:
//do nothing, page has reposted
//45 seconds before timeout
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 update panel, 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 update panel 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 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 = true;
_sessionTimer.Enabled = this.TimerEnabled;

Now we just need to retrieve the current update panel 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 update panel 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 update panel, 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 hardcoding it as a property of the TimeoutWatcherConrol in the markup.

And, mutatis mutandis, after about two minutes from the time in the update panel, 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 reinvoked 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, but sure to remain cognizant of the minor naming discrepencies 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.

/// <reference name="MicrosoftAjax.js"/>
Type.registerNamespace("SessionTimeoutTool");
SessionTimeoutTool.TimeoutWatcherBehavior = function(element) {
SessionTimeoutTool.TimeoutWatcherBehavior.initializeBase(this, [element]);
}
SessionTimeoutTool.TimeoutWatcherBehavior.prototype = {
initialize: function() {
AjaxServerControl1.TimeoutWatcherBehavior.callBaseMethod(this, 'initialize');
// Add custom initialization here
},
dispose: function() {
//Add custom dispose actions here
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 these 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.

2. 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.

3. 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.

4. Property constructors are preceded by "get_" and "set_".

5. All methods, including your class declaration, follow the paradigm function name, colon, function declaration (e.g. myFunction: function(){}).

6. 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.

7. 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.

8. 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, classname 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 editting. 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 resource 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 javscript 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 clientscript, which has no other way of ascertaining this. We also will want to pass a popup message text to the clientscript, 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 update panel is only created and added to the current control if the consumer chooses the ServerSide option.

if (RunMode == ScriptMode.ServerSide)
{
//create new ajax components
UpdatePanel timeoutPanel = GetTimeoutPanel();
...
//add update panel to timeout watcher control
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 "typeof(Sys) !== 'undefined'" line. I'll place the enumerator from our custom control next to the javascript version so you can see the similarities:

//C#
public enum mode
{
PageRedirect,
PopupMessage,
ExtendTime,
CustomHandler
}
//JScript
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 variables to the main class, _timer:

   this._timer = null;

These will be used to handle our internal timer. We could try to handle the window.setInterval call in ourselves in order to set a timer. However, in this case, we are going to use a timer class 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 ScriptResource. 1. Set the script file's Build Action to "Embedded Resource". 2. 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. 4. 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:

Collapse
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Permissive License.
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx.
// All other rights reserved.
/// <reference name="MicrosoftAjax.js" />
/// <reference name="MicrosoftAjaxTimer.js" />
/// <reference name="MicrosoftAjaxWebForms.js" />
///////////////////////////////////////////////////////////////////////////////
// Sys.Timer
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.

// Generate the script reference
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, 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 is 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.

Collapse
    initialize: function() {
SessionTimeoutTool.TimeoutWatcherBehavior.callBaseMethod(this
, 'initialize');
//create timer
this._timer = new Sys.Timer();
//create timer handler           
tickHandlerDelegate = Function.createDelegate(this, this.tickHandler);
this._timer.add_tick(tickHandlerDelegate);
//create onload handler
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 server 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 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 should 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 uncool. 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 camay 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" //path
, "ExtendSessionTimeout" //methodName
, false //useHttpGet
, null //parameters 
, this.succeededCallback
, this.failedCallback
, "User Context");//userContext 
},
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 reimplemented 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 Reflecting 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:

Collapse
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 update panel 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.

check session state

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 Server Mode Timeout 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:

customhandler

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 stand alone 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 make 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.

    //public class TimeoutWatcherAjaxControl : ScriptControl
[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() -- old  
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 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 an 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's 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.

Javscript is always going to be hairy, and the various attempts to clean it up and make it more OOP-like occassionally ressemble 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.

posted on 2008-02-25 23:19 KiMoGiGi 阅读(2130) 评论(0)  编辑 收藏 引用 所属分类: AJAX.NETASP.NET
只有注册用户登录后才能发表评论。