Click here to Skip to main content
Click here to Skip to main content

Implement Script Callback Framework in ASP.NET 1.x

By , 2 Aug 2004
 

Sample Image

Introduction

One of the challenges that every ASP.NET 1.x web developer faced is that there isn't any native support for script callbacks to server without post-back of the current page. For example, retrieving and presenting the details of a selected employee in a dropdown list without forcing the full page refresh, and this is particularly an expensive call for UI-rich pages. So, how can we handle this?

Thanks to the ASP.NET team, they've built in this feature in ASP.NET 2.0, which allows calls to server events from client code without causing the page to post back and refresh. This is great, but we have to consider 2 facts: Time and Money. For time, we have to wait until ASP.NET 2.0 is released and we can't do anything in the meanwhile. For Money, we may be forced to upgrade Visual Studio .NET IDE from 2002/2003 to 2005 if we can't live without the Visual Studio.NET IDE. More importantly, implementing the script callbacks in ASP.NET 1.x isn't that hard at all. So, what are we waiting for?

Thanks to the extensible architecture of Page Controller framework, it gives developers the flexibility in hand to extend its functionalities, where native support is not sufficed. In this article, I'll walk you through the process of implementing the Script Callbacks Framework, and also provide a simplified but effective framework that you can use in ASP.NET 1.x.

Objectives

Several objectives have been set for the implementation of Script Callback Framework in ASP.NET 1.x, they are:

  • Co-exists with the same functionality provided in ASP.NET 2.0 - avoids the use of similar function or variable name in our implementation.
  • Provide a consistent programming model as ASP.NET - similar to the concept of Page.GetPostBackEventReference function.
  • Emulate as many functionalities as Script Callbacks Framework implemented in ASP.NET 2.0.
  • Provide synchronous and asynchronous callback.

Background

Script Callback Framework consists of 2 core components, which are PageTemplate.cs and ScriptCallback.js files. Each of them takes care of the callbacks processing at server side and client side, respectively. The steps involved in callbacks processing are shown below:

  1. Script preparation - Generate and attach the callback JavaScript code to Control's event.
  2. Script callbacks - Invoke the embedded JavaScript, which in turn opens the HTTP connection to the specified remote ASP.NET Page whenever the control's event is triggered.
  3. Server responses - Lookup and invoke the corresponding control to respond accordingly.
  4. Callback handling - Invoke the client Callback Handler or Error Handler based on the status code returned from the server's response.

Script preparation

PageTemplate derives from System.Web.UI.Page where it encapsulates most of the processing logic and plays a critical role in the Script Callback Framework. It provides the GetAsyncCallbackEventReference and GetSyncCallbackEventReference functions, which generate the JavaScript code to be attached to the Control's event.

/// <summary>
/// Obtains a reference to a client-side script function
/// that causes, when invoked, 
/// the server to callback asynchronously to the page. 
/// This method also passes a parameter to the server control
/// that performs the callback processing on the server. 
/// </summary>
/// <param name="control">The server control to process the callback.
///    It must implement the IClientCallbackEventHandler</param>
/// <param name="args">Argument to be passed to the server control
///    that performs the callback processing on the server</param>
/// <param name="cbHandler">Callback Handler (JavaScript function),
///    invokes when callback operation completed successfully</param>
/// <param name="context">Any DHTML reference or extra information
///    to pass to the Callback or Error Handler</param>
/// <param name="errHandler">Error Handler (JavaScript function),
///    invokes when error occurred during the callback operation</param>
/// <returns>Asynchronous callback JavaScript code</returns>
public string GetAsyncCallbackEventReference(
     System.Web.UI.Control control,
     string args,
     string cbHandler,
     string context,
     string errHandler)
/// <summary>
/// Obtains a reference to a client-side script function that causes, 
/// when invoked, the server to callback synchronously to the page. 
/// This method also passes a parameter to the server control
/// that performs the callback processing on the server. 
/// </summary>
/// <param name="control">The server control to process
///    the callback. It must implement the IClientCallbackEventHandler</param>
/// <param name="args">Argument to be passed to the server
///    control that performs the callback processing on the server</param>
/// <param name="cbHandler">Callback Handler (JavaScript function),
///    invokes when callback operation completed successfully</param>
/// <param name="context">Any DHTML reference or extra information
///    to pass to the Callback or Error Handler</param>
/// <param name="errHandler">Error Handler (JavaScript function, 
///    invokes when error occurred during the callback operation</param>
/// <returns>Synchronous callback JavaScript code</returns>
public string GetSyncCallbackEventReference(
    System.Web.UI.Control control,
    string args,
    string cbHandler,
    string context,
    string errHandler)

For example, the following call to GetAsyncCallbackEventReference function, will generate the callback JavaScript code and attaches it to the IncreaseButton's onclick event.

Server Side code

// Callback asynchronously when the button is clicked
IncreaseButton.Attributes["onclick"] = GetAsyncCallbackEventReference
(
Form1,
String.Format("document.getElementById('{0}').value", txtValue.UniqueID),
"IncreaseValueHandler",
String.Format("document.getElementById('{0}')", txtValue.UniqueID),
null
);

Client Side code

<input name="IncreaseButton" id="IncreaseButton" 
 type="button" value="Increase Value"
 onclick="javascript:WebForm_DoAsyncCallback('Form1',
 document.getElementById('txtValue').value,
 IncreaseValueHandler,
 document.getElementById('txtValue'),
 null);" />

PageTemplate overrides the Render method of System.Web.UI.Page class to ensure that ScriptCallback.js file and all the script code necessary to perform the callback is correctly referenced within the page or control before rendering to the client. ScriptCallback.js file contains the client code to initialize the callback infrastructure, opens HTTP connection to specified remote ASP.NET Page for callback processing, as well as invokes the corresponding Callback Handler or Error Handler after receiving the result from the server.

Script callbacks

Whenever the control's event is fired, its embedded JavaScript code invokes the WebForm_DoAsyncCallback or WebForm_DoSyncCallback contained in ScriptCallback.js file to callback to the specified remote ASP.NET Page for processing. Internally, it uses a COM object to issue an HTTP POST or GET command to the specified target URL. This COM object comes with Internet Explorer v5.0 or above, and it is an old acquaintance of many developers:

var xmlRequest = new ActiveXObject("Microsoft.XMLHTTP");

The HTTP verb is GET or POST depending on the size of the data to be sent. If the size exceeds 2KB, a POST command is used. The HTTP request consists of three logical elements: __SCRIPTCALLBACKID, __SCRIPTCALLBACKPARAM, and posted data. The __SCRIPTCALLBACKID value contains the ID of the control to be invoked (the event target parameter), whereas __SCRIPTCALLBACKPARAM carries the input parameter for the server-side stub method.

Server responses

PageTemplate overrides the OnInit method of System.Web.UI.Page class to determine if the current request is Postback or Callback mode. It looks for a __SCRIPTCALLBACKID entry in the Request collection. If so, it sets the public IsCallback property to true and concludes that a callback invocation is being made. In the OnLoad method, it first calls the base.OnLoad method to ensure all the controls including those dynamically created controls are properly initialized. If it's a callback, it then invokes the HandleClientCallback function to further process the request.

Internally, it uses the value obtained from __SCRIPTCALLBACKID entry, which is the ID of the control to be invoked. It looks for the specified control in the Page's Controls collection and checks to see if the referenced control implements the IClientCallbackEventHandler interface (which could be the page itself). If the Control implements the required interface, the PageTemplate invokes the RaiseClientCallbackEvent method on the interface, and prepares the response from the results of the call.

The IClientCallbackEventHandler interface has just one method with the following signature:

string RaiseClientCallbackEvent(string eventArgument);

The eventArgument parameter for the method is retrieved from the Request collection of posted values. The string representation of the input data is contained in an entry named __SCRIPTCALLBACKPARAM. This string can be anything you want and need, including numbers, dates, comma-separated values, XML data or Base64 data, JavaScript and so forth.

PageTemplate uses different status codes to notify client script code about the status of callback processing at server. The status code used here has different meaning than those used in the standard HTTP STATUS_CODE. The table shown below lists down the 4 possible status codes and their meaning:

STATUS DESCRIPTION
200 OK
404 Unable to find the specified control
500 Internal Error (Unknown Error)
501 The specified control does not implement the IClientCallbackEventHandler interface

Note: The status code is appended into a custom HTTP Header entry named "__SCRIPTCALLBACKSTATUS".

Callback handling

Once a callback operation has completed, the client script will be notified to check the status code returned from the custom HTTP Header entry named "__SCRIPTCALLBACKSTATUS". It invokes the registered Callback Handler if the status code is "200". Otherwise, the registered Error Handler is invoked to complete the job.

Using the code

With Script Callback Framework in place, developing a Page that makes use of script callbacks is as easy as the few steps listed below:

  1. Implement the IClientCallbackEventHandler interface on any control, which intends to support the callback. The control can be Page, UserControl as well as any Server Controls.
  2. Write the server-side code in the RaiseClientCallbackEvent method that will be invoked from the client. In doing so, you need to define the data model for the call and decide what information to be exchanged and in what format. The actual data being exchanged must be a string, but the contents of the string can be anything you want and need, including numbers, dates, comma-separated values, XML data or Base64 data, JavaScript and so forth.
  3. Assign a unique ID to every control that supports IClientCallbackEventHandler interface either declaratively (HTML) or programmatically (Code).
  4. Attach the JavaScript code emitted from GetAsyncCallbackEventReference or GetSyncCallbackEventReference function to the control's event. When invoked, initiate and handle the callbacks operation asynchronously or synchronously. Please look at the Limitation section if System.Web.UI.Page or its derivative is used as the callback target.

    Limitation

    System.Web.UI.Page or its derivative cannot be used as the first input parameter for GetAsyncCallbackEventReference or GetSyncCallbackEventReference functions because the system is unable to find out their ID. The workaround is that you should declare a variable of type System.Web.UI.HtmlControls.HtmlForm, and its name must match the Form element's ID used in the Page's HTML source, and use this variable as the first input parameter to the aforementioned functions.

    HTML
    <form id="Form1" method="post" runat="server"></form>
    Code behind
    protected System.Web.UI.HtmlControls.HtmlForm Form1;
    
    IncreaseButton.Attributes["onclick"] = GetSyncCallbackEventReference
    (
        Form1,
        String.Format("document.getElementById('{0}').value", 
        txtValue.UniqueID), "IncreaseValueHandler",
        String.Format("document.getElementById('{0}')", 
        txtValue.UniqueID), null
    );
  5. Write the Callback Handler or Error Handler JavaScript function in the Page's HTML source or in any script file referenced by the Page.

In short, you can use callbacks to update individual elements of a page, such as a Label or a Panel, provide different views of the same data, download additional information on demand, or auto-fill one or more fields. To merge the server-side generated values with the existing page, you typically use the Page's DHTML object model. You give each updateable HTML tag a unique ID and modify its contents using the innerHTML property or any other property and method the DOM supplies.

Last, but not least, I've included a sample, available from the link at the top of this article, that demonstrates the capability of Script Callback Framework.

Points of Interest

Throughout the numerous testing on Script Callback Framework, I found that Microsoft.XMLHTTP COM object caches the result of each call by its requested URL and post data. No connection will be opened to the requested URL if the same requested URL and post data have been used before. However, the numbers of cache items is unknown to me.

Conclusion

Script callbacks allow you to perform callbacks to the server without refreshing the whole page, which gives users the illusion that everything is taking place on the client. Once again, I have presented the Script Callback Framework which is a simplified version of code that will be completely built when ASP.NET 2.0 is released. You might as well get the functionality you want today, and you'll be ahead of the curve in understanding this similar concept in ASP.NET 2.0 tomorrow.

License

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

A list of licenses authors might use can be found here

About the Author

Elvin Cheng
Singapore Singapore
Member
Elvin Cheng is currently living in Woodlands, Singapore. He has been developing applications with the .NET Framework, using C# and ASP.NET since October 2002. Elvin specializes in building Real-time monitoring and tracking information system for Semi-conductor manufacturing industry. During his spare time, he enjoys reading books, watching movie and gym.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralRe: Script ReferencememberEpitka20 Jul '05 - 9:41 
from what I see the reference is injected dynamically into the Head section so you do not need to include it in the html
GeneralMultiple Parametersmembersgentile@woh.rr.com14 Jul '05 - 3:46 
I want to pass more than one parameter. Is this possible with this framework?
 
If so, how would I do this?
 
Thanks

GeneralRe: Multiple Parametersmembershaul_ahuva14 Jul '05 - 4:39 
You could use a separated value list (such as "A|B|C").
GeneralRe: Multiple Parametersmembersgentile@woh.rr.com14 Jul '05 - 4:58 
I tried this and it removed the values past A
 
somewhere it is removing the items from | on ?
 

GeneralRe: Multiple Parametersmembersgentile@woh.rr.com14 Jul '05 - 5:01 
ie
 
//     Callback asynchronously
               employeeList.Attributes["onchange"] = GetAsyncCallbackEventReference
                    (
                    Page.FindControl("Form1"),
                    String.Format("document.getElementById('{0}').options[document.getElementById('{0}').selectedIndex].value|{1}", employeeList.UniqueID, "1"),
                    "JSCallBackHandler",
                    //txtEmployeeDetail.ClientID,
                    txtEmployeeDetail.ClientID,
                    "JSCallBackErrorHandler"
                    );
 
When it generates the click event, it looks like this: 5|1
 
But when the public string RaiseClientCallbackEvent(string eventArgument) is called, the eventArgument is only 5
 
The 1 is getting dumped somewhere?
GeneralRe: Multiple Parametersmembersgentile@woh.rr.com14 Jul '05 - 5:07 
Just fyi as well, when I look at the source I see this:
 
onchange="javascript:WebForm_DoAsyncCallback('Form1',document.getElementById('employeeList').options[document.getElementById('employeeList').selectedIndex].value|1,JSCallBackHandler,txtEmployeeDetail,JSCallBackErrorHandler);
 
document.getElementById('employeeList').options[document.getElementById('employeeList').selectedIndex].value|1
 
but when I click on that, and look at the DoAsyncCallback, the |1 is removed?
GeneralRe: Multiple Parametersmembershaul_ahuva14 Jul '05 - 5:11 
You need to do:
 
String.Format("document.getElementById('{0}').options[document.getElementById('{0}').selectedIndex].value + '|' + {1}", employeeList.UniqueID, "1"),
 
The browse is interpreting 5|1 as the bitwise "or" operation which results in 5 (the bits of "1" are already contained by "5"):
 
00000101 | 00000001 = 00000101
GeneralRe: Multiple Parametersmembersgentile@woh.rr.com14 Jul '05 - 5:27 
ah excellent catch!
 
Thanks for your response!!!
GeneralSugestion to the implementationmemberwofsin8 Jul '05 - 4:47 
Hello, to set if a request is a Callbak you look in the Request.QueryString, but if the lenght of the postData is greater than 2067 it´s used the post method to send data. So the request has no querystrings, wich isn´t detected as a callback.
 
There is a little problem there, because U do all the data treatment on the RaiseClientCallbackEvent. So the page posts, and nothing happens at best.
 
Solution: change in the OnInit and HandleClientCallback of PageTemplate from Request.QueryString to Request.Params. So even if it´s posted it will call the RaiseClientCallbackEvent, and your data will still be treated normally.
 
Yugo Watari
QuestionWhat is the difference between this and AJAX?memberMohsen Khani19 Jun '05 - 21:51 
please, sombody tell me that what is the difference between ScriptCallbackFramwork and AJAX (http://ajax.schwarz-interactive.de/csharpsample/default.aspx[^])
 
Thanks
AnswerRe: What is the difference between this and AJAX?memberKeith Farmer22 Aug '05 - 15:30 
AJAX is a trendy name some people came up with to describe this technique.
GeneralRe: What is the difference between this and AJAX?memberMichael Freidgeim23 Aug '05 - 21:16 
See Brock Allen's Blog post ICallbackEventHandler vs AJAX.NET
[^]
 
Michael
GeneralRe: What is the difference between this and AJAX?memberKeith Farmer23 Aug '05 - 21:24 
And follow the links to Bertrand LeRoy's series of posts: http://weblogs.asp.net/bleroy/archive/2005/04/08/397761.aspx [^]
GeneralUpdated script callback frameworkmembershaul_ahuva26 May '05 - 12:44 
I have updated the version of the script callback framework that I've been working on. This new version has some interface-breaking changes. Here's a list of the changes:
 
    Server-side:
  • Re-wrote in C# (I _finally_ got approval to use C# at work Smile | :) ).
  • Rearranged PageEx internal code to allow the viewstate to be refreshed on the client.
  • Added a couple overloads for GetCallbackEventReference.
    Client-side:
  • Instead of two callback functions that are mostly alike there is now only one: WebForm_DoCallback. This function takes a bool flag as the last argument to indicate if the request should be asynchronous or not.
  • Added an UpdateViewState function that (surprise, surprise) updates the viewstate on the client after receiving a response. ALL responses in the new version are formatted as follows:

    "[viewstate]<--#AJAXRESPONSE#-->[result from RaiseCallbackEvent]"

     
    The response is parsed and only the result from RaiseCallbackEvent is sent to the client event handler.
The code and sample are now in a VS.NET 2003 solution. The first project (ScriptCallback) is the callback framework and (as many people have requested) an extended DataGrid control that passes it's events through the callback framework. The second project is just a simple web app demoing the framework (make sure to change the connection string and data source in GetTable if needed).
 
Since the script callback framework now has the ability to persist viewstate and pass events, the next logical step will be to develop a generic AJAX engine for ASP.NET. The general idea is to provide access to the callback framework for any control(s) without modifying/extending anything (other than setting a dynamic property if AJAX should be enabled). Look for this (probably as a separate article) in the next few months (unless someone out there is feeling ambitious Smile | :) ).
 
I didn't thoroughly test the demo app, but it should be sufficient to demonstrate what's possible with the script callback framework.
 
The zip file is located at http://mywebpages.comcast.net/shaul_ahuva/ScriptCallback.zip.
 
If you have any constructive criticism, please let me know.
GeneralUPDATE - Bug fixmembershaul_ahuva1 Jun '05 - 10:05 
I discovered a bug in PageEx.js - when the xml request is opened the async parameter is not set to the value of the async parameter being passed to WebForm_DoCallback.
 
I have uploaded the fix to my web site at http://mywebpages.comcast.net/shaul_ahuva/ScriptCallback.zip
 
Also, I have updated the code-behind to only render the controls that are needed to reduce server processing time.
GeneralRe: UPDATE - Bug fixmembersgentile@woh.rr.com14 Jul '05 - 4:09 
This link is not working for me?
GeneralRe: Updated script callback frameworksussAnonymous9 Sep '05 - 6:56 
Hi,
I have suggestion and few questions.
- It's not bad idea to include textarea also in your pageex.js.
- In my opinion it is good to escape values too.
- I extend a little bit your solution using controlCollection as a Callback result, because there is situation where two or more grids work together, Smile | :) raising events and responding to events. What is your opinion?
--
My question is about onload event. When I open a page using callback and when this page has onload event in the body, I found that handler never executes.
What I'm doing wrong?
 
Thanks in advance
Darko
 


GeneralRe: Updated script callback frameworksussAnonymous9 Sep '05 - 7:16 
Hi,
Hear is an example.When I open this page using callback window_onload never executes. Everything is the same when using javaScript instead of vbscript.
 

<BODY bottomMargin="0" vLink="blue" link="blue" leftMargin="0" topMargin="0" rightMargin="0"
language="javascript" önload="return window_onload()">
<meta content="Microsoft Visual Studio .NET 7.1" name="GENERATOR">
<meta content="Visual Basic .NET 7.1" name="CODE_LANGUAGE">
<meta content="JavaScript" name="vs_defaultClientScript">
<meta content="http://schemas.microsoft.com/intellisense/ie5" name="vs_targetSchema">
<LINK href="Styles.css" type="text/css" rel="stylesheet">
<form id="Form1" method="post" runat="server">
</form>

<activereportsweb:WebViewer id="WebViewer1" runat="server" height="0px" width="0px" ViewerType="ActiveXViewer"
HtmlCharacterSet="UnicodeUtf8">
<script language="vbscript">
Sub WebViewer1_ActiveX_LoadCompleted()
document.all("WebViewer1_ActiveX").Object.PrintReport(false)
 
End Sub
</script>
</BODY>
 
Thanks in advance
D

GeneralExamplesussAnonymous9 Sep '05 - 7:16 
Hi,
Hear is an example.When I open this page using callback window_onload never executes. Everything is the same when using javaScript instead of vbscript.
 

<BODY bottomMargin="0" vLink="blue" link="blue" leftMargin="0" topMargin="0" rightMargin="0"
language="javascript" önload="return window_onload()">
<meta content="Microsoft Visual Studio .NET 7.1" name="GENERATOR">
<meta content="Visual Basic .NET 7.1" name="CODE_LANGUAGE">
<meta content="JavaScript" name="vs_defaultClientScript">
<meta content="http://schemas.microsoft.com/intellisense/ie5" name="vs_targetSchema">
<LINK href="Styles.css" type="text/css" rel="stylesheet">
<form id="Form1" method="post" runat="server">
</form>

<activereportsweb:WebViewer id="WebViewer1" runat="server" height="0px" width="0px" ViewerType="ActiveXViewer"
HtmlCharacterSet="UnicodeUtf8">
<script language="vbscript">
Sub WebViewer1_ActiveX_LoadCompleted()
document.all("WebViewer1_ActiveX").Object.PrintReport(false)
 
End Sub
</script>
</BODY>
 
Thanks in advance
D

GeneralMaintaining ViewStatememberBen Kitzelman25 May '05 - 19:03 
Hi,
 
Just have a quick question regarding Viewstate. If I implement the IClientCallbackEventHandler interface on a control and set a property server side - how can I update the ViewState to reflect the changes (obviously without posting back)? I've been playing around with the Page Template and I am finding that controls which have been modified on CallBack aren't maintaining state between postbacks. Thanks
 
Ben
GeneralRe: Maintaining ViewStatemembershaul_ahuva26 May '05 - 11:07 
Ben,
 
I actually have been looking at this issue, and I have a solution - always render the page during the callback and pull the viewstate out of the page source. Then, just update the viewstate client-side. This allows ASP developers to use AJAX concepts. In fact, the next logical step would be to create a generic ASP.NET AJAX engine for normal controls.
 
I'll be uploading the changes later tonight.
GeneralCustom Datagrid + Double Combo Box using Callback Frameworkmembertimyee22 May '05 - 8:57 
I have a working sample (using Northwind database) which displays the functionality of the code posted by shaul_ahuva.
 
If you would like it, please email me at tim@timyee.com.
 
Features:
1. Uses a custom datagrid (which has client-side paging and sorting and also column dragging, taken from msdn cutting edge article). The paging event uses the callback framework. The sorting event i used the msdn version which sorts only the records that are currently displayed, but not the whole datatable. I am going to test out sorting using the callback framework later.
2. Uses a dynamic client side drop down list population (with a loader message....displaying "Loading Drop Down List...". This uses the callback framework.
 
Note: There are a few "hacks" that I had to do to make it work. Maybe you can find a better way, but it works for me. Let me know if you do.
 
Another Note: within the code the names of the components are not exactly those consistent with Northwind, as I am integrating this code into my own project.
 
If you have any questions feel free to ask.
 
Hope you find this helpful, and thankyou shaul_avuna for posting your solution.
 
Regards,
 
Tim Smile | :)
GeneralI get an error everytime I use an XML files which is over 2KBmembertimyee11 May '05 - 13:32 
Does anyone know why this is so?
 
When I use an xml file which is under 2KB, there is no problem. When I use an xml which is over 2KB the code in Employee.ascx, more specifically function ShowEmployeeErrorHandler(responseText, context), gets called instead of function ShowEmployeeDetailHandler(responseText, context) which should be called when everything is normal.
 
I traced the code in ScriptCallback.js, more specifically the function WebForm_DoSyncCallback(eventTarget, eventArgument, eventCallback, context, errorCallback),
 
if (pageUrl.length + postData.length + 1 > 2067) {
usePost = true;
}
if (usePost) {
xmlRequest.open("POST", pageUrl, false);
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send(postData);
}
else
{
if (pageUrl.indexOf("?") != -1) {
xmlRequest.open("GET", pageUrl + "&" + postData, false);
}
else {
xmlRequest.open("GET", pageUrl + "?" + postData, false);
}
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send();
}
 
which determines whether to do a POST or a GET depending on the size of the data. Obviously the GET works, but the POST for some reason just does not work.
 
Does anyone have any idea how I would fix this? I am very excited about these pieces of code and would like to use them in my program.
 
Thankyou!!!!
 
Tim Smile | :)
tim@timyee.com
GeneralRe: I get an error everytime I use an XML files which is over 2KBmembertimyee11 May '05 - 15:47 
Never mind. I didn't see the next page of posts. Someone had the exact same problem. Solved! Thanks.
GeneralRe: I get an error everytime I use an XML files which is over 2KBmembersrajkumar_in13 May '05 - 1:55 
You need to Pass the "__SCRIPTCALLBACKID" ID & and its value AS Bellow
then it will work fine.
 

if (usePost) {
xmlRequest.open("POST", pageUrl + "&__SCRIPTCALLBACKID=" + eventTarget + "&__SCRIPTCALLBACKPARAM=" + escape(eventArgument).replace(re, "%2B"), true);
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send(postData);
}

 
Regards,
Rajkumar.S
QuestionSecurity Issues?memberDavid Freeman29 Apr '05 - 13:55 
Hi There!
 
Great Article!! I'm gonna adpot your technique to my web apps very soon. But just one question, since Client side JavaScript can be inspected by the end-user, do you think that exposing the JavaScript call backs functions on the client would allow an attacker to gain knowledge of the business logic on the server? Confused | :confused:
 
Thanks!!!
AnswerRe: Security Issues?membershaul_ahuva3 May '05 - 14:29 
I don't think it would be a problem; the only danger would be the same danger inherent in all web development - undesired/malicious data coming across in the request. This can easily be protected against with data validation.
GeneralStatus CodesmemberRamTough_669928 Mar '05 - 12:13 
Hi There!
 
Fantastic example...it's really cool.
 
Is there a way to get rid of the status codes at the bottom of the screen? (the 200, etc.)
 
Thanks!
 
Big Grin | :-D
GeneralRe: Status CodesmemberShukhrat21 Jun '05 - 11:04 
I don't know if it was done intentially , but one of variables that was used to get the coming back result is named which matches with window.status. Just add declaration in Javascript and replace with newly declared variable.
 
Thank you
GeneralEvents through the callback frameworkmembershaul_ahuva23 Mar '05 - 6:06 
A co-worker of mine really liked this idea and starting using it. He was using the client-side paging datagrid from msdn, but I thought a better way would be to modify the datagrid to work like the new GridView control. In order to do this, I had to get the paging/sorting events to be called through the callback framework. I made the following changes:
 
1) WebForm_InitCallback is broken:
Checkboxes are added into the form/querystring regardless of their state (this problem is also in .NET 2.0 -- see http://lab.msdn.microsoft.com/productfeedback/viewfeedback.aspx?feedbackId=5c115f35-d7d7-47c0-9e2f-262c3cccdec2".
If trying to pass an event through the framework, the last button processed will kick off it's event and not the value of __EVENTARGET because all buttons are added to the form/querystring. This problem is fixed in .NET 2.0.
 
2) Created an enhanced version of __doPostBack called __doPostBackEx. This function sets up the post to pass an event through the callback framework. Any events that go through the callback framework must use this function.
 
3) Changed the return value of IClientCallbackEventHandler.RaiseClientCallbackEvent to "object" and changed how the return value was handled so that a control's entire html markup could be returned to the client and updated dynamically.
 
4) Added code to allow the Page class to implement the IClientCallbackEventHandler.
 
There's probably other changes, but I think those are the major ones.
 
I included a sample (just a code snippet) of how I got the DataGrid to pass it's events through the callback framework.
 
I made some breaking changes (removed un-needed status codes, changed interface/method names to match .NET 2.0), so if you use this merge the changes carefully!
 
You can get the updated files from http://mywebpages.comcast.net/shaul_ahuva/scriptCallback.zip
GeneralRe: Events through the callback frameworkmemberRonBurd24 Mar '05 - 10:25 
Nice work!
I have a couple of questions:
 
1) Is your enhanced framework working correctly with Firefox?
2) Could you expand on the checkbox problem
3) Did you managed to fix it for 1.1 ?
 
Thanks!
 

GeneralRe: Events through the callback frameworkmembershaul_ahuva25 Mar '05 - 1:01 
1) I don't know if it works with FireFox - I would assume not if the original code doesn't work. I guess I can take a look at it once I get to work today Smile | :)
 
2) Basically, a checkbox should not be added to the POST's form (or the GET's querystring) unless it is checked. Both the CheckBox WebControl and HtmlInputCheckBox HtmlControl check to see if they get any post data during Page.ProcessPostData(). If they have a value (it doesn't matter what it is), they infer that they should be checked.
 
What thens happens is that EVERY single checkbox reads as "Checked=True" during a callback, regardless of their true state on the client.
 
This happens because the WebForm_InitCallback method just rips through all input elements and adds them to the form/querystring without regard for the fact that different types of input elements should handled differently.
 
3) Yes, I did get it fixed. I also fixed a problem with how .NET 1.1 handles postback events so that the events could get passed through the callback framework (again, a problem with WebForm_InitCallback) - this problem doesn't exist in .NET 2.0 because they changed the way the framework handles events.
 
To see for yourself, get copy of Reflector (http://www.aisto.com/roeder/dotnet/) and look at the Page.ProcessRequestMain and Page.ProcessPostData methods as well as the CheckBox.LoadPostData and HtmlInputCheckBox.LoadPostData implementations.
GeneralRe: FireFox problemsmembershaul_ahuva25 Mar '05 - 6:27 
I'm assuming that the code is breaking on this line:
 
selectChild = element.children.length;
 
If it is, just replace it with the following:
 
selectCount = (element.children == null) ? 0 : element.children.length;
 
Evidently, FireFox sets children to "null" while IE sets children to a valid collection with 0 elements.
 
Hope this helps...
 
Hmmm...just reflected .NET 2.0 and saw that the problem exists there as well. Oh well, this is why we have betas and CTPs.
GeneralRe: Events through the callback frameworksussDarkoMartinovic4 Apr '05 - 9:24 
Hi,
First of all thank you for your update!
I have a suggestiong and few questions.
A full funcional example would be great.
I do not understand what PrepareDataGridEx means? Is it your server side code where your accomplish databinding?
What does controlSource variable mean?
What does ncsi in following statement mean
= new streamReader([Assembly].GetExecutingAssembly().GetManifestResourceStream("Ncsi.PageEx.js")) Do you put PageEx in resx file or what?
Please help.

GeneralRe: Events through the callback frameworkmembershaul_ahuva4 Apr '05 - 17:22 
Going in the order they are accessed:
 
controlSource is just a string containing the control's HTML source (obtained by creating my own HtmlTextWriter with a StringWriter and calling "MyBase.Render / base.Render"). In the example given, I simply replace any __doPostBack calls in the first and last table rows with updated __doPostBackEx calls.
 
PrepareDataGridEx is the JS function called just prior to an event being raised through the callback framework. If you look at the signature of __doPostBackEx you'll notice that the fourth argument (preCallBackHandler) is called just prior to rebuilding the form. This gives your control a chance to update any elements with current data (in this case I just keep the previous page number in a hidden element since the DataGrid keeps it in viewstate).
 
When retrieving embedded resources from an assembly, you must prefix the resource name with the namespace it is in. You can add files as embedded resources by setting the "Build Action" property to "Embedded Resource" for the file(s).
 
While I can't give a more comprehensive example (mostly due to time right now), I can point out what I have done to extend a control so events can be passed through the callback framework:
 
1) Create a hidden element during CreateChildControls and append it to the Controls collection. I don't know if this is the best place or not, but I also load the hidden element's value from the request's form. I think there was a specific reason why I chose to do it in CreateChildControls, but the reason escapes me right now.
2) Register a function to be called prior to the callback being posted. Again, this function prepares any current data that you'll need.
3) Render the control and replace __doPostBack calls with __doPostBackEx calls as needed.
4) Implement the IPostBackClientEventHandler (IClientCallbackEventHandler in the original code) interface and return "this/Me" as the return value.
 
That's really about it; the rest of the code is in PageEx and PageEx.js. What will happen is something akin to the following:
 
1) The user clicks on something that calls __doPostBackEx. This calls your pre-callback handler if one exists, updates the form and posts the callback.
2) ASP.NET will create all of your controls and load them with data from viewstate. The hidden control I detailed above will also be created at this time.
3) ASP.NET will find the control to get the event in the __EVENTTARGET form value and call the RaisePostBackEvent). In the case of my DataGrid, it is a DataGridLinkButton; the event for the button is bubbled to the DataGrid, which figures out which event delegate needs to be called and calls it.
4) After you handle the event and control returns to ASP.NET, the Render method is called on PageEx.
5) The callback is handled at this time. In this scenario, the control's RaiseCallbackEvent is called and returns a reference to itself.
6) PageEx determines what type of data was returned (either a class descending from System.Web.UI.Control or anything else), and renders the response appropriately.
7) The response is received by the client, which updates the innerHTML property of the context. I changed this recently in my own code since it was a bad idea; all controls are now responsible for updating themselves.
 
That's pretty much it. I'll try to find time to get a working sample up at some point in the near future.
GeneralRe: Events through the callback frameworkmembertimyee22 May '05 - 8:56 
I have a working sample (using Northwind database) which displays the functionality of the code posted by shaul_ahuva.
 
If you would like it, please email me at tim@timyee.com.
 
Features:
1. Uses a custom datagrid (which has client-side paging and sorting and also column dragging, taken from msdn cutting edge article). The paging event uses the callback framework. The sorting event i used the msdn version which sorts only the records that are currently displayed, but not the whole datatable. I am going to test out sorting using the callback framework later.
2. Uses a dynamic client side drop down list population (with a loader message....displaying "Loading Drop Down List...". This uses the callback framework.
 
Note: There are a few "hacks" that I had to do to make it work. Maybe you can find a better way, but it works for me. Let me know if you do.
 
Another Note: within the code the names of the components are not exactly those consistent with Northwind, as I am integrating this code into my own project.
 
If you have any questions feel free to ask.
 
Hope you find this helpful, and thankyou shaul_avuna for posting your solution.
 
Regards,
 
Tim Smile | :)
GeneralExample not working in Firefox 1.0memberRonBurd22 Mar '05 - 13:49 
Hi, I downloaded and tried your code. Very nice work!
 
I could not make the employee control work in Firefox. I started debugging it and the server is binding the XML correctly but the problem seem to be in the client script.
 
Since Im not a Javascript saavy I wanted to ask, does someone made it work?
QuestionHow to use on IE TreeView webcontrolmemberEzra11 Mar '05 - 11:10 
Nice code. Demo works nicely.
 
But, I'm lost on how to use it for the treeview.
 
Basically, I want the nodes to be added when the plus sign is clicked.
 
When the node (without children) is clicked, I add the node value to listbox.
 
Any suggestion is greatly appreciated.
 
Thanks,
 
Sam.
 

AnswerRe: How to use on IE TreeView webcontrolmembershaul_ahuva23 Mar '05 - 6:13 
I posted updated code that allows controls to be returned (the entire markup, not specific elements).
 
Assuming that the code I wrote is being used, you could do this one of two ways:
 
1) Modify/extend the TreeView to replace any __doPostBack calls with __doPostBackEx calls so that the event gets passed through the framework. Then, if it's a callback render just the nodes that are needed and return them to a post-event js function. I haven't tried it, but the eventCallback function could probably be used for this.
 
2) In the RaiseCallbackEvent, return the entire TreeView. This approach would require passing the needed data in via the eventArgument parameter.
 
Hope this helps...
GeneralDisabling cashing during callsmemberArtur M.25 Feb '05 - 5:06 
Currently, when you use XMLHttpRequest object, system cashes all you calls and if GET string is the same it loads page from the cash. It is not always what is required. Simplest solution is to make GET string every time different. I have change this framework a little bit to add this feature. For interested people I am passing solution beneath.
GeneralImplementationmemberArtur M.25 Feb '05 - 5:17 
I tried to leave most implication intact like conformance to ASP.NET 2.0. However, I have changed a lot javascript for my convience, so I am passing all javascript and related parts from PageTemplate
 
script:
 

var __callbackList = new Array(); // Holds the asynchronous callback handler
 
// Add the Callback Handler to array list
function addToCallbackList(cb) {
__callbackList[__callbackList.length] = cb;
}
 
var pageUrl = ""; // Post Back URL
var __theFormPostData = ""; // Form Data
 
function WebForm_InitClientCallback(cbUrl) {
pageUrl = cbUrl;
var theForm = document.forms[0]; // ASP.NET currently support single form PostBack only
count = theForm.elements.length;
var element;
re = new RegExp("\\x2B", "g");
for (i = 0; i < count; i++) {
element = theForm.elements[i];
if (element.tagName.toLowerCase() == "input") {
__theFormPostData += element.name + "=" + element.value.replace(re, "%2B") + "&";
}
else if (element.tagName.toLowerCase() == "select") {
selectCount = element.children.length;
for (j = 0; j < selectCount; j++) {
selectChild = element.children[j];
if ((selectChild.tagName.toLowerCase() == "option") && (selectChild.selected == true)) {
__theFormPostData += element.name + "=" + selectChild.value.replace(re, "%2B") + "&";
}
}
}
}
}
 
//Create request object
function getXMLHttpRequest() {
var xmlhttp;
if (window.XMLHttpRequest){
xmlhttp = new XMLHttpRequest();
}
else if (window.ActiveXObject) {
try{
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
}
catch(e) {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
}
//Make the last try to get
else {
xmlhttp = new XMLHttpRequest();
}
return xmlhttp;
}
 
//Helper fuctions
function prepareFormPostback(eventTarget, eventArgument, disableCash)
{
re = new RegExp("\\x2B", "g");
var strData = ""
if (!disableCash) {
var date = new Date();
strData = date.toString();
//strData += " " + date.getTime();
}
postData = __theFormPostData +
"__SCRIPTCALLBACKID=" + eventTarget +
"&__SCRIPTCALLBACKPARAM=" + escape(eventArgument).replace(re, "%2B");
if ( !disableCash && strData != null && strData.length > 0){
postData += "&__DATEHASH="+escape(strData);
}
return postData
}
 
//Create request object, send data using it and return object
function sendData(xmlRequest ,postData)
{
usePost = false;
if (pageUrl.length + postData.length + 1 > 2067) {
usePost = true;
}
if (usePost) {
xmlRequest.open("POST", pageUrl, true);
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send(postData);
}
else {
if (pageUrl.indexOf("?") != -1) {
xmlRequest.open("GET", pageUrl + "&" + postData, true);
}
else {
xmlRequest.open("GET", pageUrl + "?" + postData, true);
}
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send(null);
}
return xmlRequest;
}
 
// Callback asynchronously
function WebForm_DoAsyncCallback(eventTarget, eventArgument, eventCallback, context, errorCallback, disableCash) {
try
{

var postData = prepareFormPostback(eventTarget, eventArgument, disableCash)
var xmlRequest = new getXMLHttpRequest();

xmlRequest.onreadystatechange = WebForm_OnClientCallbackComplete;
sendData(xmlRequest, postData);

var __callbackObject = new Object();

__callbackObject.xmlRequest = xmlRequest;
__callbackObject.eventTarget = eventTarget;
__callbackObject.eventArgument = eventArgument;
__callbackObject.eventCallback = eventCallback;
__callbackObject.context = context;
__callbackObject.errorCallback = errorCallback;

addToCallbackList(__callbackObject);

}
catch(e)
{
if (errorCallback != null)
errorCallback(e.message, context);
else
{
// For Testing Purposes
//alert(e.message);
}
}
}
 
// Callback Synchronously
function WebForm_DoSyncCallback(eventTarget, eventArgument, eventCallback, context, errorCallback, disableCash) {
try
{
var postData = prepareFormPostback(eventTarget, eventArgument, disableCash);

var xmlRequest = getXMLHttpRequest()
sendData(xmlRequest, postData)

response = xmlRequest.responseText;
status = xmlRequest.getResponseHeader("__SCRIPTCALLBACKSTATUS");
if (status == "200")
{
if (eventCallback != null)
eventCallback(response, context);
}
else
{
if (errorCallback != null)
errorCallback(response, context);
else
{
// For Testing Purposes
//alert(response);
}
}
}
catch(e)
{
if (errorCallback != null)
errorCallback(e.message, context);
else
{
// For Testing Purposes
//alert(e.message);
}
}
}
 

// Asynchronous Callback Completed
function WebForm_OnClientCallbackComplete()
{
for(var i = 0; i < __callbackList.length; i++)
{
var __cbObject = __callbackList[i];

if (__cbObject != null && __cbObject.xmlRequest.readyState == 4)
{
try
{
xmlText = __cbObject.xmlRequest.responseXML;
response = __cbObject.xmlRequest.responseText;
status = __cbObject.xmlRequest.getResponseHeader("__SCRIPTCALLBACKSTATUS");
if (status == "200")
{
if (__cbObject.eventCallback != null)
__cbObject.eventCallback(response, __cbObject.context);
}
else
{
if (__cbObject.errorCallback != null)
__cbObject.errorCallback(response, __cbObject.context);
else
{
// For Testing Purposes
//alert(response);
}
}
}
catch(e)
{
if (__cbObject.errorCallback != null)
__cbObject.errorCallback(e.message, __cbObject.context);
else
{
// For Testing Purposes
//alert(e.message);
}
}
finally
{
__cbObject.xmlRequest = null;
__cbObject = null;
__callbackList[i] = null;
}
}
}
}

 
and PageTemplate:
 

 
public static string GetAsyncCallbackEventReference(
System.Web.UI.Control control,
string args,
string cbHandler,
string context,
string errHandler,
bool disableCash) {
 
if (control == null)
throw new ArgumentNullException("control");

if (cbHandler == null || cbHandler == String.Empty)
throw new ArgumentException("Client Callback Handler is required.", "cbHandler");

if (args == null || args == String.Empty)
args = "null";

if (context == null || context == String.Empty)
context = "null";

if (errHandler == null || errHandler == String.Empty)
errHandler = "null";
string strDisbCashed = disableCash ? "true" : "false";
return String.Format("javascript:WebForm_DoAsyncCallback('{0}',{1},{2},{3},{4}, {5});",
control.ID,
args,
cbHandler,
context,
errHandler,
strDisbCashed);
 
}
public static string GetAsyncCallbackEventReference(
System.Web.UI.Control control,
string args,
string cbHandler,
string context,
string errHandler)
{
return GetAsyncCallbackEventReference(control, args, cbHandler, context, errHandler, false);
}
 
public static string GetSyncCallbackEventReference(
System.Web.UI.Control control,
string args,
string cbHandler,
string context,
string errHandler,
bool disableCash) {
 
if (control == null)
throw new ArgumentNullException("control");

if (cbHandler == null || cbHandler == String.Empty)
throw new ArgumentException("A Callback Handler is required.", "cbHandler");

if (args == null || args == String.Empty)
args = "null";

if (context == null || context == String.Empty)
context = "null";

if (errHandler == null || errHandler == String.Empty)
errHandler = "null";
string strDisbCashed = disableCash ? "true" : "false";
return String.Format("javascript:WebForm_DoSyncCallback('{0}',{1},{2},{3},{4}, {5});",
control.ID,
args,
cbHandler,
context,
errHandler,
strDisbCashed);
}
 
public static string GetSyncCallbackEventReference(
System.Web.UI.Control control,
string args,
string cbHandler,
string context,
string errHandler)
{
return GetSyncCallbackEventReference(control, args, cbHandler, context, errHandler, false);
}

 
I hope it would be useful for somebody. Smile | :)
 
I have checked it with IE, Mozilla and Opera. However, Opera is not working correctly even current Beta version Frown | :(
 
Also I have not checked async calls but they should work. If not, let me know I will fix it.
 
Besides, thanx Elvin you did a great work. Big Grin | :-D
GeneralRe: ImplementationmemberArtur M.25 Feb '05 - 5:25 
Code looks ugly Frown | :(
 
Also you could change from date.toString() to date.getTime() in the script.
GeneralRe: Disabling cashing during callsmemberRajaRavipati2 Mar '05 - 6:46 
The solutions to the caching problem.
1.Add xmlRequest.setRequestHeader("Cache-Control","no-cache"); in the JS File
2.Add Response.AddHeader("Cache-Control","no-cache"); in the finally block of the HandleClientCallback Method in the PageTemplate.cs
 
You can use one or both of the above mentioned solutions.
 

 
Promise only what you can do & then deliver more than what you promised.
GeneralRe: Disabling cashing during callsmemberArtur M.2 Mar '05 - 20:30 
Thanx Raja, for me it is just in time note for some how something is not working nice with Mozilla.
 
Thanks a lot.
GeneralQuestionmemberArtur M.2 Mar '05 - 23:50 
Yes, this the right way how this has to be handled.
 
I have read RFC. It says that "no-cache" can be applied to both request and response.
 
However, for me is not clear what happens if caching is applied to request and not to response?
 
Could someone enlight on this?
GeneralIf data sent &gt; 2KB and using non IE Browsermemberoyvindhabberstad5 Feb '05 - 12:08 
Hi!
 
I have just experienced a problem when using Firefox and sending a SyncCallback with more than 2KB.  
I got the following exception:
Error: uncaught exception: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIXMLHttpRequest.send]"   nsresult: "0x80004005 (NS_ERROR_FAILURE)"   location: "JS frame :: http://localhost/crm/Javascript/ScriptCallback.js :: WebForm_DoSyncCallback :: line 137"   data: no]
 
To solve the problem I used the same piece of code that is used for the IE browser. In ScriptCallback.js I changed the following code (starting at line 131)
 
if (pageUrl.indexOf("?") != -1) {
     xmlRequest.open("GET", pageUrl + "&" + postData, false);
}
else {
     xmlRequest.open("GET", pageUrl + "?" + postData, false);
}     
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send(null);
 
With this code (similar to the one used of the IE Browser):
 
if (pageUrl.length + postData.length + 1 > 2067) {
     usePost = true;
}
if (usePost) {
        alert('post');
     xmlRequest.open("POST", pageUrl, false);
     xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
     xmlRequest.send(postData);
}
else {
     if (pageUrl.indexOf("?") != -1) {
xmlRequest.open("GET", pageUrl + "&" + postData, false);
     }
     else {
xmlRequest.open("GET", pageUrl + "?" + postData, false);
     }     
     xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
     xmlRequest.send(null);
 
}
 
I thought I just mention it...
 
- Cheers, Øyvind
GeneralExcellent ArticalsussJaco Roux31 Jan '05 - 1:56 
This is very informative.
 
However I am experiencing some difficulties.
 
I have created a composit control that Adds Some other controls in a override
for the createChildControls method.
 
Now on postback this works fine and the onload events of the child controls fire without an issue. However on the callback the onload override is never called. This happens even thought the controls are added to the Control Collection of the custome composite control.
 
Does Anyone have any idea why the onload method will not be called?
 

 
Thanks
Jaco
GeneralUsing Control inside ControlsussAnonymous10 Jan '05 - 7:57 
I had a problem when using a control that has a control inside. To replicate the issue just put employee.ascx (sample) into a new control and put that onto a page.
you get the error "Unable to find the specified Control".
You can solve the problem by changing GetAsyncCallbackEventReference and GetSyncCallbackEventReference: from control.ID to control.UniqueID.
 
I enjoy using your code
 
Mathias

GeneralGreat!memberSteven Berkovitz3 Jan '05 - 12:40 
I was looking to start developing some web apps using this technology and I recalled reading about this in MSDN magazine. However, the implementation provided was lacking just about everything I wanted (especially the similarities to the postback model). Your implementation is far more complete and comprehensive. Great work!
 
-Steven

QuestionHow to create a decrease button as well!? While keeping the increase button!membersim08531 Dec '04 - 2:01 
Cry | :(( I don't know if it is too late to ask!!
 
but if I want to place a decrease button as well, what should I do!!
 
I tried just creating the button and pass different data, but for some reason it does not want to work!!
 
could anyone pleas provide a simple example?
tahnsk in advance!
 


 
Simon

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 3 Aug 2004
Article Copyright 2004 by Elvin Cheng
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid