65.9K
CodeProject 正在变化。 阅读更多。
Home

DHTMLX 与 ASP.NET MVC

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2009年11月4日

GPL3

8分钟阅读

viewsIcon

33408

本文介绍 DHTMLX 组件如何与 ASP.NET MVC 结合使用,并通过扩展路由功能来构建灵活的 Web 应用程序。

引言

在本文中,我想向您展示如何借助 DHTMLX 组件和具有扩展路由功能的 ASP.NET MVC 来构建一个灵活且可扩展的应用程序。首先,我假设您已经熟悉 jQuery,至少是基础知识,因为它默认是 ASP.NET MVC 应用程序的一部分。我猜您也听说过 DHTMLX,这是一个用于构建富 Web UI 的 AJAX 组件库。

大多数 DHTMLX 组件(如 grid、tree、combo 等)使用相同的客户端-服务器通信机制,无需为每个组件单独描述。我将以 dhtmlxTree 为例,向您展示一种快速进行 Web 应用程序开发的方法。使用下面介绍的方法,您可以向同一 URL 发送 AJAX 请求,并触发不同的控制器和操作。所需的控制器和操作名称定义在提交的数据中。我倾向于使用 JSON 格式的数据,因为它紧凑且易于开发。此外,.NET 3.5 框架具有将数据转换为/从 JSON 格式的功能。您可以创建自己的数据格式提供程序而不是 JSON 或 XML,并将其链接到解决方案。

接下来,我将演示如何使用 dhtmlxTree 组件构建一个简单的目录管理器应用程序。该树用于显示目录结构,并包含一个 input type=text 框来定义新的子文件夹名称。

入门

客户端

下载最新 dhtmlxTree 版本后(您可以在 此处 获取),请务必包含 dhtmlxtree_json.js 以支持 JSON 数据格式。将脚本放入 MVC 项目的 \dhtmlx 文件夹中(我通常将所有 DHTMLX 文件放在 \dhtmlx 文件夹内,但您可以将其放在您喜欢的任何文件夹中,这无关紧要)。在创建 ASP.NET MVC 项目时,Visual Studio 会自动将所需的 jQuery 文件放在 \Scripts 文件夹中。DHTMLX 拥有足够的组件来构建常见 Web 应用程序的大部分功能,通常您将创建 HTML 文件来实现客户端功能。让我们创建一个简单的 HTML 文件,其中包含一个 div 用于放置树,以及一个 input type=text 控件用于定义新的文件夹名称。树将使用 onload 进行填充,并使用以下 JavaScript 代码展开树节点:

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");
}

在此,控制器名称和操作名称都定义在查询字符串数据中,子文件夹的路径也是如此。dhtmlxTree 的优点在于它支持自动加载所需数据。您无需在服务器端构建庞大的分层结构;只需定义 setXMLAutoLoading,当需要显示树节点子节点时,树就会发送请求来获取它们。创建子文件夹使用 jQuery 功能发送 AJAX 请求。

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 = '';
    }
    });
}

注意:在这里,我将所有参数都放在 JSON 对象中,看起来好像是要发送 JSON 格式的数据,但事实并非如此。尽管我定义了所有发送数据均为 JSON 格式(相对于 Prototype),jQuery 会自动将此 JSON 对象转换为标准的表单数据 POST 字符串,并以“application/x-www-form-urlencoded”的内容类型发送。您需要使用 Prototype 或其他工具将数据作为 JSON 字符串以 x-json 内容类型发送。请求已成功执行,我以 JSON 对象的形式获取数据并使用 tree.insertNewChild 创建新的树节点。在我的示例中,控制器和操作参数是 ASP.NET MVC 路由所必需的。在两个示例中,控制器都是相同的:Tree,但操作不同:List'CreateSubFolder'

路由

默认的 MVC 路由在 URL 中查找控制器和操作名称,但我将它们作为参数传递到查询字符串中。MVC 路由需要解释如何通过扩展默认功能来获取所需的 MVC 数据,您可以扩展路由以支持将 MVC 视图名称作为参数。所有“MVC 路由请求”都由 MvcRouteHandler 类处理,该类将简单地返回 MvcHandler 类型的实例。我创建了一个自定义路由处理程序和关联的 HTTP 处理程序。路由处理程序继承自 IRouteHandler,并在创建 JSON 请求路由时使用。

public class JSONRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new JSONMvcHandler(requestContext);
    }
}

我定义路由 URL JSON/json,并使用自定义路由处理程序 JSONRouteHandler,在 Global.asax 中注册路由,如下所示:

routes.Add(new Route("JSON/json", new JSONRouteHandler()));

HTTP 处理程序继承自 MvcHandler,因为它提供了关键信息,如 RequestContext,这对于控制器和操作名称的定义是必需的。我们的 HTTP 处理程序 JSONMvcHandler 重写了默认 MvcHandlerProcessRequest 方法,以便根据提交数据中的控制器名称创建控制器并定义 Route 实例的操作名称。客户端通过另一个提交数据被保存在 DataTokens 中供控制器稍后使用。

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);
    }
}

关于我们的数据定义,我创建了一个简单的 ServiceAPI 类容器来从 HttpRequestBase 实例中提取数据并保存。

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);
    }
}

如您所见,此类支持 QueryStringForm 集合以及 JSON 格式的传入数据,并且可以通过由“Content-Type”决定的其他格式进行扩展。(我同意,如果您说最好将每种内容类型的每个数据处理功能放在其自己的类中,但设计模式不是本文的目标,而且由于这是一个模板应用程序,并且此类仅支持三种类型,我允许自己忽略模式。当然,在实际项目中,您需要遵循设计模式。)如上所述,jQuery 使用 Content-Type:application/x-www-form-urlencoded 将数据发送到服务器,我们可以轻松地通过 NameValueCollection 实例遍历请求的表单集合以获取所需的控制器、操作和其他数据。

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,>();
            }
            ((Dictionary<string,>)this.data).Add(key, collection[key]);
        }
    }
}

如果以 x-json 格式发送数据,则需要将十六进制输入流转换为 ASCII 字符串,并使用 JavaScriptSerializer 进行反序列化。

控制器 (Controller)

在我的示例控制器中,名称是 Tree,操作是 ListCreateSubFolder,我创建了与公共函数对应的 TreeController 类:

[AcceptVerbs("GET")]
public ActionResult List()
{
    string parentId = "0";
    string path = 
      Request.ServerVariables["APPL_PHYSICAL_PATH"] + this.workFolder;
    Dictionary<string,> data = 
      this.RouteData.DataTokens["data"] as Dictionary<string,>;
    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,> data = this.RouteData.DataTokens["data"] as Dictionary<string,>;
    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.Folders2Tree,然后使用 View 转换为 JSON 格式并调用所需的 View。匿名实例的属性名称与 dhtmlxTree 组件所需的名称相同,因为 View("json") 仅进行 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;
}

该控制器看起来像一个常规的 ASP.NET MVC 控制器,但有一些特点:目录名称和路径来自我们在 JSONMvcHandler 中保存的 DataTokens["data"]。一个重要的技巧是,我们的控制器继承自我们的基类 JSONControllerBase,该基类重写了基类的 ViewResult 函数。结果是,我们得到了自定义的灵活 View 定义。

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);
    }
}

我们的控制器调用带参数“json”(View("json"))的 View,这在重写的 View 功能中是 viewName 参数。如您所见,我们对 View 定义拥有完全控制权,甚至可以为所有控制器使用一个 View,通过硬编码 fullViewName。如果您的应用程序只需要 JSON 对象交互,您可以硬编码 View 名称。结果是,您的应用程序将只有一个 View。这在 ASP.Net MVC 中不常见,但它是可能的,如果这对您有利并且可以节省您的开发时间,那么为什么不这样做呢。

视图

在我们的示例中,控制器创建一个匿名对象并将其保存在 ViewData 字典中,键为 result(ViewData["result"]),然后调用 View("json")。您应用程序的所有业务逻辑都应该定义在 Model 中,有时也在 Controller 中。View 不应该包含任何业务逻辑,在我的示例中,它只是将所需对象作为 JSON 字符串发送给客户端。我创建了一个 JSON View,它会清除任何 Response,因为 IIS 会缓冲响应数据,并写入序列化的 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();
    }
}

请注意另一个技巧,View 是 ASP.NET 页面扩展的 ViewPage。就这样。

最终想法

我们创建了一个路由 JSON/json 来处理对不同控制器和不同操作的多个请求。提交的数据包含了控制器名称和操作名称。我们还创建了一个 View,用于将数据从多个不同的控制器发送回客户端浏览器。重要的是,我们保持了控制器单元测试的简便性,只需在 DataTokens["data"] 中设置所需的测试对象即可。此外,我们可以轻松构建回归测试功能。我们只需为主请求定义相应的响应文件,以及一个读取请求、将其发送到服务器并比较收到的响应与所需相应响应的应用程序。

我们获得的另一个灵活的功能是:客户端和服务器端部分可以独立开发。应用程序架构师定义客户端-服务器应用程序 API,开发人员创建测试请求数据并测试 JSON 响应。使用这些测试数据,客户端开发人员可以在没有服务器端的情况下开发客户端部分,服务器端开发人员也可以在没有客户端的情况下开发服务器端部分。通过这种方式,您可以更快地完成应用程序。

© . All rights reserved.