ASP.Net MVC routing customization
Introduction.
For the sake of this article, I am going to assume you are already at least basically familiar with
Prototype. My goal is not to teach how to use Prototype, but rather how Prototype might be used with
ASP.Net MVC extensions with extended routing functionality to build flexible and scalable application.
Using explained bellow way you can send AJAX request to the same url and fire different controllers and
actions. Required controller and action name defined inside posted data. I prefer use data in JSON format
because it is compact and easy in developing process and .Net 3.5 framework has functionality to convert
data to/from JSON format. You can create your own data format provider instead of JSON or XML and link up
to solution.
Prototype JavaScript Framework enables you to deal with Ajax calls in a very easy way and it is also
cross-browser. It provides few simple functions to encapsulate AJAX request.
Client side.
Once we have downloaded latest Prototype version, drop the script into /js folder in MVC project (I usually have all javascript files inside \js folder, you can keep in your preferred folder, it does not matter). Anything you are going to do with Prototype requires this file. Depending how often you use Prototype on your site, you might want to put the script on your MasterPage or if you only use it on a single page, you could drop it on that specific page. Running aspx pages much slowly than html and in some specific situations may be better to keep web gui as html files. We create a simple html file with one javascript function that fires by click on the button(s) and sends request to server to get product data by product id and populates html page with gotten data.
function sendRequest(url, controller, action)
{
if (!controller) controller = "";
if (!action) action = "";
var jsonObject = {"controller": controller, "action": action, "data":{"id": $("productid").value}};
var http = new Ajax.Request(url,
{
method:'post' ,
parameters: Object.toJSON(jsonObject),
contentType: "x-json",
onSuccess: function(transport)
{
if (transport.responseText)
{
var json_response = transport.responseText.evalJSON();
if (json_response.error)
{
alert(json_response.error);
}
else
{
$("gotten_productId").innerHTML = json_response.id;
$("gotten_productName").innerHTML = json_response.name;
}
}
else
{
alert("Success! \n\nno response text");
}
},
onFailure: function(){ alert('Something went wrong...') }
});
}
Here controller name and action name defined inside JSON data the same as product id. To be more
correct we defined following request protocol:
controller = "…."
action = "…"
data = {custom data}
Your application will have own protocol, for example you need pass protocol version number, some
authorization key and so on….. You are free with protocol definition, the same as data format. Just
implement required functionality. May be you are confused with term "protocol" here but all your
applications used it, you just did not name it as protocol. For example: you send request like
http://somehost.com/product.aspx?id=4&action=edit. in this case you send to server id and action, it is
your particular protocol of product.aspx.
In our example JSON object sent to server looks as
var jsonObject = {"controller": controller, "action": action, "data":{"id": $("productid").value}};
Routing.
Controller name and action name are defined in the JSON object and url does not keep information required by default ASP.Net MVC routing functionality. Even in some your particular situation you can define MVC View name in the protocol as well. If your application requires JSON object interchanging only then you can build application using one the same url for all requests using different controllers and actions. As we mentioned above default ASP.Net MVC routing functionality does not work and we need create custom routing. All "MVC routing requests" are handled by the MvcRouteHandler class which will simply return an instance of the MvcHandler type. We create custom routing handler and associated http handler. The route handler derives from IRouteHandler and will be the class used when creating your json request routing.
public class JSONRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new JSONMvcHandler(requestContext);
}
}
We define one url JSON/json for our json requests with corresponding custom route handler
JSONMvcHandler and register route in the Global.asax as
routes.Add(new Route("JSON/json", new JSONRouteHandler()));
The http handler derives from MvcHandler because it gives us some critical information,
like RequestContext, required for controller and action name definitions. Our http handler
JSONMvcHandler overrides ProcessRequest of default MvcHandler to create controller based on controller
name and define action name of Route instance from posted data. Another different posted data by client
we can keep in the DataTokens for later using in the controller.
protected override void ProcessRequest(HttpContextBase httpContext)
{
IServiceAPI serviceAPI = this.GetServiceAPI();
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(RequestContext, serviceAPI.controller);
if (controller == null)
{
throw new InvalidOperationException(String.Format(
"The IControllerFactory '{0}' did not return a controller for named '{1}'.",
factory.GetType(),
serviceAPI.controller));
}
try
{
this.RequestContext.RouteData.Values.Add("controller", serviceAPI.Controller);
this.RequestContext.RouteData.Values.Add("action", serviceAPI.Action);
this.RequestContext.RouteData.DataTokens.Add("data", serviceAPI.Data);
controller.Execute(this.RequestContext);
}
finally
{
factory.ReleaseController(controller);
}
}
Regarding our protocol definitions we create simple ServiceAPI class container to keep posted data
splitted by your protocol properties. As we know, IIS gets our posted JSON object as string and
prototype encodes service symbols to hex codes. We decode service symbols and deserialize JSON
string to Dictionary and populate ServiceAPI instance. Controller property keeps controller name,
action property keeps acttion name, data property keeps some posted data.
private IServiceAPI GetServiceAPI()
{
JavaScriptSerializer jss = new JavaScriptSerializer();
System.IO.Stream stream = this.RequestContext.HttpContext.Request.InputStream;
byte[] byteArray = new byte[stream.Length];
stream.Read(byteArray, 0, Convert.ToInt32(stream.Length));
string s = System.Text.Encoding.ASCII.GetString(byteArray);
s = this.HexString2Ascii(s);
object json_object = jss.DeserializeObject(s);
Dictionary json_dictionary = json_object as Dictionary;
JSONServiceAPI p = new JSONServiceAPI()
{
controller = json_dictionary["controller"].ToString(),
action = json_dictionary["action"].ToString(),
data = json_dictionary["data"],
};
return p;
}
Controller.
For example our controller name is JSONTest and action GetProduct in the case need call our javascript funcation as:
sendRequest("JSON/json", "JSONTest", "GetProduct");
by click on some button of web page and define controller class JSONTestControler like
public class JSONTestController : ControllerBase.JSONControllerBase
{
[AcceptVerbs("POST")]
public ActionResult GetProduct()
{
object dataToken = this.ControllerContext.RouteData.DataTokens["data"];
Dictionary data = dataToken as Dictionary;
int productId;
if (
data != null
&&
data.Keys.Contains("id")
&&
Int32.TryParse(data["id"].ToString(), out productId)
)
{
Models.Product p = new Models.Product();
p.Populate(productId);
ViewData["product"] = new { id = p.list[productId].id, name = p.list[productId].name};
}
else
{
ViewData["product"] = new { error = "Incorrect product id" };
}
return View("AnotherJSON");
}
[AcceptVerbs("POST")]
public ActionResult Index()
{
Models.Product p = new Models.Product();
p.Populate(0);
ViewData["product"] = new { id = p.list[0].id, name = p.list[0].name };
return View("json");
}
}
It looks as usual ASP.Net MVC controller with a few peculiarity, product id is coming from
DataTokens["data"] we saved in the our JSONMvcHandler and important tricky, our controller extends our
base class JSONControllerBase overrides base View function. As result we get customized flexible View
definition.
public abstract class JSONControllerBase : Controller
{
protected override ViewResult View(string viewName, string masterName, object viewData)
{
if (!HttpContext.Request.Headers["Content-Type"].Contains("x-json"))
{
viewName = "error";
}
string noun = "JSON";
string fullViewName = string.Format("~/Views/{0}/{1}.aspx", noun, viewName);
return base.View(fullViewName, masterName, viewData);
}
}
Our controller calls View with parameter "AnotherJSON" and it is viewName parameter in the overrided View
functionality. As you can see we have a full control on View definition and even we can have one View for
all controllers with hard coding fullViewName. You can hard code View name if your application requires
JSON object interchanging only. As result your application will have just one View, it is not usual for
ASP.Net MVC, but it is possible and if it is good for you, if it keeps your developing time then why do
not have it.
View.
In our example controller creates anonymous object and saves it inside ViewData dictionary with key product: ViewData["product"] = new {id=….., name=….} and calls View named as AnotherJSON. All business logic of any application should be defined in the Model and sometimes in the Controller. View should not keep any business logic, just sends required object to client as JSON string. We create AnotherJSON View that clears Response because IIS buffered response data already and writes serialized ViewData["product"].
public partial class AnotherJSON : ViewPage
{
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.SendResponse();
}
private void SendResponse()
{
JavaScriptSerializer jss = new JavaScriptSerializer();
StringBuilder output = new StringBuilder();
jss.Serialize(ViewData["product"], output);
Response.Clear();
Response.ContentType = "x-json";
Response.Write(output.ToString());
Response.Flush();
Response.End();
}
}
That’s it.
Final Thoughts.
We created one route JSON/json to process a lot requests to different controllers and different actions, posted data keeps controller name and action name. We created one View to send objects back to client browser from a lot different controllers. And what is important we keep easy unit testing of controllers, need set required test object in the DataTokens["data"]. Furthermore we can build easy regression testing functionality. Need define requests corresponding responses files and an application that will read request, send to server and compare gotten response with required corresponding response. And one more too flexible feature we got, client side and server side parts can be developments independent. Application architect defines client-server application API. Using this API client side developers create request/response JSON test objects and can develop client part without server part, server side developers create request/response c# test objects and can develop server part without client. Using this developing way you can get finished application much faster.Contact universal@vitana-group.com if you have any questions or found a bug.
Source code you can get here.