Blog Home  Home Feed your aggregator (RSS 2.0)  
ASP.NET AJAX Extensions Internals - Web Service Proxy Generation - Manuel Abadia's ASP.NET stuff
 
# Saturday, March 17, 2007

In my last post about MS AJAX I gave a brief explanation about the web.config changes needed and explained the serialization process.

Now, I'm going to explain how a client proxy is generated. As I told in the last post, the WebServiceClientProxyGenerator.GetClientProxyScript method was were all the job was done.

There are a few classes involved in the client proxy generation:

  • WebServiceTypeData, that is used to store type information about the web service to call.
  • WebServiceMethodData, that is used to store information about a web service method (parameters, return type, caching, etc), and it has the functionality to invoke the method.
  • WebServiceParameterData, used to store information about a parameter of a web service method.
  • WebServiceData. This class uses the previous classes and has a lot of logic. Basically a WebServiceData instance is created for each web service to call (for each instance there is an associated WebServiceTypeData instance). When the web service methods are required, the EnsureMethods method uses reflection to obtain all WebMethods from the type (and its ancestors).

By default, a web service can't be called from client script. To change this, the web service needs the ScriptServiceAttribute applied to it.

To be able to call a web method from client script, the parameters and return types of the web method have to be available in client script, and the web service needs to have the WebMethodAttribute (the ScriptMethodAttribute should be used only to override the default invocation method and respone format, that is HTTP POST and JSON). If a web service method needs a parameter of a custom type, we have to use the GenerateScriptTypeAttribute attribute in the method or in the web service so MS AJAX will automatically generate a client side class for that type.

So when the methods are processed, the ClientTypes property will be filled in the ProcessClientTypes method with a list of the types that need to be available in client script.

However, the automatic type generation has some limitations, as the GenerateScriptTypeAttribute can not be applied to types implementing IEnumerable or IDictionary, interfaces, abstract classes, types without a public parameterless constructor, and generic types with more than one parameter.

The WebServiceData class also inherits from the JavaScriptTypeResolver class to provide string to type conversion (and viceversa) for the serialization/deserialization process. You may be wondering why the SimpleTypeResolver isn't used, but as the GenerateScriptTypeAttribute has a property called ScriptTypeId that modifies the __type field stored in the JSON, another type resolver was needed.

As you can see, the WebServiceData class does a lot of work, so to get WebServiceData instances the static method GetWebServiceData is used because it caches instances by URL.

If you don't want to create a new class for a few methods, you can add them to your ASPX page and call them directly. For this to work, the methods should be static and have the WebMethodAttribute attribute applied.

The ClientProxyGenerator class uses a WebServiceData instance and generates the client proxy code, although the code calls to the static method GetClientProxyScript of the WebServiceClientProxyGenerator class to get the generated client proxy code, because it performs caching based on the web service type and the modified time of the associated assembly.

For the following web service:

[WebService(Namespace = "http://tempuri.org/")]

[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

[ScriptService]

public class AjaxWebService {

 

    [WebMethod]

    [GenerateScriptType(typeof(User))]

    [GenerateScriptType(typeof(Address))]

    public User GetModifiedUser(User usr) {

        usr.ID = usr.ID * 2;

        usr.Name = "Manu";

 

        return usr;

    }  

}

the User and Address classes were introduced in the previous post.

the generated proxy is:

var AjaxWebService=function() {

    AjaxWebService.initializeBase(this);

    this._timeout = 0;

    this._userContext = null;

    this._succeeded = null;

    this._failed = null;

}

 

AjaxWebService.prototype={

    GetModifiedUser:function(usr,succeededCallback, failedCallback, userContext) {

        return this._invoke(AjaxWebService.get_path(), 'GetModifiedUser',false,{

            usr:usr

        }

        ,succeededCallback,failedCallback,userContext);

    }

}

 

AjaxWebService.registerClass('AjaxWebService',Sys.Net.WebServiceProxy);

AjaxWebService._staticInstance = new AjaxWebService();

AjaxWebService.set_path = function(value) {

    var e = Function._validateParams(arguments, [{

        name: 'path', type: String

    }

    ]); if (e) throw e; AjaxWebService._staticInstance._path = value;

}

 

AjaxWebService.get_path = function() {

    return AjaxWebService._staticInstance._path;

}

 

AjaxWebService.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);

    }

    AjaxWebService._staticInstance._timeout = value;

}

 

AjaxWebService.get_timeout = function() {

    return AjaxWebService._staticInstance._timeout;

}

 

AjaxWebService.set_defaultUserContext = function(value) {

    AjaxWebService._staticInstance._userContext = value;

}

 

AjaxWebService.get_defaultUserContext = function() {

    return AjaxWebService._staticInstance._userContext;

}

 

AjaxWebService.set_defaultSucceededCallback = function(value) {

    var e = Function._validateParams(arguments, [{

        name: 'defaultSucceededCallback', type: Function

    }

    ]); if (e) throw e; AjaxWebService._staticInstance._succeeded = value;

}

 

AjaxWebService.get_defaultSucceededCallback = function() {

    return AjaxWebService._staticInstance._succeeded;

}

 

AjaxWebService.set_defaultFailedCallback = function(value) {

    var e = Function._validateParams(arguments, [{

        name: 'defaultFailedCallback', type: Function

    }

    ]); if (e) throw e; AjaxWebService._staticInstance._failed = value;

}

 

AjaxWebService.get_defaultFailedCallback = function() {

    return AjaxWebService._staticInstance._failed;

}

 

AjaxWebService.set_path("/AJAXServerSide1/AjaxWebService.asmx");

AjaxWebService.GetModifiedUser= function(usr,onSuccess,onFailed,userContext) {

    AjaxWebService._staticInstance.GetModifiedUser(usr,onSuccess,onFailed,userContext);

}

 

var gtc = Sys.Net.WebServiceProxy._generateTypedConstructor;

 

if (typeof(Address) === 'undefined') {

    var Address=gtc("Address");

    Address.registerClass('Address');

}

 

if (typeof(User) === 'undefined') {

    var User=gtc("User");

    User.registerClass('User');

}

 

The generated proxy class is called like the web service, and inherits from the Sys.Net.WebServiceProxy class, which has the functionality to invoke a web service. The class have some properties set in order call the specific web service that it is proxying, and the web methods exposed (GetModifiedUser in our case).

After the class to call the web service, the User and Address classes are created and registered to be used by client code (thanks to the GenerateScriptTypeAttribute). However, no property of the original classes is generated in client script. The core of this client classes is the call to the Sys.Net.WebServiceProxy._generateTypedConstructor method. This method creates a new class that accepts an object as a parameter for the constructor. The constructor of the class will iterate through the members of the object passed as the parameter and copy them to the current instance.

So you can create an instance of the Address class that has 2 fields (propertyOne and propertyTwo) like this:

new Address({"firstField":"firstValue","secondField":"secondValue"}).

 

and accessing to Address.firstField will return "firstValue".

So if you want to call the previous web service from client script you can do something like this:

       

function callWebService()

        {

            var user = new User();

            user.ID = 1;

            user.Name = "Manuel";

            user.SurName = "Abadia";

            user.CreationDate = new Date();

            user.Address = new Address();

            user.Address.FirstLine = "C/ Torre Alvarez";

            user.Address.SecondLine = "Murcia, 2007";

            user.Address.Country = "Spain";

            AjaxWebService.GetModifiedUser(user, OnSucceeded, OnError);

        }

 

        function OnSucceeded(result)

        {

            var RsltElem = document.getElementById("Results");

            RsltElem.innerHTML = result.Name + "[" + result.ID + "]";

        }

 

        function OnError(message)

        {

           alert(message.get_message() + " " + message.get_stackTrace());

        }

 

when the User instance is created it contains no members, but you keep adding them in each assignment (this is how JavaScript works). You can even add nonexisting members like user.Foo = "This property doesn't exists in server code" and they will be ignored on the server side in the deserialization process.

When the GetModifiedUser method is called from client script, it generates an asynchronous HTTP request to the web service (http://localhost/AJAXServerSide1/AjaxWebService.asmx/GetModifiedUser in my case) with the following posted data:

{"usr":{"__type":"User","ID":1,"Name":"Manuel","SurName":"Abadia","CreationDate":"\/Date(1174133124369)\/","Address":{"__type":"Address","FirstLine":"C/ Torre Alvarez","SecondLine":"Murcia, 2007","Country":"Spain"}}}

 

and the server generates a response with the following data:

 

 

{"__type":"User","ID":2,"Name":"Manu","SurName":"Abadia","Address":{"__type":"Address","FirstLine":"C/ Torre Alvarez","SecondLine":"Murcia, 2007","Country":"Spain"},"CreationDate":"\/Date(1174133124369)\/"}

that is passed to directly the User constructor in order to generate the returned value from the web service.

In my next post about MS AJAX I'll sumarize what we have learned and how it applies to the ScriptHandlerFactory, the application web services, and then continue with the next HttpHandler, the ScriptResourceHandler.

Saturday, March 17, 2007 2:09:19 PM (Romance Standard Time, UTC+01:00)  #    Comments [4]   Ajax | ASP.NET | JavaScript  | 
Sunday, April 15, 2007 11:01:57 PM (Romance Daylight Time, UTC+02:00)
This article is incredibly informative, thankyou! I'd like to ask one thing though: I have an custom control contained in an assembly. I'd like this control to call web services also contained within the same assembly, exactly like in ASP.NET AJAX. Is there a way I can use .NET AJAXs ScriptHandlerFactory to do all the work of this, or am I going to have to copy the code files to my project and create my own HttpHandler?

Thanks again so much, great article!
Tuesday, April 17, 2007 12:57:35 AM (Romance Daylight Time, UTC+02:00)
Josh,

ASP.NET AJAX uses HostingEnvironment.VirtualPathProvider.FileExists(virtualPath) to check that the file of the webservice exists in the server before calling it, so probably you will need to create your own HttpHandler.

Another option can be to create a web service that has redirects its calls to the web service in your assembly. You don't have to code your own HttpHandler at the expense of losing some performance.
Thursday, April 19, 2007 8:48:27 PM (Romance Daylight Time, UTC+02:00)
Thanks Manuel,

I have got quite far; I have an HttpHandler which is intercepting calls to my "embedded" web service (the .asmx file doesn't really exist, as it's just a dll). The issue where I've got stuck is with the JSON string.

When in ProcessRequest(HttpContext), I can see the string in context.Request.InputStream, it looks exactly like the one you describe above. But how do I deserialize it into an array of objects such as is received by the web service method as arguments? Under normal circumstances, somewhere behind the scenes AJAX.NET is taking that JSON string and turning it into the arguments for the web service. Is there a way I can hook into this? JavaScriptSerializer looks like it should be what I'm after, but it only works on one JSON object at a time, not an array.

At http://forums.asp.net/thread/1670036.aspx I outline the problem and where I'm up to.

Thanks again,
Josh
Friday, April 20, 2007 11:51:31 PM (Romance Daylight Time, UTC+02:00)
Josh,

you example is wrong:

Person p = JavaScriptSerializer.DeserializeObject("{\"__type\":\"Person\",\"Name\":\"Josh\"}");

doesn't compile. I suppose you wanted to say something like this:

JavaScriptSerializer js = new JavaScriptSerializer(new SimpleTypeResolver());
Person p = (Person)js.DeserializeObject("{\"__type\":\"Person\",\"Name\":\"Josh\"}");

And that fails with an InvalidOperationException. Why? Because the type Person can't be resolved. The simple type resolver uses Type.GetType to find a type and if you try Type.GetType("Person") you will find that it returns null.

AJAX.NET uses the WebServiceData type resolver instead of the SimpleTypeResolver for WebServices as explained above, so types passed to the web service are handled properly. As that class is internal you can try creating your own TypeResolver:
using System;
using System.Web;
using System.Web.Compilation;
using System.Web.Script.Serialization;

public class ManuTypeResolverEx : SimpleTypeResolver
{
public ManuTypeResolverEx()
{
}

public override Type ResolveType(string id)
{
return BuildManager.GetType(id, false);
}
}

And with that type resolver, this works:

JavaScriptSerializer js = new JavaScriptSerializer(new ManuTypeResolverEx());
Person p = (Person)js.DeserializeObject("{\"__type\":\"Person\",\"Name\":\"Josh\"}");

The AJAX.NET source code is available, so to be able to push the limits of it, you'll have to get dirty into code to solve problems like this.

I hope it helps,
Manu.
All comments require the approval of the site owner before being displayed.
Name
E-mail
Home page

Comment (Some html is allowed: a@href@title, strike) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

[Captcha]Enter the code shown (prevents robots):

Live Comment Preview
Copyright © 2014 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.