Introduction
In this article I would like to show
you the way how to build a flexible and scalable application with the help of
dhtmlx components and ASP.Net MVC with extended routing functionality. To begin
with, I'm going to assume that you are already familiar with
jQuery, at least basically, as it is a part of ASP.Net MVC application by
default. Let me also guess that you've heard about DHTMLX, an Ajax components library for building rich web
UI. My goal is not to teach you how to use dhtmlx components, but rather how dhtmlx
components might be effectively used with ASP.Net MVC with extended routing
functionality to build a flexible and scalable application.
Most of dhtmlx
components (such as grid, tree, combo, etc.) use the same client-server communication
mechanism, and there is no need to describe it for each component. As an
example I'll take dhtmlxTree to show you a fast way for web application
development. Using explained bellow way you can send Ajax request to the same url and fire
different controllers and actions. Required controller and action name are
defined inside posted data. I prefer to use data in JSON format because it is
compact and easy in developing process. Besides, .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 the solution.
Further, I will demonstrate how to use dhtmlxTree component to build a simple directory
manager application. The tree is used to show directory structure and input
type=text box to define a new sub folder name.
Getting Started
Client Side
Once we have downloaded the latest dhtmlxTree
component be sure you have file dhtmlxtree_json.js to support json data format,
drop the script into \dhtmlx folder in MVC project (I usually have all dhtmlx files
inside \dhtmlx folder, but you can keep them in your preferred folder, it does
not matter). When you create ASP.Net MVC project, Visual Studio places required
jQuery files in the \Scripts folder automatically. dhtmlx has enough components
to build major functionality of common web applications and usually you will
create html files for client-side functionality. Let's create a simple html file
with div to keep tree and input type=text control to define a new folder name.
The tree will be populated by onload and will be expanding tree node with the
following javascript code:
function
doOnLoad() {
tree = new
dhtmlXTreeObject(document.getElementById('treeBox'),
"100%", "100%",
0);
tree.loadJSON("JSON/json?controller=Tree&action=List");
tree.setImagePath("dhtmlx/imgs/");
tree.attachEvent("onSelect", function()
{ });
tree.setXMLAutoLoading(
function(id)
{
tree.loadJSON("JSON/json?controller=Tree&action=List&path="
+ getParents(id));
}
);
tree.setXMLAutoLoadingBehaviour("function");
}
Here controller name and action name
are defined inside query string data the same as path to show subfolders. The
good thing about dhtmlxTree is that it supports auto loading required data. You
do not need to build big hierarchical structure on the server, just define setXMLAutoLoading and tree will send requests to get tree node children when
there is a need show them. Creating sub folder uses jQuery functionality to
send AJAX
request.
function
createSubFolder() {
var pId
= tree.getSelectedItemId();
var
name = document.getElementById("folderName").value;
if
(name == "")
return;
var
data = {
'controller'
: 'Tree'
, 'action'
: 'CreateSubFolder'
, 'path':
getParents(pId)
, 'name'
: name
};
jQuery.ajax({
'type': "POST",
'url': 'JSON/json',
'data': data,
'dataType': 'json',
'error': function()
{ alert('Error occurred. Please contact the
administrator') },
'success': function(r)
{
tree.insertNewChild(pId,
r.id, r.name, null, "folderClosed.gif",
"folderOpen.gif", "folderClosed.gif");
document.getElementById("folderName").value = '';
}
});
}
NOTE: here I keep all parameters in
the JSON object and it looks like I will send data in JSON format but it is not
true. Even though I defined all sending data in the JSON format against
Prototype the jQuery automatically converts this JSON object to standard form data
post string and sends in "application/x-www-form-urlencoded" content
type. You need to use Prototype or another tool to send data as json string in x-json Content-Type. Request
executed successfully, I get data as JSON object and create a new tree node
with tree.insertNewChild.
In my example controller and action
parameters are required for APS.Net MVC routing. In both examples controller is
the same 'Tree' but actions are different: List and 'CreateSubFolder'
Routing
Default MVC routing looks for
controller and action names in the url but I pass them as parameters in the
query string. MVC routing requires our explanation how get required MVC data
with extending default functionality and you can extend a little described
routing to add supporting MVC View name as parameters. All "MVC routing
requests" are handled by the MvcRouteHandler class which will simply return an
instance of the MvcHandler type. I 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);
}
}
I define route url JSON/json with
custom route handler JSONRouteHandler 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 me 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 I keep in the DataTokens for later using in the controller.
protected
override void
ProcessRequest(HttpContextBase httpContext)
{
ServiceAPI
serviceAPI = new ServiceAPI(this.RequestContext.HttpContext.Request);
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 data definitions I
create simple ServiceAPI class container to extract data from HttpRequestBase
instance and keep it.
public
ServiceAPI(HttpRequestBase request)
{
// read
data from query string
this.populateFromCollection(request.QueryString);
if (
request.Headers["Content-Type"] != null
&& request.Headers["Content-Type"].Contains("x-json")
)
{
//
read data from stream if data sent in json format with Prototype for example
this.populateFromJSONStream(request.InputStream);
}
else
{
//
read data from form collection
this.populateFromCollection(request.Form);
}
}
As you can see this class supports
passed data in QueryString, Form collection and JSON formats and can be
extended with another formats determined by "Content-Type". (I
agree if you say that it'd be much better to keep each data processing
functionality per content type in its own class, but design patterns are not
the goal of this article and as it is a template application and this class
supports three types only, I allow myself to ignore patterns. Of course, in a
live project you need to follow design patterns.). As I mentioned above jQuery
sends data to server in Content-Type: application/x-www-form-urlencoded
and we can easy go through request form collection as NameValueCollection instance to get required controller, action and other
sent data.
private void
populateFromCollection(NameValueCollection
collection)
{
foreach
(string key in
collection.Keys)
{
if
(key.Equals("controller"))
{
this.controller
= collection[key];
}
else
if (key.Equals("action"))
{
this.action
= collection[key];
}
else
{
if
(this.data == null)
{
this.data = new Dictionary<string,
object>();
}
((Dictionary<string, object>)this.data).Add(key, collection[key]);
}
}
}
If you send data in x-json format
then it's necessary to convert hex input stream to ASCII string and deserialize
with JavaScriptSerializer.
Controller
In my example controller name is Tree
and action is List, CreateSubFolder ,and I create TreeController class corresponding public function:
[AcceptVerbs("GET")]
public ActionResult List()
{
string
parentId = "0";
string
path = Request.ServerVariables["APPL_PHYSICAL_PATH"]
+ this.workFolder;
Dictionary<string, object>
data = this.RouteData.DataTokens["data"] as
Dictionary<string,
object>;
if
(data != null && data.ContainsKey("path"))
{
path += data["path"];
}
Models.Folder
folder = new Models.Folder(path);
if
(data != null && data.ContainsKey("path"))
{
parentId = folder.Parent.Id;
}
ViewData["result"]
= this.Folders2Tree(parentId,
folder.GetChildren());
return
View("json");
}
[AcceptVerbs("POST")]
public ActionResult CreateSubFolder()
{
string
path = Request.ServerVariables["APPL_PHYSICAL_PATH"]
+ this.workFolder;
Dictionary<string, object>
data = this.RouteData.DataTokens["data"] as
Dictionary<string,
object>;
if
(data != null && data.ContainsKey("path"))
{
path += data["path"];
}
Models.Folder
folder = new Models.Folder(path);
Models.Folder
newFolder = folder.CreateSubFolder(data["name"]
as String);
ViewData["result"]
= new
{
id = newFolder.Id
, name = newFolder.Name
};
return
View("json");
}
This controller creates anonymous instances
with this.Folders2Tree in first and plain for next converting to JSON format with
View and calls required View. Property names of anonymous instance are
identical as required by dhtmlxTree component because View("json") just
converts to JSON.
private
object Folders2Tree(string
rootId, IEnumerable<Models.Folder> folders)
{
var
tree = new {
id = rootId,
item = new
List<object>()
};
foreach
(Models.Folder folder in folders)
{
tree.item.Add(new {
id = folder.Id
, text = folder.Name
, child =
folder.HasChildren?"1":"0"
, im0 = "folderClosed.gif"
, im1 = "folderOpen.gif"
, im2 = "folderClosed.gif"
});
}
return
tree;
}
The controller looks as a usual
ASP.Net MVC controller with a few peculiarities: directory name and path are
coming from DataTokens["data"] we saved in our JSONMvcHandler. And an important
trick - our controller extends our base class JSONControllerBase overrides base
ViewResult function. As a result we get
customized flexible View definition.
public abstract class JSONControllerBase
: Controller
{
protected override ViewResult
View(string viewName, string masterName, object
viewData)
{
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 "json" (View("json")) and it is viewName parameter in the overridden View
functionality. As you can see, we have a full control on View definition
and we even 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 a 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 and it saves your developing time then why not to have it.
View
In our example the controller
creates anonymous object and saves it inside ViewData dictionary with key result (ViewData["result"]) and calls View("json"). All business logic of your application should be defined
in the Model and sometimes in the Controller. View should not keep any business
logic, in my example it just sends required object to client as JSON
string. I create json View that clears any Response because IIS buffered response
data already and writes serialized ViewData["result"].
public partial class json: 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["result"], output);
Response.Clear();
Response.ContentType = "x-json";
Response.Write(output.ToString());
Response.Flush();
Response.End();
}
}
Please note, it is another trick,
View is ASP.Net page extended ViewPage.
That's it.
Final Thoughts
We've created one route JSON/json to process multiple requests
to different controllers and different actions. Posted data keeps controller
name and action name. We've also created one View to send data back to client
browser from lots of different controllers. And what is important, we keep easy
unit testing of controllers, just need to set required test object in the
DataTokens["data"]. Furthermore, we can build easy regression testing
functionality. We just need to define requests corresponding responses files
and an application that will read request, send to server and compare received
response with required corresponding response.
And one more flexible feature we
got: client side and server side parts can be development independent. An application
architect defines client-server application API and developers create test
requests data and test JSON responses. Using these test data, client-side
developers can develop client part without server part, and server-side
developers can develop server part without client. Using this way you can complete
an application much faster.