Introduction
Anyone who has been building .NET application frameworks, custom controls or just likes to separate his concerns, will have come across the same problem I was having. I hope I can help some of you without having to go to the lengths I did, but let me start with explaining the problem. It's a pretty long story so hold on; the level in this article ranges from beginner to advanced in a lot of different areas.
The problem
When you want to develop Custom Controls with Javascript call-back functionality, you can use the ICallBack and IScriptable interface. These interfaces implement a call from your class and a return call to your class by use of Javascript, without having to implement an update panel or page refresh. The ICallback interface has a problem, though: it has no nice JSON (De)Serialization. It's confined to one function per component and although implementing more is possible, it is not very manageable.
How it should be
If you have ever worked with Webservices in ASP.NET, you will have seen the power of this mechanism. This is especially the case when in combination with ASP.NET Ajax 1.0, which generates a Javascript proxy for you. For those of you who haven't used ASP.NET Webservices, here's a small recap: You create an ASMX file in your project, fill it with a class containing a few attributes meant for Webservices and you are done! No SOAP creation, no JSON (DE)Serialization, no proxy writing. The only thing you still have to do is to register it with your <ScriptManager> for it to generate the Javascript Proxy. Would this not be great to have in our custom control library?
Yet another problem
The big problem with this approach is that this functionality only works with not-yet-compiled ASMX files in the web directory. You have no "out of the box" way of pointing this functionality to a compiled assembly. To take on this problem, we first take a look at how the current functionality works.
How it works by default
The ASMX files are translated into a web service and Javascript proxy through an HTTPHandler. This HTTPHandler is set in the config.xml file.
<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="*.asmx" validate="false"
type="System.Web.Script.Services.ScriptHandlerFactory"/>
</httpHandlers>
The handler you see here is the one that is created by default in a new "ASP.NET Ajax-enabled Web Application." When a request is done for a file with the extension ASMX ("*.asmx
"), it will forward that Request to the ScriptHandlerFactory
. The scripthandler factory then creates the web service, Javascript Proxy or Javascript Method handler for you. When you add the web service to the ScriptManager
, it will generate a <script src="Webserver.asmx/js"/>
on your webpage. The problem with this class is that it will check for the ASMX file and nothing else. The class has a bigger problem, in that almost everything is Internal
and Private
. If you don't know what that means, it means that extending or changing anything is impossible.
Reflect it
Okay, maybe it's not impossible. Everything compiles into managed code and, with reflection, we can get into every little corner we like. Even private constructors can be reached through one of the most powerful components of the .NET Framework. There are a lot of tutorials out there about Reflection, so in this article I'm going to assume you know about the basics.
Beginning the solution
We start out with a new empty "ASP.NET AJAX-enabled Web application," which comes with ASP.NET Ajax 1.0. Then we add a second empty C# class library project as a second project and we reference the second project in the first.
AutoDiscoveryWebServiceHandlerFactory
To implement our own code, we are going to create our own HttpHandlerFactory in our CustomControl Library. We name it AutoDiscoveryWebServiceHandlerFactory with the interface IHttpHandlerFactory, which implements :
public IHttpHandler GetHandler(HttpContext context,
string requestType, string url, string pathTranslated)
and
public void ReleaseHandler
Then in the Web Application, we change the config.xml file to forward ASMX requests to our own handler factory.
<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="*.asmx" validate="false"
type="Devusion. Services.AutoDiscoveryWebServiceHandlerFactory"/>
</httpHandlers>
Custom web service
Next we create our own web service, not the normal ASMX file, but a normal C# class file.
namespace Devusion.CustomControls
{
[ScriptService]
[WebService(Namespace = "<a href="%22http:
public class Service : WebService
{
[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public string HelloWorldYou(string name)
{
return DateTime.Now.ToLongTimeString()+ ": Hello " + name;
}
}
}
The WebServiceHandlerFactory
Now let's write the code that makes it possible to call /Devusion.CustomControls.Service.asmx. This will call our AutoDiscoveryWebServiceHandlerFactory.GetHandler function. Here we have to translate the URL to the Type of the class. We do this by looping over the loaded assemblies and checking for web service attributes. This should be optimized for better performance. If we find no matching type, we will return null.
private Type GetServiceType(string TypeName)
{
foreach (
Assembly loadedAssembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type ClassType = loadedAssembly.GetType(TypeName);
if (ClassType != null)
return ClassType;
}
return null;
}
In Gethandler, we write:
Type WebServiceType =
this.GetServiceType(Path.GetFileNameWithoutExtension(pathTranslated));
if (WebServiceType == null)
{
IHttpHandlerFactory ScriptHandlerFactory =
(IHttpHandlerFactory)System.Activator.CreateInstance(
AjaxAssembly.GetType(
"System.Web.Script.Services.ScriptHandlerFactory"));
UsedHandlerFactory = ScriptHandlerFactory;
return ScriptHandlerFactory.GetHandler(
context, requestType, url, pathTranslated);
}
The code here will get the WebServiceType. If no type is found, it will forward the request to the default Ajax Framework ScriptHandlerFactory, using reflection because it is a internal class. Almost all functionality for the ScriptHandlerFactory that normally handles the web service and Javascript proxy are either private or internal. Note that most of the functionality you will see from here on was found using the Reflector.net tool. The web service handler can be accessed with 3 different requests:
A normal web service Method request (/Devusion.CustomControls.Service.asmx?......)
A Javascript Proxy Generation request (/Devusion.CustomControls.Service.asmx/js or /jsdebug)
A Javascript method Call ( /Devusion.CustomControls.Service.asmx/method )
First, we check for a normal or Javascript request:
IHttpHandlerFactory JavascriptHandlerFactory =
(IHttpHandlerFactory)System.Activator.CreateInstance(
AjaxAssembly.GetType("System.Web.Script.Services.RestHandlerFactory"));
System.Reflection.MethodInfo IsScriptRequestMethod =
JavascriptHandlerFactory.GetType().GetMethod("IsRestRequest",
BindingFlags.Static | BindingFlags.NonPublic);
if ((bool)IsScriptRequestMethod.Invoke(null, new object[] { context }))
{
}
else
{
}
Then for the Javascript request we check for a proxy request or a method request:
bool IsJavascriptDebug =
string.Equals(context.Request.PathInfo, "/jsdebug",
StringComparison.OrdinalIgnoreCase);
bool IsJavascript = string.Equals(context.Request.PathInfo, "/js",
StringComparison.OrdinalIgnoreCase);
if (IsJavascript || IsJavascriptDebug)
{
}
else
{
}
Now for actions in the 3 request types.
The JavaScript proxy request
Here we do a lot more reflection; every reflection is explained in the comments. We create a WebServiceData with WebServiceType, which will then contain all of the information needed to create the web service. We directly use this object in the GetClientProxyScript method, which is a method of the object WebServiceClientProxyGenerator. It then returns the Javascript proxy.
ConstructorInfo WebServiceDataConstructor = AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData").GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance, null,
new Type[] { typeof(Type), typeof(bool) } , null);
ConstructorInfo WebServiceClientProxyGeneratorConstructor =
AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceClientProxyGenerator").GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance, null,
new Type[] { typeof(string), typeof(bool) }, null);
MethodInfo GetClientProxyScriptMethod =
AjaxAssembly.GetType(
"System.Web.Script.Services.ClientProxyGenerator").GetMethod(
"GetClientProxyScript", BindingFlags.NonPublic | BindingFlags.Instance,
null, new Type[] { AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData") }, null);
new WebServiceData(WebServiceType));
string Javascript = (string)GetClientProxyScriptMethod.Invoke(
WebServiceClientProxyGeneratorConstructor.Invoke(
new Object[] { url, IsJavascriptDebug }), new Object[]
{
WebServiceDataConstructor.Invoke(
new object[] { WebServiceType, false })
}
);
Then for a little caching, this was almost literally taken from the original unit with Reflector.
DateTime AssemblyModifiedDate = GetAssemblyModifiedTime(
WebServiceType.Assembly);
string s = context.Request.Headers["If-Modified-Since"];
DateTime TempDate;
if (((s != null) && DateTime.TryParse(s, out TempDate)) && (
TempDate >= AssemblyModifiedDate))
{
context.Response.StatusCode = 0x130;
return null;
}
if (!IsJavascriptDebug && (
AssemblyModifiedDate.ToUniversalTime() < DateTime.UtcNow))
{
HttpCachePolicy cache = context.Response.Cache;
cache.SetCacheability(HttpCacheability.Public);
cache.SetLastModified(AssemblyModifiedDate);
}
Then we need to get the Javascript Proxy String back to the client trough an HTTPHandler.
internal class JavascriptProxyHandler : IHttpHandler
{
string Javascript = "";
public JavascriptProxyHandler(string _Javascript)
{
Javascript = _Javascript;
}
bool IHttpHandler.IsReusable { get { return false; } }
void IHttpHandler.ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/x-Javascript";
context.Response.Write(this.Javascript);
}
}
Then we create and return in in the GetHandler code:
HttpHandler = new JavascriptProxyHandler(Javascript);
return HttpHandler;
That's it for the Javascript proxy. Let's get on with the other, simpler two.
The JavaScript method request
When you use the methods generated in the Javascript Proxy, it will access the web service with /methodname. The reason this handler is different from the normal web service handler (SOAP/XML), is that this interface needs to translate between Javascript JSON in our C# Methods.
IHttpHandler JavascriptHandler =
(IHttpHandler)System.Activator.CreateInstance(
AjaxAssembly.GetType("System.Web.Script.Services.RestHandler"));
ConstructorInfo WebServiceDataConstructor = AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData").GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance, null,
new Type[] { typeof(Type), typeof(bool) } , null);
MethodInfo CreateHandlerMethod = JavascriptHandler.GetType().GetMethod(
"CreateHandler", BindingFlags.NonPublic | BindingFlags.Static, null,
new Type[] { AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData"), typeof(string) }, null);
HttpHandler = (IHttpHandler)CreateHandlerMethod.Invoke(
JavascriptHandler, new Object[]
{
WebServiceDataConstructor.Invoke(
new object[] { WebServiceType, false }),
context.Request.PathInfo.Substring(1)
}
);
return HttpHandler;
We create the JavascriptHandler and, like in the previous handler, the WebServiceData. Then we call CreateHandler with the WebServiceData as Parameter, which returns the correct HTTPHandler.
Normal web service request
The normal request is simpler. The class is public and only the function that receives WebServiceData instead of a filepath is private. We use this function to get another HttpHandler and return it.
IHttpHandlerFactory WebServiceHandlerFactory = new WebServiceHandlerFactory();
UsedHandlerFactory = WebServiceHandlerFactory;
MethodInfo CoreGetHandlerMethod = UsedHandlerFactory.GetType().GetMethod(
"CoreGetHandler", BindingFlags.NonPublic | BindingFlags.Instance);
context.Request, context.Response);
HttpHandler = (IHttpHandler)CoreGetHandlerMethod.Invoke(UsedHandlerFactory,
new object[]
{
WebServiceType, context, context.Request, context.Response
}
);
return HttpHandler;
Default.aspx
Now to see it all in action. First, we add a web service reference to the ScriptManager:
<asp:ScriptManager ID="ScriptManager1" runat="server" >
<Services>
<asp:ServiceReference Path="Devusion.CustomControls.asmx" />
</Services>
</asp:ScriptManager>
Below it, we put:
<script type="text/Javascript">
function RunHelloWorld(name)
{
name = prompt("Please enter your name :","John Doe");
Devusion.CustomControls.Service.HelloWorldYou(name,
HelloWorldDone,WebServiceCallFailed,'');
}
function HelloWorldDone(result,context,senderFunction)
{
alert(result);
}
function WebServiceCallFailed(error,context,senderFunction)
{
alert(error.get_message());
}
</script>
<input type="button" onclick="RunHelloWorld();" value="Click Here" />
You can now place a breakpoint in the HelloWorldYou method in the WebService class in the service.cs file. Just run the page and let the magic of reflection do the rest! In this example, I did not implement the actual custom control or the preferred script# implementation of all the Javascript in combination with some Silverlight. I will divulge on that in future articles.
Demo code
The attached ZIP is a complete demo solution. It should open and build directly, so let me know if there are any problems. The code was created using Visual Studio 2005 Professional and ASP.NET Ajax 1.0.
History
- 30 May, 2007 - Original version posted