Download source code for this article.
Second part of the article is available here.
In my current project I need to cleanly separate logic of some API that resides in class library project from web sites that will use it.
If I use standard approach - to call server methods from javascript code - I have only two options: WebService call or PageMethod call. But I don't want to configure each site or each page - by decorating it with additional static method, or creating a webservice in the root of website or anywhere else. I WANT just to call my method directly from class library, because actually that class library will create client side script to access that method.
Of course I cannot call methods directly from class library, - I use HttpHandler for this. So to use my library there will be one thing that needs to be configured - handler should be defined in the web.config of target web site. But I personally think this is much less work, than adding webservice and calling my method from there.
In the sample code below I show how I did it for one single method - but I think it's not hard to change it to fit your needs if you want to call several methods. For example in my earlier post I show how we can use reflection to execute different methods on the server by method name.
Also note - for now I will attach javascript file to use class library method - but actually this will not be needed when script will be generated by class library itself. I am doing this just to simplify sample.
My method resides in class named LoaderService. This method will be used to load user controls in future API, for now it just returns some dummy data.
What I did:
1) Configured web.config
<httpHandlers> ... <!-- The following line configures http handler for GetContents method in LoaderService class--> <add verb="*" path="LoaderService.axd" validate="false" type="SaFlow.Core.LoadServiceHandler, SaFlow.Core"/> ... </httpHandlers>
type attribute points to class that implements IHttpHandler interface and will process the request from the client.
2) Created The class library named SaFlow.Core with following classes inside it -
ContentsResponse - simple object I am going to return as a result from my method
using System; using System.Collections.Generic; using System.Text; namespace SaFlow.Core { public class ContentsResponse { #region Constructor public ContentsResponse(string _html, string _script, string _customStyle) { html = _html; script = _script; customStyle = _customStyle; } #endregion #region Class level variables private string html = ""; private string script = ""; private string customStyle = ""; private object data = null; #endregion #region Properties public string Html { get { return html; } set { html = value; } } public string Script { get { return script; } set { script = value; } } public string CustomStyle { get { return customStyle; } set { customStyle = value; } } public object Data { get { return data; } set { data = value; } } public static ContentsResponse Empty { get { ContentsResponse result = new ContentsResponse(string.Empty, string.Empty, string.Empty); result.Data = null; return result; } } #endregion } }
Constants - simple class with constants - names of the parameters that my method accepts - I want to validate and be sure that all parameters were specified when calling it.
using System; using System.Collections.Generic; using System.Text; namespace SaFlow.Core { public static class Constants { public const string Parameter_ControlID = "controlID"; public const string Parameter_Data = "data"; } }
LoaderService - the class containing my method itself. its name is GetContents:
namespace SaFlow.Core
{
public classLoaderService
{
publicContentsResponse GetContents(string controlID, object data)
{
ContentsResponse response = newContentsResponse("test", "alert(1);", "");
response.Data = "test";
returnresponse;
}
}
}
LoaderServiceHandler - the class implementing IHttpHandler. This is where all magic happens :
using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Web; using System.Web.Configuration; using System.Web.Script.Serialization; namespace SaFlow.Core { public class LoadServiceHandler : IHttpHandler { #region Class level variables JavaScriptSerializer serializer = new JavaScriptSerializer(); #endregion #region IHttpHandler Members public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { try { // Get parameters from post request IDictionary<string, object> parameters = GetRawParams(context); if (!parameters.ContainsKey(Constants.Parameter_ControlID) || !parameters.ContainsKey(Constants.Parameter_Data)) throw new InvalidDataException(); // execute our method LoaderService loader = new LoaderService(); ContentsResponse response = loader.GetContents(parameters[Constants.Parameter_ControlID].ToString(), parameters[Constants.Parameter_Data]); string responseString = serializer.Serialize(response); // prepare headers to send correct response to the client string contentType = "application/json"; context.Response.ContentType = contentType; context.Response.Cache.SetMaxAge(new TimeSpan(0)); //write actual response if (responseString != string.Empty) { context.Response.Write(responseString); } } catch (Exception ex) { WriteExceptionJsonString(context, ex); } } #endregion #region Private methods private IDictionary<string, object> GetRawParams(HttpContext context) { if (context.Request.HttpMethod == "POST") { return GetRawParamsFromPostRequest(context); } else { throw new InvalidOperationException("POST should be used"); } } private IDictionary<string, object> GetRawParamsFromPostRequest(HttpContext context) { TextReader reader = new StreamReader(context.Request.InputStream); string bodyString = reader.ReadToEnd(); return serializer.Deserialize<IDictionary<string, object>>(bodyString); } #endregion #region Error handling internal class WebServiceError { public string Message; public string StackTrace; public string ExceptionType; public WebServiceError(string msg, string stack, string type) { Message = msg; StackTrace = stack; ExceptionType = type; } } internal void WriteExceptionJsonString(HttpContext context, Exception ex) { context.Response.ClearHeaders(); context.Response.ClearContent(); context.Response.Clear(); context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.Response.StatusDescription = HttpWorkerRequest.GetStatusDescription((int)HttpStatusCode.InternalServerError); context.Response.ContentType = "application/json"; context.Response.AddHeader("jsonerror", "true"); using (StreamWriter writer = new StreamWriter(context.Response.OutputStream, new UTF8Encoding(false))) { if (ex is TargetInvocationException) { ex = ex.InnerException; } if (context.IsCustomErrorEnabled) { writer.Write(serializer.Serialize(new WebServiceError("Error occured during service call", String.Empty, String.Empty))); } else { writer.Write(serializer.Serialize(new WebServiceError(ex.Message, ex.StackTrace, ex.GetType().FullName))); } writer.Flush(); } } #endregion } }
The most important method here is ProcessRequest. It handles the request data sent by the client. First it calls GetRawParams private method to parse request and get parameters with values, sent by javascript code. Then it validates that all parameters needed for the method are present. After this it simply creates instance of LoaderService class and executes it's method by passing retrieved parameters. If some error occures during request - exception is written to the response exactly the same way it is done in AJAX framework itself.
Some methods are taken from AJAX framework source code. I used it to simplify some matters when writing the code. For example methods WriteExceptionJsonString, GetRawParamsFromPostRequest, GetRawParams and class WebServiceError are actually the same that are doing job when calling REST webservices using AJAX.
After I called my method and have the results, I just serialize the result with JavaScriptSerializer class (see my previous post about JavaScriptSerializer for more info) and write to the response.
And that's it!
Now I want to show the client side code - how I call the method from javascript.
As I said I use javascript file that wraps all needed complexity of calling web resource, but it can be generated from inside class library itself. The code I used in my javascript library mimics standard javascript code that is generated for webservice when using AJAX - I just created one dummy webservice with signiture equal to class that will be called(LoaderService) and placed generated javascript in a separate file:
var LoaderService = function() { LoaderService.initializeBase(this); this._timeout = 0; this._userContext = null; this._succeeded = null; this._failed = null; } LoaderService.prototype = { GetContents : function(controlID,data,succeededCallback, failedCallback, userContext) { return this._invoke( LoaderService.get_path(), 'GetContents', false, {controlID:controlID,data:data}, succeededCallback, failedCallback, userContext ); } } LoaderService.registerClass('LoaderService',Sys.Net.WebServiceProxy); LoaderService._staticInstance = new LoaderService(); LoaderService.set_path = function(value) { var e = Function._validateParams( arguments, [{name: 'path', type: String}] ); if (e) throw e; LoaderService._staticInstance._path = value; } LoaderService.get_path = function() { return LoaderService._staticInstance._path; } LoaderService.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); } LoaderService._staticInstance._timeout = value; } LoaderService.get_timeout = function() { return LoaderService._staticInstance._timeout; } LoaderService.set_defaultUserContext = function(value) { LoaderService._staticInstance._userContext = value; } LoaderService.get_defaultUserContext = function() { return LoaderService._staticInstance._userContext; } LoaderService.set_defaultSucceededCallback = function(value) { var e = Function._validateParams( arguments, [{name: 'defaultSucceededCallback', type: Function}] ); if (e) throw e; LoaderService._staticInstance._succeeded = value; } LoaderService.get_defaultSucceededCallback = function() { return LoaderService._staticInstance._succeeded; } LoaderService.set_defaultFailedCallback = function(value) { var e = Function._validateParams( arguments, [{name: 'defaultFailedCallback', type: Function}] ); if (e) throw e; LoaderService._staticInstance._failed = value; } LoaderService.get_defaultFailedCallback = function() { return LoaderService._staticInstance._failed; } LoaderService.set_path("LoaderService.axd"); LoaderService.GetContents= function(controlID,data,onSuccess,onFailed,userContext) { LoaderService._staticInstance.GetContents(controlID,data,onSuccess,onFailed,userContext); }
And the page code looks not very different from when we use simple webservice:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager ID="ScriptManager1" runat="server"> <Scripts> <asp:ScriptReference Path="~/LoaderService.js" /> </Scripts> </asp:ScriptManager> <script type="text/javascript"> function GetData() { LoaderService.GetContents( "2", "3", GotResults, null, null ); } function GotResults(result) { $get("resultsDiv").innerHTML = result.Html; } </script> <div> <input type="button" value="Get" onclick="GetData();return false;" /> </div> <div id="resultsDiv"> </div> </form> </body> </html>
Quite simple..
Now I will show a little of internals of call to my method. I will use FireBug and Mozilla to see what is going on behind the call:
And if we stop on breakpoint inside GetContents method:
Now if you want to compare the request and response with ones that are sent when using WebServices or PageMethods - just create test webservice and see that the Headers, Post and Response data are the same.
Hope this helps.
hey hello ,
ReplyDeleteits very nice informative,
its really very informative for me,
i want to call .ashx file function() from another class (in App_Code folder)
how can i call handler function from class in asp.net
please replay me with example please sir