With my plate relatively clear, I have decided to spend some time investigating the released version of Microsoft AJAX Extensions for ASP.NET 2.0 (formarly Atlas and from now on I will call it MS AJAX). Specifically, I want to see what advantages and disadvantages I would have by utilizing this framework instead of (or addition to) the ClientAPI. One of the first things that I looked for was the ability to make "callbacks". I had heard about Atlas' ability to invoke webservices for some time now and have been on a project that was IE specific and therefore was able to use the WebService behavior to accomplish the same thing. While this functionality is cool, people who know me, know that my real passion is for writing reusable webcontrols. Writing a webcontrol that needs to communicate with the server
- Should not require the end user to expose a webservice.
- Communication to the server should allow the control be able to raise events in the context of the current page.
In my opinion, these two requirements are vital to writing webcontrols.
My initial look through the documentation I found little that would assist me in the answer (eventually I found this last sample). Searching google yielded some promising results. Apparently, there is this concept called PageMethods that appeared to be able to make a callback from the client to the server without the need for a separate webservice. After digging through several sites explaining their use and lack of documentation I soon learned that through the various releases of this feature, quite a few changes were made. Two of these changes are the need to also decorate your method with a ScriptMethod
attribute (though when I try it, this does not seem necessary any more), and the fact that in the later betas these methods are now required to be static!
It appears that PageMethods will satisfy my first requirement (not needing external webservices), however, the second is not met. For we need the context of the current page to be useful. To understand what I mean, lets take the DNNLabelEdit control. A developer can place this on any page or module (for DNN), handle an event (UpdateLabel) and successfully have access to all it needs to update the database (ModuleId for example). If we don't have the context of the page, we will need the information posted to the server through the callback. The control cannot make assumptions on what should and shouldn't be posted, so this leaves it up to the developer.
One of the reasons for the change in requiring static/shared methods is stated here
"They reason the AJAX team decided to make these methods static was so you could avoid page-lifecycle issues and subtleties, such as the inability to modify or access page state"
ASP.NET 2.0 out of the box supports callbacks. These same issues are present, yet they still support the feature! I know, for I have researched and documented them when dealing with the DNNTabStrip control. I suppose I am not as upset as I could be, especialy if I had tried to adopt Atlas earlier and had developed solutions requiring this functionality like others have commented in the blogs listed above. It is looking like I will be continuing the use of the ClientAPI's callback functionality. Note: I could choose to use the ASP.NET 2.0 callback functionality, but I don't think it worth moving, since my callback implementation has a few more enhancments included.
Some people have questioned why even bother with PageMethods if they have to be static. They argue they are just like webservices. The fact that they appear to satisfy my first requirement makes them different.
Two final notes before moving on.
First, I am readily admitting that I just started researching MS AJAX, and therefore may not have all my facts straight. If someone knows another way that MS AJAX solves my requirements above, please comment or send me an email.
Finally, I see many people who decide to use PageMethods do so by setting the ScriptManager's EnablePageMethods property to true. I would highly recommend against this. While it makes your invoking of the method simple, it generates a large amount of payload sent down to the client. To see what I mean, I am listing the output generated within the page for only two simple methods (Add and Add2)
var PageMethods = function() {
PageMethods.initializeBase(this);
this._timeout = 0;
this._userContext = null;
this._succeeded = null;
this._failed = null;
}
PageMethods.prototype = {
Add:function(x,y,succeededCallback, failedCallback, userContext) {
return this._invoke(PageMethods.get_path(), 'Add',false,{x:x,y:y},succeededCallback,failedCallback,userContext); },
Add2:function(x,y,succeededCallback, failedCallback, userContext) {
return this._invoke(PageMethods.get_path(), 'Add2',false,{x:x,y:y},succeededCallback,failedCallback,userContext); }}
PageMethods.registerClass('PageMethods',Sys.Net.WebServiceProxy);
PageMethods._staticInstance = new PageMethods();
PageMethods.set_path = function(value) {
var e = Function._validateParams(arguments, [{name: 'path', type: String}]); if (e) throw e; PageMethods._staticInstance._path = value; }
PageMethods.get_path = function() { return PageMethods._staticInstance._path; }
PageMethods.set_timeout = function(value) { var e = Function._validateParams(arguments, [{name: 'timeout', type: Number}]); if (e) throw e;
if (value < 0) { throw Error.argumentOutOfRange('value', value, Sys.Res.invalidTimeout); }
PageMethods._staticInstance._timeout = value; }
PageMethods.get_timeout = function() {
return PageMethods._staticInstance._timeout; }
PageMethods.set_defaultUserContext = function(value) {
PageMethods._staticInstance._userContext = value; }
PageMethods.get_defaultUserContext = function() {
return PageMethods._staticInstance._userContext; }
PageMethods.set_defaultSucceededCallback = function(value) {
var e = Function._validateParams(arguments, [{name: 'defaultSucceededCallback', type: Function}]); if (e) throw e;
PageMethods._staticInstance._succeeded = value; }
PageMethods.get_defaultSucceededCallback = function() {
return PageMethods._staticInstance._succeeded; }
PageMethods.set_defaultFailedCallback = function(value) {
var e = Function._validateParams(arguments, [{name: 'defaultFailedCallback', type: Function}]); if (e) throw e;
PageMethods._staticInstance._failed = value; }
PageMethods.get_defaultFailedCallback = function() {
return PageMethods._staticInstance._failed; }
PageMethods.set_path("/MSAjax10/PageMethods.aspx");
PageMethods.Add= function(x,y,onSuccess,onFailed,userContext) {PageMethods._staticInstance.Add(x,y,onSuccess,onFailed,userContext); }
PageMethods.Add2= function(x,y,onSuccess,onFailed,userContext) {PageMethods._staticInstance.Add2(x,y,onSuccess,onFailed,userContext); }
While it is nice to be able to invoke these methods like
PageMethods.Add(1,2, methodComplete, methodError);
The amount of overhead for each page request is not worth it IMO.
Instead I recommend calling the method like this
Sys.Net.WebServiceProxy.invoke('/MSAjax10/PageMethods.aspx', 'Add', false, { x:1, y:2 }, methodComplete, methodError);