I have searched far and wide for the past week looking for a way to create a page method (a neat feature in ASP.NET which allows you to call static methods in your page decorated with an attribute as an AJAX web service) in an ASCX user control and possibly even a master page. First, a little background… (yes, there will be a code download link at the end of the article)
I am currently working on an enterprise level ASP.NET AJAX application to be used in a call center, as most of the applications I create are for use in call center automation and task management. Because Update Panels are evil and this application was riddled with them, I have been slowly but surely stripping them out one by one. The problem, the application loads 90% of its screens and functionality based on configuration data, dynamically building pages and loading user controls located in both ASCX files and server controls located in external assemblies bound at run-time. The major roadblock is that our middle tier is all WCF, which works great, however they are all on a different domain than the web application and use secure SOAP (not REST), therefore I can’t call them from JavaScript. Not only that, there are only 4 pages in the entire application, everything else is dynamic. With over 130+ and growing user controls, and plug-and-play custom functionality, it would be impossible to put all of these methods in copy-cat services using WCF in the web application itself. Did I mention that the site uses a dozen or so host headers in IIS, which rules out WCF hosted svc files in the web site right there. Page methods seemed like a perfect fit, only I wasn’t about to put 200+ web methods on a single page when only one or two user controls would ever use them. That would be stupid and the JavaScript would take forever to compile in even a fast browser.
Everything I found on the web searching Google, MSDN forums, user groups, etc has turned up one and only one answer to my dilemma: “This can not be done, only public, static methods on an actual ASPX page may contain page methods in this manner because ASCX controls do not have their own unique post-back address like a page does.” I thought to myself, “This is stupid!” Of all the brilliant things Microsoft has done with ASP.NET you would think that they would have built the ability to register methods dynamically on the page through a child control, whether a user control or master page, either as a callback, delegate, etc, at least some model for doing this, but alas, apparently myself and the hundreds of other posters out there have not been diligent in filling out feature requests for this item, which I will do sometime later this week.
The solution:
While ASCX user controls may not contain page methods (ScriptMethod), pages can. Duh, right! So why can’t I create a single, general page method in my base page class which all of my site pages inherit from, and provide a means, through reflection and custom attributes, along with my base user control class that all my ASCX user controls ultimately inherit from, to dynamically create JavaScript wrappers for calling my page’s single page method, which in turn knows how to invoke my user control type’s public static method, passing in any collection of parameters and even providing a wrapper for JSON serialization of the method’s result back to the client, INCLUDING ANONYMOUS TYPES!!! OK, now I’m on to something.
The end result is a custom attribute which my base user control class detects on static, invoking, public methods, which then registers some well-crafted JavaScript script block with the page’s ScriptManager, which in turn know how to invoke the page’s ScriptMethod dynamically passing in some information about the type name and method name to invoke through reflection.
Now, to the code
First, we need to create an attribute to attach to our ultra-nifty public static methods in our user controls that we want to expose to our client as ScriptMethods. I’m going to call my attribute UserControlScriptMethodAttribute, but you can call it whatever you want. This is a very simple attribute, not much to do here. You could add some validation that it’s on a static and public member, but that’s up to you.
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] public sealed class UserControlScriptMethodAttribute : Attribute { /// <summary> /// Initializes a new instance of the <see cref="UserControlScriptMethodAttribute"/> class. /// </summary> public UserControlScriptMethodAttribute() { } }
BasePage.cs
Then, I needed my base implementation for the “generalized” ScriptMethod. This would be on the page class that all of my pages inherit from, exposing the capability of using these ScriptMethod from any user control or master page that I want to.
There are several important things going on in the base page class. First, I needed to create my script method. This method has a simple signature, basically taking a fully qualified type name (including the assembly if need be), as well as the method name to invoke in that type and a boxed object array for any additional arguments which should be passed through as parameters when invoking the method. I’ve hard-coded my response format as JSON and HttpGet=false. You could theoretically create several versions of these method for different options and add these options as well to your custom parameter for consumption, but I didn’t need anything that sophisticated here.
[WebMethod(EnableSession = true)] [ScriptMethod(ResponseFormat = ResponseFormat.Json, UseHttpGet = false)] public static string PageServiceRequest(string typeName, string methodName, object[] args) { Type ctl = Type.GetType(typeName); if (ctl != null) { object o = ctl.InvokeMember( methodName, System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.InvokeMethod | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase, null, null, args ?? new object[]{}); if (o != null) { if (o is string || o.GetType().IsValueType) return o.ToString(); // If it is a string or value type, return a string // If it is a complex object, return a serialized version of it. JavaScriptSerializer serializer = new JavaScriptSerializer(); return serializer.Serialize(o); // allow anonymous types, etc } } return "{}"; // return an empty JSON object }
Notice that once I get the Type I need from the type name, getting the method using some simple binding flags is rather easy, then invoking it passing the arguments object array straight through. From there, I take the result of the method invocation and determine if it is a value type or a string, if it is, I simply return the value.ToString(), otherwise I’m serializing it to JSON and still returning a string (only this string is JSON). The JavaScriptSerializer even allows me to take an anonymous type from the invocation target and serialize it directly back to the client in JSON without having to any further work, which you’ll see a little later in this article, is really freakin’ cool!
Next, I need to expose a more friendly wrapper around the ScriptMethod in JavaScript so my user controls will have to know less about how to invoke this guy and it makes it a bit more usable.
private void RegisterPageServiceRequestProxy() { ScriptManager.RegisterClientScriptBlock( this, GetType(), "PageServiceRequestProxy", "function InvokeServiceRequest(typeName,methodName,successCallback,failureCallback){if(PageMethods.PageServiceRequest){try{var parms=[];for(var i=4;i<arguments.length;i++){parms.push(arguments[i]);}PageMethods.PageServiceRequest(typeName,methodName,parms,successCallback,failureCallback);}catch(e){alert(e.toString());}}}", true); }
Wow, this a really long, condensed line of JavaScript which you don’t necessarily need to understand, other than the fact that it really will make life easy for you. Essentially it creates a wrapper method taking the type name, method name, callbacks and allows you to pass in any number of additional arguments… as many as you want after the failure callback. This wrapper will then simply take those extra parameters and build an Array object to pass as the “args” parameter to the base ScriptMethod. I need to call this method OnInit as seen below:
protected override void OnInit(EventArgs e) { base.OnInit(e); RegisterPageServiceRequestProxy(); }
BaseUserControl.cs
Our base user control, which all ASCX pages will inherit is a bit more involved. For one, we need to obfuscate the innards of how our new attribute is consumed. We also want to hide all the minutia around passing fully qualified type names and method names, etc. We also want to expose some nicely formed methods on the client via JavaScript that can be called using the same method name and similar parameter signature as the method in the ASCX code-behind, with the exception of the callback client methods (the whole point of asynchronous client processing).
First, I want to emit and register my scripts and “stuff” during the Pre-Render phase of the page life-cycle, so I’m going to override OnPreRender to call my register method.
protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); RegisterUserControlWebMethods(); }
Next, let’s build out the implementation of our RegisterUserControlWebMethods method stub. I made it private because it doesn’t need visibility outside of our base class, remember our attribute will take care of this for us.
private void RegisterUserControlWebMethods() { foreach (MethodInfo method in this.GetType().GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod)) if (method.GetCustomAttributes(typeof(UserControlScriptMethodAttribute), true).Length > 0) RegisterUserControlWebMethod(method); Type baseType = this.GetType().BaseType; if (baseType != null && (baseType.Namespace == null || !baseType.Namespace.StartsWith("System"))) foreach (MethodInfo method in baseType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod)) if (method.GetCustomAttributes(typeof(UserControlScriptMethodAttribute), true).Length > 0) RegisterUserControlWebMethod(method); }
In the example above, you can see our initial search within the current type for any methods with the appropriate binding flags, being static, public and invoke, as well as a test for our custom attribute. If we find any, for each one that we find we’re going to further call our RegisterUserControlWebMethod (more on this in a few lines). You’ll notice that we also do a second round through reflection to get the base type and check any methods in there for these super special attribute decorated methods. I could have made this a bit cleaner in the recursion department, but again, I didn’t need to for my purposes, I’m sure there are better ways to do this. The reason I’m doing this second loop on base types is simple, depending on your project structure, either a Web Project or Web Site, you will find that your methods exist not in the current type, but the base type as the current type you have loaded is some temporary type drummed up in the CLR at run-time or located in a special page or directory assembly, etc.
Now that we’ve found the methods we need to be able to invoke on the client, we need to emit and register wrappers for those methods, obfuscating the details around type names, method names, etc and providing a neat and concise means of invoking them on the client.
private void RegisterUserControlWebMethod(MethodInfo method) { string blockName = string.Concat(method.Name, "_webMethod_uc"); StringBuilder funcBuilder = new StringBuilder(); funcBuilder.Append("function "); funcBuilder.Append(method.Name); funcBuilder.Append("(successCallback,failureCallback"); foreach (var par in method.GetParameters()) funcBuilder.AppendFormat(",{0}", par.Name); funcBuilder.Append("){if(PageMethods.PageServiceRequest){try{var parms=[];for(var i=2;i<arguments.length;i++){parms.push(arguments[i]);}PageMethods.PageServiceRequest("); funcBuilder.AppendFormat("'{0}','{1}'", method.DeclaringType.AssemblyQualifiedName, method.Name); funcBuilder.Append(",parms,successCallback,failureCallback);}catch(e){alert(e.toString());}}}"); ScriptManager.RegisterClientScriptBlock(this, GetType(), blockName, funcBuilder.ToString(), true); }
The RegisterUserControlWebMethod method takes the method info through reflection and builds a JavaScript function with the same name as the method, then adds the appropriate callback parameters which are always the first and second parameter respectfully, then takes the parameter collection (much like the base page registered method did) and passes those an a JavaScript Array, invoking the PageServiceRequest through the PageMethods namespace. When invoking the PageServiceRequest method, it also passes the fully qualified type name of the method info’s declaring type and the method name through reflection, keeping these values in the function wrapper as coded string values.
ServerTime.ascx
ServerTime is my example User Control which gets loaded on a simple page, Default.aspx. There i no code on Default.aspx, therefore there is no need to put anything here for it. All I did was drop the ServerTime.ascx user control onto the designer surface of Default.aspx in Visual Studio so the page would host my user control. Keep in mind I’m setting my base page type and base user control type in my web.confg as shown here:
<system.web> <pages pageBaseType="ScharfHoldings.BasePage" userControlBaseType="ScharfHoldings.BaseUserControl"
The code-behind file, ServerTime.ascx.cs looks like this below:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; public partial class ServerTime : ScharfHoldings.BaseUserControl { [ScharfHoldings.UserControlScriptMethod] public static string GetTime() { // Return a simple string to the client. This by-passes serialization since it is only a string. return DateTime.Now.ToLongTimeString(); } [ScharfHoldings.UserControlScriptMethod] public static ComplexTime GetComplexTime() { // Return a strongly typed object instance to the client. return new ComplexTime() { Time = DateTime.Now.ToLongTimeString(), MachineName = HttpContext.Current.Server.MachineName }; } [ScharfHoldings.UserControlScriptMethod] public static object GetAnonymousTime() { // Return an anonymous type instance to the client. // this anonymous type is intended to mimic our ComplexTime type, only it is anonymous, however will // behave the exact same when it is serialized to the client through JavaScript. return new { Time = DateTime.Now.ToLongTimeString(), MachineName = HttpContext.Current.Server.MachineName }; } [ScharfHoldings.UserControlScriptMethod] public static object GetTimeWithParameter(string clientMessage) { // Return an anonymous type instance to the client. // The clientMessage parameter gets passed in dynamically from the client and we're going to set the // MachineName property to this value to pass back from the server to prove we got the client value in the parameter. return new { Time = DateTime.Now.ToLongTimeString(), MachineName = clientMessage }; } [Serializable] public class ComplexTime { public string Time { get; set; } public string MachineName { get; set; } } }
As you can see I have 4 of my custom script methods to show-case the different capabilities built into this methodology. One is a simple call, with no parameters, which returns a string.
GetComplexTime takes no parameters and returns an object of type ComplexType which has a property, Time and MachineName.
GetAnonymousType showcases the ability for this method to return anonymous types to the client, which is a very powerful feature and presents itself more like JavaScript rather than .NET. The anonymous type I am created here is intended to look exactly like ComplexType for no other reason than to re-use code on client and show it can be done.
GetTimeWithParameter takes a simple type parameter (a string in this case) and returns another anonymous type, but this time plugging the parameter into the MachineName property instead of the server’s machine name. This again is for ease of example and code re-use.
Now, let’s take a look at the client side of this. First, I have a simple structure for displaying my data and a button to invoke all of my server methods asynchronously:
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="ServerTime.ascx.cs" Inherits="ServerTime" %> <div id="myTime"> <div id="time"></div> <div id="complexTime"></div> <div id="anonymousTime"></div> <div id="parameterTime"></div> <input type="button" id="refreshMe" value="Get Time" title="Click here to refresh the time from the server" onclick="refreshTime();return false;" /> </div>
As you can see, I have a DIV for each method result to show the results of each method call and a simple button labeled Get Time that will call a simple JavaScript function, that in turn calls all of my user control Script methods.
Here is what the page and user control look like upon their initial state:
Now we need to make the button do something interesting. Below is the JavaScript block that provides the method that executes all the other methods, refreshTime() as well as the callback methods for success and failure of each ScriptMethod call that we make. the last two methods are simply for displaying the results of the script methods in their respectful DIV elements.
<script type="text/javascript"> //<![CDATA[ function refreshTime() { try { // Call the UserControlWebMethod attributed functions on the server. GetTime(refreshTime_Success, refreshTime_Failure); GetComplexTime(refreshTimeComplex_Success, refreshTimeComplex_Failure); GetAnonymousTime(refreshTimeAnonymous_Success, refreshTimeAnonymous_Failure); GetTimeWithParameter(refreshTimeWithParameter_Success, refreshTimeWithParameter_Failure, 'the Client... Hi!'); } catch (e) { alert(e.message); } } // Delegate functions for the GetTime method function refreshTime_Success(result) { displayTime(result); } function refreshTime_Failure(result) { } // Delegate functions for the GetComplexTime method function refreshTimeComplex_Success(result) { displayComplexTime(result, $get('complexTime')); } function refreshTimeComplex_Failure(result) { } // Delegate functions for the GetAnonymousTime method function refreshTimeAnonymous_Success(result) { displayComplexTime(result, $get('anonymousTime')); } function refreshTimeAnonymous_Failure(result) { } // Delegate functions for the GetTimeWithParameter method function refreshTimeWithParameter_Success(result) { displayComplexTime(result, $get('parameterTime')); } function refreshTimeWithParameter_Failure(result) { } // Simply set the innerText of the simple time element from the raw result of the service method. function displayTime(timeAsString) { document.getElementById('time').innerText = timeAsString; } // Evaluate and parse out the JSON object to a type using eval, then build a display string and display the result. function displayComplexTime(timeAsObj, ctrl) { try { var obj = eval('(' + timeAsObj + ')'); if (obj) ctrl.innerText = obj.Time + ' on ' + obj.MachineName; } catch (e) { alert(e.message); } } //]]> </script>
As you can see, when we’re getting back an object (rather than a simple or value type) we have to call eval() on that object’s JSON string, wrapped in parenthesis in order to get the actual object instance from the JSON (otherwise it is still just a string). Once I have an instance from the JSON using eval, I can get property values from that object, such as you see here, which are exactly the same name as the properties specified in both the ComplexTime class, as well as both uses of the anonymous types.
The end result looks like this once I click the button (notice, depending on your computer’s speed, you may see all the lines appear or refresh at the same time or like me you may see a slight delay in the asynchronous processing of each of the methods.
Viola! That’s all there is to it. NEVER let anyone tell you ever again that you can’t create page methods in an ASCX user control, because using these technique, you most definitely can!
Source Code Download
I’ve created a simple, sample web site in Visual Studio 2008, .NET 3.5 to show off the capabilities and give you a starting point to play around with the code. This is the full working source code that I based the article on. I look forward to getting your comments, feedback and improvements you’ve made or found or any bugs you might have encountered. I’m really excited about using this functionality and have already been successfully incorporating it in our enterprise software with great success. Enjoy!
~ Chad.
