Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 156 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
156
Dung lượng
898,32 KB
Nội dung
Chapter 11: Data Classes this._onCollectionChanged(Sys.Preview.NotifyCollectionChangedAction.Remove, row); this._onPropertyChanged(“length”); if (oldIsDirty !== this.get_isDirty()) this._onPropertyChanged(“isDirty”); } The Remove method takes a JavaScript object as its argument The object can be a DataRow or a row object Remove first checks whether this object is a DataRow If so, it calls the get_rowObject method on the DataRow object to return a reference to its associated row object: if (Sys.Preview.Data.DataRow.isInstanceOfType(rowObject)) rowObject = rowObject.get_rowObject(); Next, the Remove method calls the get_isDirty method to return and store the current value of the isDirty property in a local variable named oldIsDirty for future reference: var oldIsDirty = this.get_isDirty(); This is done because the code following this line of code could change the current value of this property: The Remove method then determines the index of the row object in the _array array, which contains all row objects associated with the DataRows object of the current DataTable object: var index = Array.indexOf(this._array, rowObject); Next, the Remove method calls the getItem method, passing in the index of the row object to return a reference to DataRow object associated with the row object: var row = this.getItem(index); It then calls the removeAt method to remove the row object from the _array array: if(typeof(this._array.removeAt) === “function”) this._array.removeAt(index); else Array.removeAt(this._array, index); Next, it invokes the removeAt static method on the Array class to remove the DataRow object from the _rows array, which contains all the DataRow objects that the current DataTable owns: Array.removeAt(this._rows, index); It then checks whether the _newRows array contains the row object; and if so, it removes the row object from this array as well: index = Array.indexOf(this._newRows, rowObject); if (index !== -1) Array.removeAt(this._newRows, index); else Array.add(this._deletedRows, rowObject); 431 c11.indd 431 8/20/07 8:14:21 PM Chapter 11: Data Classes Next, it calls the internal _set_state method on the DataRow object to change its state to Deleted: row._set_state(Sys.Preview.Data.DataRowState.Deleted); As you can see, _set_state is an internal method and you should not directly use this method in your own code Then, the Remove method calls the _onCollectionChanged method to raise the collectionChanged event: this._onCollectionChanged(Sys.Preview.NotifyCollectionChangedAction.Remove, row); This is expected because the Remove method is removing a row object from the _array collection Next, the Remove method calls the _onPropertyChanged method to raise the propertyChanged event for the length property of the DataTable object: this._onPropertyChanged(“length”); Again this is expected because the Remove method is removing a row object from the _array collection and, consequently, changing the length of the collection Finally, the Remove method calls the get_isDirty method to access the current value of the isDirty property of the DataTable object and compares this value with the old value If they are different, it calls the _onPropertyChanged method to raise the propertyChanged event for the isDirty property: if (oldIsDirty !== this.get_isDirty()) this._onPropertyChanged(“isDirty”); Descriptor As Listing 11-20 shows, the DataTable class exposes a static property named descriptor, which enables its clients to use the ASP.NET AJAX type inspection capabilities to inspect its members at runtime Listing 11-20: The descriptor Property of the DataTable Class Sys.Preview.Data.DataTable.descriptor = { properties: [ { name: ‘columns’, type: Array, readOnly: true }, { name: ‘keyNames’, type: Array, readOnly: true }, { name: ‘length’, type: Number, readOnly: true }, { name: ‘isDirty’, type: Boolean, readOnly: true } ], methods: [ { name: ‘add’ }, { name: ‘clear’ }, { name: ‘remove’ } ], events: [ { name: ‘collectionChanged’, readOnly: true }, { name: ‘propertyChanged’, readOnly: true } ] } The descriptor property of the DataTable class is set to an object literal that contains the following three name/value pairs: 432 c11.indd 432 8/20/07 8:14:22 PM Chapter 11: Data Classes ❑ The first name/value pair describes the properties of the DataTable class The name part of the name/value pair is properties, and the value part is an array of four object literals that describe the columns, keyNames, length, and isDirty properties of the DataTable class Each object literal itself contains three name/value pairs, where the first pair specifies the name of the property, the second pair describes the type of the property, and the last pair specifies whether the property is editable properties: [ { name: ‘columns’, type: Array, readOnly: true }, { name: ‘keyNames’, type: Array, readOnly: true }, { name: ‘length’, type: Number, readOnly: true }, { name: ‘isDirty’, type: Boolean, readOnly: true } ] ❑ The second name/value pair describes the methods of the DataTable class The name of the pair is methods, and the value is an array of three object literals that describe the add, clear, and remove methods of the DataTable class: methods: [ { name: ‘add’ }, { name: ‘clear’ }, { name: ‘remove’ } ], ❑ The third name/value pair describes the events of the DataTable class The name part of the pair is the keyword events, and the value part is an array of two object literals that describe the collectionChanged and propertyChanged events of the DataTable class: events: [ { name: ‘collectionChanged’, readOnly: true }, { name: ‘propertyChanged’, readOnly: true } ] As Listing 11-21 shows, the DataTable class exposes three getter methods named get_columns, get_keyNames, and get_isDirty that you can invoke to access the values of the columns, keyNames, and isDirty properties of a given DataTable object Listing 11-21: The get_columns, get_keyNames, and get_isDirty Getter Methods function Sys$Preview$Data$DataTable$get_columns() { return this._columns; } function Sys$Preview$Data$DataTable$get_keyNames() { if (!this._keys) { this._keys = []; var len = this._columns.length; for (var i = 0; i < len; i++) { var col = this._columns[i]; if (col.get_isKey()) Array.add(this._keys, col.get_columnName()); } (continued) 433 c11.indd 433 8/20/07 8:14:22 PM Chapter 11: Data Classes Listing 11-21 (continued) } return this._keys; } function Sys$Preview$Data$DataTable$get_isDirty() { return (this._deletedRows.length !== 0) || (this._newRows.length !== 0) || (this._updatedRows.length !== 0); } The DataTable object properties that the getter methods expose include the following: ❑ The columns property is an array that contains all the DataColumn objects that the DataTable owns ❑ The keyNames property is an array that contains the column names of all DataColumn objects that represent the primary key data fields of the data table that the DataTable represents ❑ The isDirty property is a Boolean value that specifies whether the DataTable object is dirty A DataTable object is considered dirty when one or more of the following arrays contains one or more row objects: ❑ ❑ _deletedRows: This array contains the row objects associated with the deleted DataRow objects of the DataTable object _newRows: This array contains the row objects associated with the newly added DataRow objects ❑ _updatedRows: This array contains the row objects associated with the updated DataRow objects return (this._deletedRows.length !== 0) || (this._newRows.length !== 0) || (this._updatedRows.length !== 0); INotifyPropertyChange The boldface portion of the following code fragment shows how the DataTable class implements the INotifyPropertyChange interface discussed in the previous chapters: Sys.Preview.Data.DataTable.registerClass(‘Sys.Preview.Data.DataTable’, null, Sys.Preview.Data.IData, Sys.INotifyPropertyChange, Sys.Preview.INotifyCollectionChanged, Sys.IDisposable); Listing 11-22 presents the DataTable class’s implementation of the members of the INotifyPropertyChange interface This interface exposes the following two methods: 434 c11.indd 434 8/20/07 8:14:22 PM Chapter 11: Data Classes ❑ add_propertyChanged: This method adds the specified method as an event handler for the propertyChanged event ❑ remove_propertyChanged: This method removes the specified method from the list of event handlers registered for the propertyChanged event Listing 11-22: The DataTable Class’s Implementation of the INotifyPropertyChange Interface function Sys$Preview$Data$DataTable$get_events() { if (!this._events) this._events = new Sys.EventHandlerList(); return this._events; } function Sys$Preview$Data$DataTable$add_propertyChanged(handler) { this.get_events().addHandler(“propertyChanged”, handler); } function Sys$Preview$Data$DataTable$remove_propertyChanged(handler) { this.get_events().removeHandler(“propertyChanged”, handler); } function Sys$Preview$Data$DataTable$_onPropertyChanged(propertyName) { var handler = this.get_events().getHandler(“propertyChanged”); if (handler) handler(this, new Sys.PropertyChangedEventArgs(propertyName)); } The DataTable class’s implementation of the propertyChanged event follows the event implementation pattern discussed in the previous chapters As previously discussed, implementing an event requires an ASP.NET AJAX class to support a private field of type EventHandlerList named _events where the event handlers registered for the events of the class will be stored The class must also expose a getter method named get_events that returns a reference to this EventHandlerList object: function Sys$Preview$Data$DataTable$get_events() { if (!this._events) this._events = new Sys.EventHandlerList(); return this._events; } The DataTable class’s implementation of the add_propertyChanged method of the INotifyPropertyChange interface first calls the get_events method to return a reference to the EventHandlerList object that maintains all the event handlers registered for the events of the 435 c11.indd 435 8/20/07 8:14:23 PM Chapter 11: Data Classes DataTable class, and then calls the addHandler method on this EventHandlerList object to add the specified method as the event handler for the propertyChanged event: this.get_events().addHandler(“propertyChanged”, handler); The DataTable class’s implementation of the INotifyPropertyChange interface’s remove_ propertyChanged method works the same as the add_propertyChanged method, with one difference Instead of invoking the addHandler method, it invokes the removeHandler method to remove the specified handler from the list of handlers registered for the propertyChanged event of the DataTable class: this.get_events().removeHandler(“propertyChanged”, handler); Following the event implementation pattern discussed in the previous chapters, the DataTable class exposes a method named _onPropertyChanged that raises the propertyChanged event: function Sys$Preview$Data$DataTable$_onPropertyChanged(propertyName) { var handler = this.get_events().getHandler(“propertyChanged”); if (handler) handler(this, new Sys.PropertyChangedEventArgs(propertyName)); } The _onPropertyChanged method first calls the get_events method to return a reference to the EventHandlerList that maintains all the event handlers registered for the events of the DataTable class Then it calls the getHandler method on the EventHandlerList object This method returns a JavaScript function whose invocation automatically invokes all event handlers registered for the propertyChanged event of the DataTable class Finally, the _onPropertyChanged method instantiates a PropertyChangedEventArgs object that encapsulates the name of the property whose value has changed This instance is finally passed into the event handlers registered for the propertyChanged event This enables the event handlers to determine the value of which property has changed INotifyCollectionChanged The boldface portion of the following code fragment shows how the DataTable class implements an interface named INotifyCollectionChanged: Sys.Preview.Data.DataTable.registerClass(‘Sys.Preview.Data.DataTable’, null, Sys.Preview.Data.IData, Sys.INotifyPropertyChange, Sys.Preview.INotifyCollectionChanged, Sys.IDisposable); Implementing this interface enables an ASP.NET AJAX class to raise an event named collectionChanged This event is useful in ASP.NET AJAX classes that contain one or more collections and want to inform their clients when the contents of these collections change For example, the DataTable class contains the _array collection where all the row objects associated with the DataRow objects of the current DataTable object are stored Implementing the INofiyCollectionChanged interface enables the DataTable class to raise the collectionChanged event when any of the following occurs: 436 c11.indd 436 8/20/07 8:14:23 PM Chapter 11: Data Classes ❑ A new row object is added to the _array collection ❑ A row object is removed from the _array collection ❑ A row object in the _array collection is updated Because a row object contains the names and values of the data fields of its associated DataRow object, updating a row object means updating the values of these data fields As Listing 11-23 shows, this interface exposes two methods named add_collectionChanged and remove_collectionChanged Your custom ASP.NET AJAX type’s implementation of these two methods must add the specified event handler to and remove the specified event handler from the internal collection where your type maintains the event handlers registered for its events This collection is an object of type EventHandlerList as discussed earlier Listing 11-23: The INotifyCollectionChanged Interface Sys.Preview.INotifyCollectionChanged = function Sys$Preview$INotifyCollectionChanged() { throw Error.notImplemented(); } function Sys$Preview$INotifyCollectionChanged$add_collectionChanged() { throw Error.notImplemented(); } function Sys$Preview$INotifyCollectionChanged$remove_collectionChanged() { throw Error.notImplemented(); } Sys.Preview.INotifyCollectionChanged.prototype = { add_collectionChanged: Sys$Preview$INotifyCollectionChanged$add_collectionChanged, remove_collectionChanged: Sys$Preview$INotifyCollectionChanged$remove_collectionChanged } Sys.Preview.INotifyCollectionChanged.registerInterface( ‘Sys.Preview.INotifyCollectionChanged’); As you can see in Listing 11-24, the DataTable class follows the same event implementation pattern discussed earlier to implement the collectionChanged event 437 c11.indd 437 8/20/07 8:14:23 PM Chapter 11: Data Classes Listing 11-24: The DataTable Class’s Implementation of the INotifyCollectionChanged Interface function Sys$Preview$Data$DataTable$add_collectionChanged(handler) { this.get_events().addHandler(“collectionChanged”, handler); } function Sys$Preview$Data$DataTable$remove_collectionChanged(handler) { this.get_events().removeHandler(“collectionChanged”, handler); } function Sys$Preview$Data$DataTable$_onCollectionChanged(action, changedItem) { var handler = this.get_events().getHandler(“collectionChanged”); if (handler) handler(this, new Sys.Preview.CollectionChangedEventArgs(action, changedItem)); } Note that the DataTable class exposes a method named _onCollectionChanged that raise the collectionChanged event This method passes an instance of an event data class named CollectionChangedEventArgs into the event handlers registered for the collectionChanged event when it calls these handlers Listing 11-25 presents the implementation of the CollectionChangedEventArgs event data class Listing 11-25: The CollectionChangedEventArgs Event Data Class Sys.Preview.CollectionChangedEventArgs = function Sys$Preview$CollectionChangedEventArgs(action, changedItem) { Sys.Preview.CollectionChangedEventArgs.initializeBase(this); this._action = action; this._changedItem = changedItem; } function Sys$Preview$CollectionChangedEventArgs$get_action() { return this._action; } function Sys$Preview$CollectionChangedEventArgs$get_changedItem() { return this._changedItem; } Sys.Preview.CollectionChangedEventArgs.prototype = { get_action: Sys$Preview$CollectionChangedEventArgs$get_action, get_changedItem: Sys$Preview$CollectionChangedEventArgs$get_changedItem } 438 c11.indd 438 8/20/07 8:14:23 PM Chapter 11: Data Classes Sys.Preview.CollectionChangedEventArgs.descriptor = { properties: [ {name: ‘action’,type: Sys.Preview.NotifyCollectionChangedAction,readOnly: true}, {name: ‘changedItem’, type: Object, readOnly: true} ] } Sys.Preview.CollectionChangedEventArgs.registerClass( ‘Sys.Preview.CollectionChangedEventArgs’, Sys.EventArgs); The constructor of the CollectionChangedEventArgs event data class takes two arguments The first argument is an enumeration of type NotifyCollectionChangedAction, and the second argument references an object As Listing 11-26 shows, the NofityCollectionChangedAction enumeration has the following three values: ❑ Add: This enumeration value specifies that the JavaScript object passed into the CollectionChangedEventArgs constructor as its second argument has been added to the collection In the case of the DataTable class, this JavaScript object is a row object associated with a new DataRow object being added to the DataTable object ❑ Remove: This enumeration value specifies that the object passed into the CollectionChangedEventArgs constructor as its second argument has been removed from the collection In the case of the DataTable class, this object is a row object associated with a DataRow object being removed from the DataTable object ❑ Reset: This enumeration value specifies that the collection has been cleared Listing 11-26: The NotifyCollectionChangedAction Enumeration Sys.Preview.NotifyCollectionChangedAction = function Sys$Preview$NotifyCollectionChangedAction() { throw Error.invalidOperation(); } Sys.Preview.NotifyCollectionChangedAction.prototype = { Add: 0, Remove: 1, Reset: } Sys.Preview.NotifyCollectionChangedAction.registerEnum( ‘Sys.Preview.NotifyCollectionChangedAction’); createRow The DataTable class comes with a method named createRow that you can use to create and optionally initialize a new DataRow object You must call this method to create a new DataRow object instead of 439 c11.indd 439 8/20/07 8:14:24 PM Chapter 11: Data Classes using the new operator directly Listing 11-27 contains the code for the createRow method As you can see, this method takes an optional parameter that provides initial values for the data fields of the newly created DataRow object Listing 11-27: The createRow Method function Sys$Preview$Data$DataTable$createRow(initialData) { var obj = {}; var undef = {}; for (var i = this._columns.length - 1; i >= 0; i ) { var column = this._columns[i]; var columnName = column.get_columnName(); var val = undef; if (initialData) val = Sys.Preview.TypeDescriptor.getProperty(initialData, columnName); if ((val === undef) || (typeof(val) === “undefined”)) val = column.get_defaultValue(); obj[columnName] = val; } var row = new Sys.Preview.Data.DataRow(obj, this, -1); row._set_state(Sys.Preview.Data.DataRowState.Detached); return row; } Now, let’s walk through this listing As previously discussed, the DataTable class contains an array named _columns that contains all the DataColumn objects of the DataTable object The createRow method iterates through the DataColumn objects in this array and takes the following steps for each enumerated object: It calls the get_columnName method on the enumerated DataColumn object to access the name of the column: var columnName = column.get_columnName(); It calls the getProperty static method on the TypeDescriptor class, passing in the optional object passed into the createRow method to return the value of the data field with the specified name: var val = undef; if (initialData) val = Sys.Preview.TypeDescriptor.getProperty(initialData, columnName); The object that you pass into the createRow method must return the value of a data field as if it were returning the value of a property with the same name as the data field If the object passed into the createRow method does not contain a property with the same name as the data field, the method calls the get_defaultValue method on the enumerated DataColumn object to return the default value of the associated data field and uses this value as the value of the data field: if ((val === undef) || (typeof(val) === “undefined”)) val = column.get_defaultValue(); 440 c11.indd 440 8/20/07 8:14:24 PM Chapter 14: Consuming Web Services Via JSON Messages Listing 14-22 presents the implementation of a fully functional replica ScriptHandlerFactory Listing 14-22: The ScriptHandlerFactory Class using using using using using using using using using using using using using System; System.Data; System.Configuration; System.Web; System.Reflection; System.Web.Compilation; System.ComponentModel; System.Web.Services; System.Web.Script.Serialization; System.Collections.Generic; System.Collections; System.Web.Services.Protocols; System.IO; namespace CustomComponents { public class ScriptHandlerFactory : IHttpHandlerFactory { private IHttpHandlerFactory _restHandlerFactory; private IHttpHandlerFactory _webServiceHandlerFactory; public ScriptHandlerFactory() { this._restHandlerFactory = new RestHandlerFactory(); this._webServiceHandlerFactory = new WebServiceHandlerFactory(); } public virtual IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated) { IHttpHandlerFactory handlerFactory; if (RestHandlerFactory.IsRestRequest(context)) handlerFactory = this._restHandlerFactory; else handlerFactory = this._webServiceHandlerFactory; IHttpHandler handler = handlerFactory.GetHandler(context, requestType, url, pathTranslated); return new HandlerWrapper(handler, handlerFactory); } public virtual void ReleaseHandler(IHttpHandler handler) { ((HandlerWrapper)handler).ReleaseHandler(); } } } Note that the constructor of this replica, just like the actual ScriptHandlerFactory, instantiates instances of two other HTTP handler factories named RestHandlerFactory and WebServiceHandlerFactory: 572 c14.indd 572 8/20/07 6:13:57 PM Chapter 14: Consuming Web Services Via JSON Messages this._restHandlerFactory = new RestHandlerFactory(); this._webServiceHandlerFactory = new WebServiceHandlerFactory(); The implementation of the replica RestHandlerFactory is discussed later in this chapter The WebServiceHandlerFactory is the standard ASP.NET HTTP handler factory that handles SOAP requests made to a Web service This book does not discuss this handler factory because the ASP.NET AJAX framework uses REST messages as opposed to SOAP messages to interact with the backend Web method Now let’s walk through the implementation of the GetHandler method of the replica ScriptHandlerFactory This method first invokes the IsRestRequest static method on the RestHandlerFactory to determine whether the current request is a REST (JSON) request If so, it invokes the GetHandler method of the RestHandlerFactory to instantiate, initialize, and return an HTTP handler that knows how to process REST (JSON) requests If not, it assumes that the request is a SOAP request and invokes the GetHandler method of the WebServiceHandlerFactory to instantiate, initialize, and return an HTTP handler that knows how to process SOAP requests IHttpHandlerFactory handlerFactory; if (RestHandlerFactory.IsRestRequest(context)) handlerFactory = this._restHandlerFactory; else handlerFactory = this._webServiceHandlerFactory; IHttpHandler handler = handlerFactory.GetHandler(context, requestType, url, pathTranslated); Finally, the GetHandler method instantiates an instance of an HTTP handler named HandlerWrapper that wraps the HTTP handler returned from the calls into the GetHandler method of RestHandlerFactory or WebServiceHandlerFactory: return new HandlerWrapper(handler, handlerFactory); This wrapper, like any other wrapper, hides the actual type of the HTTP handler from the caller of the GetHandler method of the ScriptHandlerFactory As previously mentioned, the actual HTTP handler type depends on whether the GetHandler method of the RestHandlerFactory or the GetHandler method of the WebServiceHandlerFactory is invoked — in other words, whether the current request is a REST or SOAP request If you check out the web.config file of your ASP.NET AJAX application, you’ll see the XML fragment shown in Listing 14-23 Note that the boldface portion of this listing will only show up if you are running IIS7 Listing 14-23: The web.config File (continued) 573 c14.indd 573 8/20/07 6:13:58 PM Chapter 14: Consuming Web Services Via JSON Messages Listing 14-23 (continued) Both the boldface and non-boldfaced portions of this code listing begin by removing the HTTP handler factory registered for handling the extension asmx The non-boldface portion uses the following XML line to remove this handler: The boldface portion uses the following XML line to remove this handler: The removed HTTP handler factory in both cases is WebServiceHandlerFactory Both cases then register the ScriptHandlerFactory for handling the requests for extensions asmx and asbx In other words, requests for these two extensions are now handled by the same HTTP handler factory As discussed earlier, the GetHandler method of ScriptHandlerFactory then uses the IsRestRequest static method of the RestHandlerFactory to determine whether the current request is a REST or SOAP request If the current request is a normal SOAP request, the request is handed back to the originally removed HTTP handler factory: WebServiceHandlerFactory This enables ScriptHandlerFactory to hand all REST requests over to RestHandlerFactory, including the REST requests for extension asmx and the REST requests for extension asbx RestHandlerFactory As discussed earlier, the ScriptHandlerFactory hands the REST requests for both the asmx and asbx extensions to the RestHandlerFactory for processing Listing 14-24 presents the implementation of a fully functional RestHandlerFactory replica 574 c14.indd 574 8/20/07 6:13:58 PM Chapter 14: Consuming Web Services Via JSON Messages Listing 14-24: The RestHandlerFactory using using using using using using using using using using using using using System; System.Data; System.Configuration; System.Web; System.Reflection; System.Web.Compilation; System.ComponentModel; System.Web.Services; System.Web.Script.Serialization; System.Collections.Generic; System.Collections; System.Web.Services.Protocols; System.IO; namespace CustomComponents { internal class RestHandlerFactory : IHttpHandlerFactory { public virtual IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated) { if (IsClientProxyRequest(context.Request.PathInfo)) return new RestClientProxyHandler(); return RestHandler.CreateHandler(context); } internal static bool IsRestRequest(HttpContext context) { if (!IsRestMethodCall(context.Request)) return IsClientProxyRequest(context.Request.PathInfo); return true; } internal static bool IsRestMethodCall(HttpRequest request) { if (string.IsNullOrEmpty(request.PathInfo)) return false; if (!request.ContentType.StartsWith(“application/json;”, StringComparison.OrdinalIgnoreCase)) return string.Equals(request.ContentType, “application/json”, StringComparison.OrdinalIgnoreCase); return true; } internal static bool IsClientProxyRequest(string pathInfo) { return string.Equals(pathInfo, “/js”, StringComparison.OrdinalIgnoreCase); } (continued) 575 c14.indd 575 8/20/07 6:13:58 PM Chapter 14: Consuming Web Services Via JSON Messages Listing 14-24 (continued) public virtual void ReleaseHandler(IHttpHandler handler) { } } } First, let’s walk through the implementation of the IsRestRequest static method As previously discussed, the ScriptHandlerFactory invokes this method to determine whether the current request is a REST request In general, the ASP.NET AJAX framework supports the following two types of REST requests: ❑ REST method call: The client code makes this type of REST request to invoke a server-side method You saw several examples of this earlier in this chapter ❑ Client proxy request: The client code makes this type of REST request to download the script that defines the proxy class The next chatper discusses this proxy class As Listing 14-24 shows, the IsRestRequest static method first invokes a method named IsRestMethodCall to determine whether the currest request is a REST method call request If not, the IsRestRequest method invokes the IsClientProxyRequest method to determine whether the current request is a client proxy request Now let’s walk through the implementation of the IsRestMethodCall method As you can see in the following excerpt from Listing 14-24, this method first calls the PathInfo property on the ASP.NET Request object to determine whether the request URL contains a path information trailer The client code adds a trailer to the request URL that contains information such as the name of the server method being invoked Therefore, the absence of a path information trailer by itself indicates that the current request cannot be a REST method call internal static bool IsRestMethodCall(HttpRequest request) { if (string.IsNullOrEmpty(request.PathInfo)) return false; If the request URL contains a path information trailer, the IsRestMethodCall method checks whether the Content-Type HTTP request header contains the string “application/json” The client code adds this value to the Content-Type HTTP header to inform the RestHttpHandler that the current request is a REST method call, which means that the client is trying to invoke a server method: if (!request.ContentType.StartsWith(“application/json;”, StringComparison.OrdinalIgnoreCase)) return string.Equals(request.ContentType, “application/json”, StringComparison.OrdinalIgnoreCase); return true; } As discussed earlier, the IsRestRequest method invokes the IsClientProxyRequest method to determine whether the current request is a client proxy request This method simply checks whether the path information trailer is the js or jsdebug string The client code adds the js path information trailer to the request URL to inform RestHandlerFactory that it needs to download the release version of the script that contains the proxy class The client code adds the jsdebug path information trailer to the 576 c14.indd 576 8/20/07 6:13:59 PM Chapter 14: Consuming Web Services Via JSON Messages request URL to inform RestHandlerFactory that it needs to download the debug version of the script that contains the proxy class As you can see, the presence of this path information trailer by itself signals that the client code has made the current request to download the script that contains the proxy class In other words, the current request is not a REST method call request To keep this discussion focused, the replica only considers the requests made for downloading the release version The next chapter discusses the proxy class and the script code that contains the definition of the proxy class in the next chapter The RestHandlerFactory, like any other HTTP handler factory, implements the GetHandler method of the IHttpHandlerFactory As you can see in Listing 14-24, this method first invokes the IsClientProxyRequest method to determine whether the client code has made the current request to download the script that contains the proxy class If so, it instantiates and returns an instance of an HTTP handler named RestClientProxyHandler If not, it invokes the CreateHandler static method on an ASP.NET class named RestHandler to instantiate and to return an instance of the RestHandler HTTP handler The next chapter discusses the RestClientProxyHandler as part of its coverage of the proxy class and the script that defines it For now, suffice it to say that the RestClientProxyHandler and RestHandler know how to handle REST client proxy and REST method call requests, respectively RestHandler Listing 14-25 presents the implementation of the replica RestHandler HTTP handler As previously shown in Listing 14-24, the GetHandler method of RestHandlerFactory invokes the CreateHandler static method on the RestHandler class to instantiate an instance of this class Listing 14-25: The RestHandler HTTP Handler using using using using using using using using using using using using using using System; System.Data; System.Configuration; System.Web; System.Web.UI; System.Reflection; System.Web.Compilation; System.ComponentModel; System.Web.Services; System.Web.Script.Serialization; System.Collections.Generic; System.Collections; System.Web.Services.Protocols; System.IO; namespace CustomComponents { internal class RestHandler : IHttpHandler { private MethodInfo _methodInfo; internal static IHttpHandler CreateHandler(HttpContext context) { string servicePath = context.Request.FilePath; Type serviceType = BuildManager.GetCompiledType(servicePath); if (serviceType == null) (continued) 577 c14.indd 577 8/20/07 6:13:59 PM Chapter 14: Consuming Web Services Via JSON Messages Listing 14-25 (continued) { object obj = BuildManager.CreateInstanceFromVirtualPath(servicePath, typeof(Page)); serviceType = obj.GetType(); } string methodName = context.Request.PathInfo.Substring(1); MethodInfo[] infoArray = serviceType.GetMethods(); MethodInfo minfo = null; foreach (MethodInfo info in infoArray) { object[] objArray = info.GetCustomAttributes(typeof(WebMethodAttribute), true); if (objArray.Length != && info.Name == methodName) { minfo = info; break; } } RestHandler handler = new RestHandler(); handler._methodInfo = minfo; return handler; } public void ProcessRequest(HttpContext context) { string text = new StreamReader(context.Request.InputStream).ReadToEnd(); IDictionary rawParams; JavaScriptSerializer serializer = new JavaScriptSerializer(); if (string.IsNullOrEmpty(text)) rawParams = new Dictionary(); else rawParams = serializer.Deserialize(text); ArrayList parameters = new ArrayList(); ParameterInfo[] infos = _methodInfo.GetParameters(); TypeConverter converter; foreach (KeyValuePair entry in rawParams) { IDictionary dictionary = entry.Value as IDictionary; if (dictionary != null) parameters.Add(dictionary); else { for (int i = 0; i < infos.Length; i++) { if (entry.Key == infos[i].Name) { converter = TypeDescriptor.GetConverter(infos[i].ParameterType); 578 c14.indd 578 8/20/07 6:13:59 PM Chapter 14: Consuming Web Services Via JSON Messages if (converter.CanConvertFrom(entry.Value.GetType())) parameters.Add(converter.ConvertFrom(entry.Value)); } } } } object[] methodParameters = new object[parameters.Count]; parameters.CopyTo(methodParameters); object target = Activator.CreateInstance(_methodInfo.DeclaringType); object obj3 = _methodInfo.Invoke(target, methodParameters); string s = serializer.Serialize(obj3); context.Response.ContentType = “application/json”; if (s != null) context.Response.Write(s); } public bool IsReusable { get { return false; } } } } The CreateHandler method begins by calling the FilePath property on the ASP.NET Request object to access the virtual path of the requested file: string servicePath = context.Request.FilePath; For example, if the request is made for an asbx file, the FilePath returns the virtual path of this asbx file If the request is made for a Web service (an asmx file), the FilePath returns the URL of the Web service Next, the CreateHandler method invokes a static method named GetCompiledType on an ASP.NET class named BuildManager, passing in the virtual path of the requested file: Type serviceType = BuildManager.GetCompiledType(servicePath); This static method parses and compiles the file with the specified virtual path into a dynamically generated NET type or class and returns a Type object that represents this class For example, consider the request made for the asbx file shown in the following excerpt from Listing 14-19: 579 c14.indd 579 8/20/07 6:14:00 PM Chapter 14: Consuming Web Services Via JSON Messages In this case, the GetCompiledType static method will dynamically create a class with the name specified in the className attribute on the bridge document element This element also belongs to a namespace with the name specified in the namespace attribute on this element That is, the GetCompiledType method will create a class named MyMath that belongs to a namespace named MyNamespace The method will then dynamically compile this class into an assembly, load this assembly into the application domain where the current application is running, and return a Type object that represents the MyMath class Next, the CreateHandler method extracts a substring of the PathInfo property of the ASP.NET Request object whose starting index is 1: string methodName = context.Request.PathInfo.Substring(1); The PathInfo property contains the data that comes after the virtual path of a file, and the substring contains the name of the server method being invoked The CreateHandler method then invokes the GetMethods method on the Type object that represents the dynamically generated class to return an array of MethodInfo objects, where each object represents a method of this class: MethodInfo[] infoArray = serviceType.GetMethods(); Next, the CreateHandler method searches this array for a MethodInfo object that represents a method with the name specified in the first substring of the PathInfo trailer and annotated with the WebMethodAttribute metadata attribute: MethodInfo minfo = null; foreach (MethodInfo info in infoArray) { object[] objArray = info.GetCustomAttributes(typeof(WebMethodAttribute), true); if (objArray.Length != && info.Name == methodName) { minfo = info; break; } } The CreateHandler method then instantiates a RestHandler HTTP handler and assigns the MethodInfo object to its _methodInfo private field: RestHandler handler = new RestHandler(); handler._methodInfo = minfo; return handler; As you can see in Listing 14-25, the RestHandler HTTP handler, like any other HTTP handler, implements a method named ProcessRequest This method is responsible for processing the current REST request It begins by loading the request stream into a StreamReader and then invoking the ReadToEnd method on this StreamReader to load the content of the StreamReader and, consequently, the entire client request into a string Because the current request is a REST (JSON) request, this string contains a JSON object that consists of name/value pairs: 580 c14.indd 580 8/20/07 6:14:00 PM Chapter 14: Consuming Web Services Via JSON Messages string text = new StreamReader(context.Request.InputStream).ReadToEnd(); Next, ProcessRequest instantiates a JavaScriptSerializer and invokes its Deserialize method to deserialize an IDictionary object from the JSON object IDictionary is a collection of KeyValuePair objects In this case, each KeyValuePair object represents a name/value pair of the JSON object: IDictionary rawParams; JavaScriptSerializer serializer = new JavaScriptSerializer(); rawParams = serializer.Deserialize(text); Next, the ProcessRequest method invokes the GetParameters method on the MethodInfo object that represents the method being invoked to return an array of ParameterInfo objects, where each ParameterInfo object represents a parameter of the method being invoked: ParameterInfo[] infos = _methodInfo.GetParameters(); Then, it iterates through the KeyValuePair objects in the IDictionary collection and uses the type converter associated with each value part of each KeyValuePair object to convert the value into its associated NET type if the value part is not of type IDictionary Otherwise, it uses the value part as is Keep in mind that the value part of the KeyValuePair contains the value part of the name/value pair of the original JSON object, and each name/value pair in the original JSON object represents a parameter of the method being invoked To keep this discussion focused, the following code uses a simple conversion mechanism: TypeConverter converter; ArrayList parameters = new ArrayList(); foreach (KeyValuePair entry in rawParams) { IDictionary dictionary = entry.Value as IDictionary; if (dictionary != null) parameters.Add(dictionary); else { for (int i = 0; i < infos.Length; i++) { if (entry.Key == infos[i].Name) { converter = TypeDescriptor.GetConverter(infos[i].ParameterType); if (converter.CanConvertFrom(entry.Value.GetType())) parameters.Add(converter.ConvertFrom(entry.Value)); } } } } Next, the ProcessRequest method calls the CreateInstance static method on the Activator class to dynamically instantiate an instance of the class that contains the method being invoked: 581 c14.indd 581 8/20/07 6:14:00 PM Chapter 14: Consuming Web Services Via JSON Messages object[] methodParameters = new object[parameters.Count]; parameters.CopyTo(methodParameters); object target = Activator.CreateInstance(_methodInfo.DeclaringType); Then the ProcessRequest method calls the Invoke method on the MethodInfo object that represents the method being invoked to invoke the method in a generic fashion: object obj3 = _methodInfo.Invoke(target, methodParameters); Next, the ProcessRequest method calls the Serialize method on the JavaScriptSerializer object to serialize the return value of the method into its JSON representation: string s = serializer.Serialize(obj3); Next, it sets the Content-Type response HTTP header to the string “application/json” to inform the client code that the response contains a JSON string: context.Response.ContentType = “application/json”; Finally, it invokes the Write method on the ASP.NET Response object to write the JSON string representation of the return value of the method into the response output stream, which is then sent back to the client: if (s != null) context.Response.Write(s); HandlerWrapper As previously shown in Listing 14-22, the GetHandler method of the ScriptHandlerFactory calls the GetHandler method of the RestHandlerFactory if the current request is a SOAP request, or the GetHandler method of the WebServiceHandlerFactory if the current request is a REST (JSON) request It then hides the return value of the GetHandler method of RestHandlerFactory or WebServiceHandlerFactory in a wrapper HTTP handler named HandlerWrapper, as defined in Listing 14-26 Listing 14-26: The HandlerWrapper HTTP Handler using using using using using using using using using using using using using System; System.Data; System.Configuration; System.Web; System.Reflection; System.Web.Compilation; System.ComponentModel; System.Web.Services; System.Web.Script.Serialization; System.Collections.Generic; System.Collections; System.Web.Services.Protocols; System.IO; namespace CustomComponents { 582 c14.indd 582 8/20/07 6:14:01 PM Chapter 14: Consuming Web Services Via JSON Messages internal class HandlerWrapper : IHttpHandler { private IHttpHandlerFactory _handlerFactory; protected IHttpHandler _handler; internal HandlerWrapper(IHttpHandler handler, IHttpHandlerFactory handlerFactory) { this._handlerFactory = handlerFactory; this._handler = handler; } public void ProcessRequest(HttpContext context) { this._handler.ProcessRequest(context); } internal void ReleaseHandler() { this._handlerFactory.ReleaseHandler(this._handler); } public bool IsReusable { get { return this._handler.IsReusable; } } } } Page Methods Demystified As discussed earlier, the ASP.NET AJAX framework provides you with three different approaches to enable your client-side code to invoke a server-side method in asynchronous fashion Again, the options are as follows: ❑ Have your client-side code make a request for an asbx file that describes the server-side method ❑ Have your client-side code make a request for an asmx file that contains the server-side method as a Web method of a Web service ❑ Have your client-side code make a request for an aspx file that contains the server-side method as a page method annotated with the WebMethod metadata attribute The web.config file of your ASP.NET AJAX application directly registers the ScriptHandlerFactory as the handler for requests for resources with file extensions asbx and asmx This registration covers only the first two approaches How about the third approach, where the server side method is a method that resides in an aspx file instead of asbx or asmx? The web.config file does not directly register the ScriptHandlerFactory as the handler for the requests for resources with the file extension aspx 583 c14.indd 583 8/20/07 6:14:01 PM Chapter 14: Consuming Web Services Via JSON Messages because aspx files must be handled by PageHandlerFactory Therefore, you need a way to make a distinction between the following two types of requests made for a resource with the file extension aspx: ❑ A normal ASP.NET request for an aspx file ❑ A REST request when the client is trying to invoke a particular server-side method that happens to reside on the aspx file This is where the ScriptModule comes into play If you check out the web.config file of your ASP.NET AJAX application, you’ll see the XML fragment shown in Listing 14-27 Note that the boldface portion of this listing will only show up if you’re running IIS7 Both the boldface and non-boldface portions of this listing register the ScriptModule with the ASP.NET request processing pipeline Every ASP.NET request is guaranteed to go through this pipeline, and every module in this pipeline registers one or more event handlers for one or more events of the HttpApplication object that represents the current ASP.NET application The HttpApplication object raises its request level events for every single ASP.NET request Listing 14-27: The web.config File The modules that make up the ASP.NET request processing pipeline are known as HTTP modules All HTTP modules implement an ASP.NET interface named IHttpModule, as defined in Listing 14-28 This interface exposes the following two methods: ❑ Init: Every HTTP module must implement this method to register one or more event handlers for one or more events of the HttpApplication object that represents the current ASP.NET application ASP.NET automatically passes a reference to the current HttpApplication object into this method ❑ Dispose: Every HTTP module must implement this method to perform its final cleanup before it is disposed of 584 c14.indd 584 8/20/07 6:14:01 PM Chapter 14: Consuming Web Services Via JSON Messages Listing 14-28: The IHttpModule Interface public interface IHttpModule { void Dispose(); void Init(HttpApplication context); } Listing 14-29 presents the implementation of the replica ScriptModule Like all HTTP modules, ScriptModule implements the IHttpModule interface Listing 14-29: ScriptModule using using using using using using using using using using using using using using System; System.Data; System.Configuration; System.Web; System.Web.UI; System.Reflection; System.Web.Compilation; System.ComponentModel; System.Web.Services; System.Web.Script.Serialization; System.Collections.Generic; System.Collections; System.Web.Services.Protocols; System.IO; namespace CustomComponents { public class ScriptModule : IHttpModule { protected virtual void Dispose() { } protected virtual void Init(HttpApplication context) { context.PostAcquireRequestState += new EventHandler(this.OnPostAcquireRequestState); } private void OnPostAcquireRequestState(object sender, EventArgs eventArgs) { HttpApplication application = (HttpApplication)sender; HttpRequest request = application.Context.Request; if ((application.Context.Handler is Page) && RestHandlerFactory.IsRestMethodCall(request)) { IHttpHandler restHandler = RestHandler.CreateHandler(application.Context); restHandler.ProcessRequest(application.Context); application.CompleteRequest(); } } (continued) 585 c14.indd 585 8/20/07 6:14:02 PM Chapter 14: Consuming Web Services Via JSON Messages Listing 14-29 (continued) void IHttpModule.Dispose() { this.Dispose(); } void IHttpModule.Init(HttpApplication context) { this.Init(context); } } } As the following excerpt from Listing 14-29 shows, the Init method of ScriptModule registers its OnPostAcquireRequestState method as an event handler for the PostAcquireRequestState event of the current HttpApplication object: context.PostAcquireRequestState += new EventHandler(this.OnPostAcquireRequestState); The HttpApplication object fires PostAcquireRequestState after the current request acquires its state from the underlying data store This state includes the session state if the session state is enabled for the current page Now let’s walk through the implementation of the OnPostAcquireRequestState method When the current HttpApplication object finally raises its PostAcquireRequestState event and, consequently, invokes the OnPostAcquireRequestState method, it passes a reference to itself into this method as its first argument: HttpApplication application = (HttpApplication)sender; HttpRequest request = application.Context.Request; The OnPostAcquireRequestState method, like the Init method of any other HTTP module, uses this reference to access the current HTTP context object This object contains the context in which the current request is running As such, it includes the complete information about the current request and response Next, the OnPostAcquireRequestState method checks whether the following two conditions are met: ❑ The HTTP handler responsible for handling the current request is of type Page If so, this indicates that the current request has been made for a resource with the file extension aspx ❑ The Init method then invokes the IsRestMethodCall static method on the RestHandlerFactory to determine whether the current request is a REST method call As previously discussed, a REST method call request is a request that the client code makes to the server to invoke a server method if ((application.Context.Handler is Page) && RestHandlerFactory.IsRestMethodCall(request)) { 586 c14.indd 586 8/20/07 6:14:02 PM ... !Sys.Net.WebRequestExecutor.isInstanceOfType(executor) || !executor) throw Error.argument(“defaultExecutorType”, String.format(Sys.Res.invalidExecutorType, this._defaultExecutorType)); webRequest.set_executor(executor);... executor = webRequest.get_executor(); if (!executor) { var failed = false; try { var executorType = eval(this._defaultExecutorType); executor = new executorType(); } (continued) 47 1 c12.indd 47 1... ] } 44 4 c11.indd 44 4 8/20/07 8: 14: 25 PM Chapter 11: Data Classes ProductID ProductName UnitPrice Product1 100 Product2 50 Product3 80 This object literal describes the Products data table with