Check this out: http://atlas.asp.net/docs/util/srcview.aspx?path=~/atlas/samples/services/WebMethodOnPage.src.
Note that, within the .aspx page source, there is a method written in C# decorated with the [WebMethod] attribute. What this means is that you have a standard .aspx page hosting a webmethod, which is typically exposed only on .asmx (webservice) files. What's going on here?
As it turns out, this is a new feature in Atlas and it utilizes an unsupported method exposed within the ASP.Net runtime: Control.SetRenderMethodDelegate. The documentation is fairly self-explanatory:
Assigns an event handler delegate to render the server control and its content into its parent control.
What this means is that you can inject your own handler function to render any control or page in the runtime, thereby circumventing its own rendering code: extremely powerful, albeit sort of a hack. Now, the way this works in the Altas framework is this:
- The script module is loaded by the ASP.Net runtime due to its entry in the web.config file for the loading application.
- The .Init() method is called on the module, and it assigns a delegate for the PostMapRequestHandler event, OnPostMapRequestHandler.
- OnPostMapRequestHandler gets called and calls Microsoft.Web.Atlas.PageServiceHandler.HookUpPage() with the page the event was raised for as a parameter.
- The PageServiceHandler class registers its own PreRenderComplete event handler for the PreRenderComplete event for the page passed to it from earlier.
- The page goes through its normal life cycle and, right after the pre-render stage calls the PreRenderComplete handler method within the PageServiceHandler class.
- PageServiceHandler's implementation of PreRenderComplete includes calling .SetRenderMethodDelete() on the page if the page has, within its form post variables, the string "__serviceMethodName". The method passed as the handler is .Render from within PageServiceHandler.
- PageServiceHandler.Render gets called when the page is requested to render itself, and the implementation of said method is below:
private void Render(HtmlTextWriter output, Control container) { Page page1 = (Page) container; WebServiceData data1 = WebServiceData.GetWebServiceData(page1.AppRelativeVirtualPath); string text1 = page1.Request.Form["__serviceMethodName"]; WebServiceMethodData data2 = data1.GetMethodData(text1); string text2 = page1.Request.Form["__serviceMethodParams"]; try { IDictionary<string, object> dictionary1 = JavaScriptObjectDeserializer.DeserializeDictionary(text2); object obj1 = data2.CallMethodFromRawParams(page1, dictionary1); string text3 = JavaScriptObjectSerializer.Serialize(obj1, data2.Owner); output.Write(text3); } catch (Exception exception1) { HttpContext context1 = HttpContext.Current; context1.Response.StatusCode = 500; context1.Response.StatusDescription = HttpWorkerRequest.GetStatusDescription(500); RestHandler.WriteExceptionJsonString(context1, output, exception1); } }
Look carefully at the implementation and see the method data2.CallMethodFromRawParams() and the output.Write(), which combine to push out to the response stream the serialized HTTP response from the webmethod. The rabbit hole goes deeper and deeper, but it really does us no good to go much further as we know how to look for methods based on their attributes and dynamically execute them from within the .Net runtime. The secret has been cracked, effectively, and we see how they're intercepting the normal request for an .aspx page and re-routing it to a new Render handler which calls the proper method in the page class.
It is the responsibility of the client framework, then, to include within the form post variables the ever-important __serviceMethodName variable. If you take a Fiddler trace of a request to the webmethod using one of the Microsoft samples, you can see it clearly in there:
POST /docs/atlas/samples/services/WebMethodOnPage.aspx HTTP/1.1 Accept: */* Accept-Language: en-us Referer: http://atlas.asp.net/docs/atlas/samples/services/WebMethodOnPage.aspx Content-Type: application/x-www-form-urlencoded UA-CPU: x86 Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322) Host: atlas.asp.net Content-Length: 166 Proxy-Connection: Keep-Alive Pragma: no-cache Cookie: .....
__serviceMethodName=HelloWorld&__serviceMethodParams={"s":"asdfasdf"}&__VIEWSTATE=/wEPDwUKMjA4Mjk0NDk4M2Rk&nameTextBox=asdfasdf&__EVENTVALIDATION=/wEWAgLlssAyAouxhI4H
Alright, you saw in the last blog entry how to get the Atlas client framework to download script that wraps a webservice reference:
<atlas:ScriptManager ID="scriptManager" runat="server" EnableScriptComponents="true" > <Services> <atlas:ServiceReference Path="SimpleService.asmx" /> </Services> </atlas:ScriptManager>
The question now is how does everything tie together? We have reference to a web service here, but that's about it. Bare in mind that the <atlas:ScriptManager> tag is processed server-side instead of client-side, so the ASP.Net runtime is responsible for processing it and not the clientside Atlas framework. What the clientside Atlas framework gets looks like this:
<page xmlns:script="http://schemas.microsoft.com/xml-script/2005"> <references> <add src="SimpleService.asmx/js" /> </references> <components /> </page>
...this is what the Atlas framework sees. Now, in a later blog post I'll go into the details of how the client framework parses and builds the script out of the XML script block in general, but for now I'll lightly touch on the topic enough to answer how the JavaScript gets downloaded to the client.
There is a class called Sys.MarkupParser whose responsibility is to deal with markup and, well, parse it. It has a method called _processXMLScript whose responsibility it is to, well, processXMLScript. The method is relatively simple (suprisingly) and looks like this:
this._processXMLScript = function(markupContext, references, componentNodes, completionHandler) { var xmlScriptContext = { markupContext: markupContext, references: references, componentNodes: componentNodes, completionHandler: completionHandler };
if (references && references.length) { var scriptLoader = new Sys.ScriptLoader(); scriptLoader.load(references, Function.createDelegate(this, this._processXMLScriptComponents), xmlScriptContext); } else { this._processXMLScriptComponents(xmlScriptContext); } }
...the section to take note of is, obviously, the one that instantiates the class Sys.ScriptLoader(). It takes all references parsed out of the references block above and snags them. The most pertinent chunk of code from the .load() method above looks like this:
if (_references.length) { var reference = _references.dequeue(); var scriptElement = document.createElement('script'); _currentLoadingReference = scriptElement;
if (Sys.Runtime.get_hostType() != Sys.HostType.InternetExplorer) { scriptElement.readyState = 'loaded'; scriptElement.onload = loadReferences; } else { scriptElement.onreadystatechange = loadReferences; } scriptElement.type = 'text/javascript'; scriptElement.src = reference;
var headElement = document.getElementsByTagName('head')[0]; headElement.appendChild(scriptElement);
return; }
See that the scriptElement.src is set to the 'reference' object, which is a string in this case that points to the web service URI with the "/js" appended. What this means is that the browser references this script as a "src" and therefore is responsible for downloading and loading the script into the script runtime, not the Atlas framework itself. When this URI is hit, the Atlas runtime on the server generates the script and everything links up appropriately.
Happy coding.
This will be a multi-part blog post 'cause the topic is rather extensive. This first part will describe the basics of calling web services via Atlas from the pure programmatic standpoint, plus a bit about how web proxies work in JavaScript. The other entries will go deeper.
First off, visit this Url: http://atlas.asp.net/docs/atlas/samples/services/SimpleService.asmx. What you see here is the nicey user interface wrapping this: http://atlas.asp.net/docs/atlas/samples/services/SimpleService.asmx?WSDL, the service description. If you're a web developer dealing with services, you've seen this before. No big deal.
If you append "/js" to the end of the first querystring, you see something new: http://atlas.asp.net/docs/atlas/samples/services/SimpleService.asmx/js. This is javascript; more interestingly, JavaScript wrapping the methods of the web service itself. After nicely beautifying the script, it looks like this:
Type.registerNamespace('Quickstart.Samples'); Quickstart.Samples.SimpleService=new function() { this.path = "http://atlas.asp.net/docs/atlas/samples/services/SimpleService.asmx"; this.appPath = "http://atlas.asp.net/docs/"; var cm=Sys.Net.ServiceMethod.createProxyMethod; cm(this,"EchoString","s"); }
Don't worry too much about the internals of the script above as I'll go into this later (in another post). But, what you've just seen here is that the Atlas framework is capable of dynamically generating javascript to wrap the methods of the web service (the web service this code wraps has one method, EchoString, with one parameter "s"). Interacting with this downloaded code, then, is as easy as doing this:
function OnbuttonGo_click() { //Call script proxy passing the input element data requestSimpleService = Quickstart.Samples.SimpleService.EchoString( document.getElementById('inputName').value, //params OnComplete, //Complete event OnTimeout //Timeout event ); return false; }
Notice the highlighted method call syntax above. It's really just that easy.
The next question is how does the Atlas client runtime know what services to retrieve proxies for; and the answer lies within the ScriptManager control:
<atlas:ScriptManager ID="scriptManager" runat="server" EnableScriptComponents="true" > <Services> <atlas:ServiceReference Path="SimpleService.asmx" /> </Services> </atlas:ScriptManager>
The profile service in Atlas is implemented in much the same way that the authentication service is - through a dynamically instantiated web service class via a registered HttpHandlerFactory object (see the previous post). The clientside Atlas runtime hooks up with the profile bits on the server using web services, etc asynchronously and in the background. I noticed an interesting bit of coding sugar in Javascript while exploring the profile classes, however, that I thought was particularly interesting. The profile bits are stored inside the Sys.Profile object in an array called "properties". You can get, say, the 'FirstName' from the profile information of the current, executing user like this:
Sys.Profile.properties["FirstName"]
The "cool" part, however, is that you can also access it this way to:
Sys.Profile.properties.FirstName
Now how is this possible?
As it turns out, each object maintains an associative array of fellow objects (an array of associations), so that you can access sub-objects using array indexers or, as you see above, access array indices using standard OO "property" syntax - effectively, they're one in the same. You may have seen this when you try to access the main form of a web page in script, you can do it (if the main form were called Form1) programmatically either way: document.forms['Form1'] or document.forms.Form1; either way gives you a handle to the Form1 object. Syntactic sugar, yes, but it has a very interesting application as I discovered when going back over the code for the authentication script....
....say you wanted to create a method with a dynamic name and a dynamic parameter list. How would you go about it? Well, given that a function is an object in JavaScript, and that we can create objects using associative arrays, then, technically we can simply create a new function using an array-like syntax. If, in Javascript, you wanted to create a proxy object that encapsulated and "felt" like you're procedurally calling methods on a web service so that the syntax of calling a web method looked like this:
Sys.Services.AuthenticationService.login(username.value, password.value, false, OnLoginComplete);
Then you could dynamically create the "login" method at runtime like this:
proxy["login"] = function() { ... return; }
You're simply creating a new object at runtime and assigning it to the parent object; it just so happens that our dynamically created object is a function. This is extraordinarily powerful when coupled with automatic discovery of methods and method parameters at runtime - how else could you build a function that felt like calling a web service method when you didn't know, until runtime, what the method would look like?
Now, what about parameters? Bare in mind that you can always access "extra" parameters passed to a javascript function through the arguments object of a function. So, even though the "login" method above has no parameters, if you pass them, then they're accessible to the method implementation through the arguments array:
proxy["login"] = function() { var username = arguments[0]; var password = arguments[1]; ... return; }
Addendum:
This is nothing special with the Atlas framework itself, but is actually a part of the javascript language. Some of my posts relate to simply showing off those features for those who may not know so that the Atlas framework makes more sense when studying it.
Here, check this out:
When watching a Fiddler dump of the http request/response pattern of the Microsoft sample on Http Forms Authentication in Atlas, you see the following resource being requested:
http://atlas.asp.net/docs/atlas/samples/formsandmem_cs/ScriptServices/Microsoft/Web/Services/Standard/AuthenticationWebService.asmx. Have a look:
POST /docs/atlas/samples/formsandmem_cs/ScriptServices/Microsoft/Web/Services/Standard/AuthenticationWebService.asmx?mn=login HTTP/1.1 Accept: */* Accept-Language: en-us Referer: http://atlas.asp.net/docs/atlas/samples/formsandmem_cs/Default.aspx Content-Type: application/json UA-CPU: x86 Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322) Host: atlas.asp.net Content-Length: 75 Proxy-Connection: Keep-Alive Pragma: no-cache Cookie: ...
Now, what's interesting about this is that if you actually follow the link (try it), you'll see that you're spat back a 404 - not found error from the web server. However, this is the webservice that gets communicated with for all forms authentication; it literally maintains the interface that all of the Atlas framework expects for this authentication mechanism (the login, verification, et al). How can this web service not exist, but be available for communication? When you see a request for a resource that doesn't exist on disk, you immediately become aware that some sort of magic is happening, and typically the party responsible for said magic is either some sort of Isapi module/extension or an HttpModule/Handler. All of the code for ASP.Net Atlas is stored within a single .Net assembly called Microsoft.Web.Atlas; which rules out the ISAPI idea, leaving us with HttpModules/Handlers.
Inspect the web.config file for your Asp.Net Atlas application and take note of the following:
<httpHandlers> <remove verb="*" path="*.asmx"/> <add verb="*" path="*.asmx" type="Microsoft.Web.Services.ScriptHandlerFactory" validate="false"/> <!-- The MultiRequestHandler enables multiple requests to be handled in one roundtrip to the server. Its use requires Full Trust. --> <add verb="*" path="atlasbatchcall.axd" type="Microsoft.Web.Services.MultiRequestHandler" validate="false"/> <add verb="*" path="atlasglob.axd" type="Microsoft.Web.Globalization.GlobalizationHandler" validate="false"/> <!-- The IFrameHandler enables a limited form of cross-domain calls to 'Atlas' web services. This should only be enabled if you need this functionality and you're willing to expose the data publicly on the Internet. To use it, you will also need to add the attribute [WebOperation(true, ResponseFormatMode.Json, true)] on the methods that you want to be called cross-domain. This attribute is by default on any DataService's GetData method. <add verb="*" path="iframecall.axd" type="Microsoft.Web.Services.IFrameHandler" validate="false"/> --> <add verb="*" path="*.asbx" type="Microsoft.Web.Services.ScriptHandlerFactory" validate="false"/> </httpHandlers>
...I intentionally highlighted the .asmx handler, Microsoft.Web.Services.ScriptHandlerFactory. If you note, the resource being requested is a web service (with the .asmx extension). The way ASP.Net works is to inspect the configuration files (machine.config, web.config) for handlers registered for particular extensions and to insert those handlers into the request/response pipeline. The statement above effectively sticks the Microsoft.Web.Services.ScriptHandlerFactory type into the pipeline for responding to requests for the .asmx extension. Now, this data type exists within the Microsoft.Web.Atlas assembly mentioned earlier, so I cracked open Lutz Roeder's most excellent .Net Reflector and peered inside (yes, I love doing this - in case you haven't noticed).
ASP.Net sees that the type registered for .asmx is the one in the config file, it locates it, loads it into the runtime, JITS it up and requests the method ::GetHandler from the instance it creates. This method is suppose to do inspection on the query string and return the correct HttpHandler object (web form, web service, etc) that is expected to generate the output stream content for the runtime to send back to the client. What gets registered in the config file is either that handler itself (IHttpHandler), or the handler factory (IHttpHandlerFactory). What's registered above is the handler factory. That means this class returns a type that is responsible for handling the .asmx request. What all this boils down to is that the object doesn't really need to exist on disk since, at this point, it's up to the handler to locate and return the resource requested. As it turns out, the type is actually stored within Microsoft.Web.Atlas itself:
/Microsoft/Web/Services/Standard/AuthenticationWebService.asmx resolves to the datatype Microsoft.Web.Services.Standard.AuthenticationWebService housed within the Microsoft.Web.Atlas assembly which inherits from System.Web.Services.WebService (which is a valid http handler that can service the web request). What this means is that the factory class above parses the query string and returns an instance of the right data type to the runtime for execution. The actual method which does all the magic is called GetWebServiceData within the WebServiceData class (also in the Microsoft.Web.Atlas assembly). Decompiled it looks like this:
internal static WebServiceData GetWebServiceData(string virtualPath, bool failIfNoData) { virtualPath = VirtualPathUtility.ToAbsolute(virtualPath); Type type1 = WebServiceData._mappings[virtualPath] as Type; bool flag1 = false; if (type1 == null) { flag1 = HostingEnvironment.VirtualPathProvider.FileExists(virtualPath); if (flag1) { type1 = BuildManager.GetCompiledType(virtualPath); } } if ((type1 == null) && !flag1) { string text1 = null; int num1 = virtualPath.IndexOf("ScriptServices/"); if (num1 != -1) { num1 += "ScriptServices/".Length; text1 = virtualPath.Substring(num1, (virtualPath.Length - num1) - 5); text1 = text1.Replace('/', '.'); type1 = typeof(WebServiceData).Assembly.GetType(text1, false, true); if (type1 == null) { type1 = BuildManager.GetType(text1, false, true); } } else { try { text1 = Path.GetFileNameWithoutExtension(virtualPath); text1 = WebServiceData.DecryptString(text1); type1 = Type.GetType(text1); } catch { } if (type1 != null) { WebServiceData._mappings[virtualPath] = type1; WebServiceData._typeVirtualPath[type1] = virtualPath; } } } if (type1 != null) { return WebServiceData.GetWebServiceData(type1); } if (failIfNoData) { throw new InvalidOperationException(); } return null; }
Note the parsing out of the string "/ScriptServices" and the replacement of forward slash (/) with period (.) to get the fully-qualified path to the data type for use in the reflection services of the runtime. The type found in this method eventually makes it way up the call chain to be instantiated and returned from the handler factory to the runtime. The result is that it, then, processes the web service page and, it executes, giving back the proper bits of data for Forms Authentication.
Happy coding,
Woah! Yesterday my blog got hit 217 times. Thanks.....
Yes, again, I'm digging into these things....if you'd like to see the ASP.Net "Atlas" sample I'm referring to in this blog post, it's located here.
Notice that the only supported form of authentication in Atlas is Forms (meaning that Windows or "Kerberos/NTLM" and Passport authentication are missing). I'm no expert in Kerberos-based authentication, but I imagine that passing along the ticket in a secure manner might be difficult or impractical via JavaScript, and I imagine the Passport authentication scheme might be rather odd too when implemented in JavaScript. Regardless of why, the only valid authentication scheme supported by Atlas at the moment is Forms; meaning you're dealing with cookies (which is simple for JavaScript to access - another reason why this form was probably chosen).
Three methods are supported by the Sys.Services.AuthenticationService class:
- validateUser()
- login()
- logout()
In the next blog post I'm going to explain how these methods work under the hood (within the script), or I'm going to at least try to - but for now, I'm going to simply show how they work programmatically (how you use them). What each function does is rather self-explanatory - login() logs a user in and adds the forms authentication cookie to the browser, validateUser() validates a user's login status by examining the forms authentication cookie and logout() removes the forms authentication cookie from the browser and effectively logs the user out. The whole system utilizes partial postbacks and so, therefore, is done asynchronously with the client experience.
The following script call then,
Sys.Services.AuthenticationService.login(username.value, password.value, false, OnLoginComplete);
This is a Fiddler trace of the process occuring on the Microsoft sample:
REQUEST:
POST /docs/atlas/samples/formsandmem_cs/ScriptServices/Microsoft/Web/Services/Standard/AuthenticationWebService.asmx?mn=login HTTP/1.1 Accept: */* Accept-Language: en-us Referer: http://atlas.asp.net/docs/atlas/samples/formsandmem_cs/Default.aspx Content-Type: application/json UA-CPU: x86 Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322) Host: atlas.asp.net Content-Length: 75 Proxy-Connection: Keep-Alive Pragma: no-cache Cookie: CommunityServer-UserCookie605740=lv=4/29/2006 8:20:41 PM&mra=5/1/2006 2:18:26 PM; .ASPXANONYMOUS=6mqTPsGZxgEkAAAAOGY3MTdhMWMtNTZkNS00YTVjLTkwMmQtYmYyMzRkZmFkMjAzT5esZ6m9p6Ha5oC1KM8c5LDCmx41; ASP.NET_SessionId=apbic1njfd45gk45x510wa45; .CommunityServer=....
{"userName":"myusername","password":"mypassword!","createPersistentCookie":false}
RESPONSE:
HTTP/1.1 200 OK Cache-Control: private, max-age=0 Date: Tue, 02 May 2006 20:52:23 GMT Content-Type: application/json; charset=utf-8 Server: Microsoft-IIS/6.0 X-Powered-By: ASP.NET X-AspNet-Version: 2.0.50727 Set-Cookie: .CommunityServer=... path=/; HttpOnly Vary: Accept-Encoding
true
Notice the "true" returned on the body of the HTTP response. If the login attempt was a failure, the return would be "false". The login method simple executes asynchronously and, in the completion event handler, lets the code know what this return value was. Your clien-side experience takes suit from this.
Now, arguably, logging in and handling all the validation on the client may not be the best choice to make in all circumstances since there may be data you don't want in the HTML; but for simple UI changes, etc. I can see the benefit of clientside forms authentication. Personally, I see the most benefit in the validateUser() ethod when having the client render itself. Also note that no data is encrypted (the username/password combo passed to the server).
I was dissapointed, quite dissapointed actually, when I realized that the sample I was looking at was enabling clientside paging but without partial postbacks to retrieve more data; meaning that even though the client was paging data, it was requesting all of the data initially and paging through it once it arrived. I'm not sure if there is a built-in way around this, but it kind of sucks to think that all the data has to be brought down to the client first. Anyway...again, I got curious about how it all worked....
...it all starts off with a <listView> and a <dataSource> object:
<dataSource id="dataSource" autoLoad="true" serviceURL="SimpleDataService1.asmx" /> <listView id="dataContents" itemTemplateParentElementId="masterTemplate" >.....
You've seen this before, it's nothing amazingly new. One thing that does strike us as interesting, though, is that the binding for the <listView> control doesn't point directly to the <dataSource> object, but instead points to a new object named "view", and that the datapath is now "filteredData".
<bindings> <binding dataContext="view" dataPath="filteredData" property="data"/> </bindings>
What's going on here is that our binding object is pointing to something called a dataView object, which, in turn has its own dataBind object that points to the dataSource:
<dataView id="view" pageSize="12"> <bindings> <binding dataContext="dataSource" dataPath="data" property="data" /> </bindings> </dataView>
The binding path goes like this, then:
Web Service -> DataSource -> Binding -> DataView -> ListView
The reason for all this middle-man object stuff is that the datView provides the paging capabilities for the listView, and therefore presents the listView with a "view" on the data for its binding. The paging capabilities are supported by the dataNavigator object:
<dataNavigator id="pageNavigator" dataView="view"/>
Which hooks itself up to the dataView object via the dataView property. So, up to now, we have the web service and the datasource object which does all the serialization for the JSON response, the binding which binds the dataView to the JSON-created object, and the listview. The dataNavigator sits aside and generates events that tells the DataView to change the data it prevents to the ListView and to therefore refresh the contents of the listView. The system is callback-based, you create buttons that send events to the dataNavigator to go to the next page, previous page, last page, etc. and it responds by calling the proper methods on the dataView object:
this.raiseBubbleEvent = function(source, args) { var currentTarget = this.get_parent(); while (currentTarget) { if (currentTarget.onBubbleEvent(source, args)) { return; } currentTarget = currentTarget.get_parent(); }
...look closely and see the reference to this.get_parent(); The routine above is the event called when the following button is clicked:
<button id="firstPageButton" parent="pageNavigator" command="FIRSTPage"> </button>
The parent points to the pageNavigator object (declared earlier) and the command is packaged into the args of the onBubbleEvent method. The parent, in this case, is the dataNavigator object whose .onBubbleEvent method looks like this:
this.onBubbleEvent = function(source, args) { if (!_data) return false; var cmd = args.get_commandName().toLowerCase(); switch(cmd) { case "page": var arg = args.get_argument(); if (arg && String.isInstanceOfType(arg)) { arg = Number.parse(arg); } if (arg || arg == 0) { _data.set_pageIndex(arg); return true; } break;
case "nextpage": _data.set_pageIndex(_data.get_pageIndex() + 1); return true;
case "previouspage": var idx = _data.get_pageIndex() - 1; if (idx >= 0) { _data.set_pageIndex(idx); } return true;
case "firstpage": _data.set_pageIndex(0); return true;
case "lastpage": _data.set_pageIndex(_data.get_pageCount() - 1); return true; } return false; }
The point of this method is to call the appropriate methods on the dataView object to change its view on the data for the listView. This means that if you click the button whose command is to go to the next page, the event is bubbled upwards to the dataNavigator object who calls the dataView object's corresponding "nextpage" case above, which calls _data.set_pageIndex() for the next page via the dataView object. This method refreshes the content of the listView and you, effectively, get the 'next page' of data.
....again, though, this doesn't require any partial postbacks to the server, so even though the paging is quick, it's not nearly as efficient with memory consumption as I had hoped it would be.
|