RIA 框架

使用 ASP.NET MVC 和 Ext JS 构建以数据为中心的 Web 应用程序

Juan Carlos Carlos

下载代码示例

丰富 Internet 应用程序 (RIA) 将桌面应用程序的可用性与基于 Web 的部署和修订的灵活性结合到了一起。构建 RIA 有两种主要方法。第一种是使用承载执行环境的浏览器插件,如 Flash 插件、Java 插件和 Silverlight 插件。第二种是使用基于 JavaScript 的扩展库,如 Dojo、Ext JS、jQuery、MooTools、Prototype 和 YUI。这两种方法各有利弊。

构建 RIA 通常选择的是 JavaScript 库,因为所有主要浏览器都支持 JavaScript,而无需安装插件或运行时环境。我已经尝试过使用上面提到的另一种库 — Ext JS,而且我发现,对于实现 Web 应用程序,它是一种有趣的选择。它易于实现,文档完善,并且在测试方面与 Selenium 兼容。Ext JS 还提供预定义控件,以简化 Web 应用程序 UI 的创建过程。

遗憾的是,Ext JS 的大部分示例都需要通过服务器端的 PHP、Python 和 Ruby on Rails 代码才能说明。但这并不意味着使用 Microsoft 技术的开发人员不可以利用 Ext JS。虽然将 Ext JS 与 Web 窗体开发集成起来比较困难(这是由于抽象层导致的;为了提供有状态的基于控件的模型,抽象层将封装 Web 的请求-响应特性),但您可以使用 ASP.NET MVC 框架,该框架使您能够在同一应用程序中同时利用 Microsoft .NET Framework 和 Ext JS。

在本文中,我将提供之前没有找到的教程,逐步介绍如何使用可从后端数据库读取并可向后端数据库写入的 ASP.NET MVC 和 Ext JS 来开发实际的 Web 解决方案。

Ext JS 窗体基础知识

要使用 Ext JS,您首先需要从 sencha.com 下载它。(我使用的是 3.2.1 版本,但是您应该获取最新版本。)请注意,免费的开源版 Ext JS 可供开源项目、非营利组织和学习使用。对于其他用途,您需要购买许可证。请参阅 sencha.com/products/license.php 以了解详细信息。

将下载的文件解压缩到您的文件系统的一个目录中。它包含使用 Ext JS 开发 Web 解决方案时所需的所有内容,特别是主文件 ext-all.js。(还有一个调试版本可以帮助您更加轻松地找到错误。)依赖项、文档和示例代码都包含在下载的文件中。

\adapters 和 \resources 是项目必需的文件夹。adapters 文件夹允许将其他库与 Ext JS 一起使用。resources 文件夹包含依赖项,如 CSS 和图像。

要正确使用 Ext JS,您还需要在您的页面中添加三个关键文件引用:

ext-3.2.1/adapter/ext/ext-base.js
ext-3.2.1/ext-all.js
ext-3.2.1/resources/css/ext-all.css

ext-base.js 文件包含 Ext JS 的核心功能。 ext-all.js 包含小组件定义,ext-all.css 包含小组件的样式表。

让我们先通过在静态 HTML 页面中使用 Ext JS 来介绍一下基础知识。 页面的开始部分包含以下几行代码,这几行代码链接着成功开发 Ext JS 解决方案所需的文件(我还在 JavaScript 模块中包含了 Ext JS 下载文件中的一些示例小组件):

<link rel="stylesheet" type="text/css" 
  href="ext-3.2.1/resources/css/ext-all.css" />
<script type="text/javascript" language="javascript" 
  src="ext-3.2.1/adapter/ext/ext-base.js"></script>
<script type="text/javascript" language="javascript" 
  src="ext-3.2.1/ext-all.js"></script>
<script type="text/javascript" language="javascript" 
  src="extjs-example.js"></script>

我在文件正文中插入了一个 div 元素,以呈现主 Ext JS 窗体:

<div id="frame">
</div>

extjs-example.js 文件对 Ext JS 应用程序的构建方式进行了深入分析。 任何 Ext JS 应用程序的模板均使用 Ext. ns、Ext.BLANK_IMAGE_URL 和 Ext.onReady 语句:

Ext.
ns('formextjs.tutorial');
Ext.BLANK_IMAGE_URL = 'ext-3.2.1/resources/images/default/s.gif';
formextjs.tutorial.FormTutorial = {
  ...
}
Ext.onReady(formextjs.tutorial.FormTutorial.init, 
  formextjs.tutorial.FormTutorial);

Ext. ns 语句使您能够按照逻辑组织命名空间中的代码,以避免命名冲突和作用域问题。

Ext.BLANK_IMAGE_URL 语句对于呈现小组件很重要。 它称为空白区域图片(1x1 像素的透明图像),主要用于生成空白区域和放置图标及分隔符。

Ext.onReady 语句是使用 Ext JS 代码进行定义的第一个方法。 完全加载 DOM 后会自动调用此方法,以保证脚本运行时您可能引用的所有 HTML 元素都可用。 至于 extjs-example.js,下面就是该脚本:

formextjs.tutorial.FormTutorial = {
  init: function () {
    this.form = new Ext.FormPanel({
      title: 'Getting started form',
      renderTo: 'frame',
      width: 400,
      url: 'remoteurl',
      defaults: { xtype: 'textfield' },
      bodyStyle: 'padding: 10px',
      html: 'This form is empty!'
    });
  }
}

创建了 Ext.FormPanel 类的实例作为字段的容器。 renderTo 属性指向将在其上呈现窗体的 div 元素。 defaults 属性指定窗体上组件的默认类型。 url 属性指定用于发送窗体请求的 URI。 最后,html 属性指定作为默认输出的文本(带有任何 HTML 格式)。

要添加字段,您需要使用 items 属性替换 html 属性:

items: [ nameTextField, ageNumberField ]

首先要添加的两个项是文本字段和数值字段:

var nameTextField = new Ext.form.TextField({
  fieldLabel: 'Name',
  emptyText: 'Please, enter a name',
  name: 'name'
});
var ageNumberField = new Ext.form.NumberField({
  fieldLabel: 'Age',
  value: '25',
  name: 'age'
});

所需的属性是:fieldLabel 属性(用于设置窗体组件附带的说明性消息)和 name 属性(用于设置请求参数的名称)。 emptyText 属性定义当字段为空时字段将包含的水印文本。 value 属性是控件的默认值。

声明控件的另外一种方法是在进行中声明:

items: [
  { fieldLabel: 'Name', emptyText: 'Please, enter a name', name: 'name' },
  { xtype: 'numberfield', fieldLabel: 'Age', value: '25', name: 'age' }
]

如您所见,对于 Name 字段,您不必指定类型,因为它是从窗体的默认属性获得的。

我将在窗体中添加一些附加元素,结果如图 1 所示。

图 1 完成的窗体

到目前为止,您已经使用 Ext JS 创建了一个窗体来从用户那里获取数据。现在,我们来将这些数据发送到服务器。您需要添加一个按钮,以处理提交过程并向用户显示结果,如图 2 所示。

图 2 窗体按钮

buttons: [{
  text: 'Save', 
  handler: function () {
    form.getForm().submit({
      success: function (form, action) {
        Ext.Msg.alert('Success', 'ok');
      },
      failure: function (form, action) {
        Ext.Msg.alert('Failure', action.result.error);
      }
    });
  }
},
{
  text: 'Reset',
  handler: function () {
    form.getForm().reset();
  }
}]

buttons 属性使窗体能够管理所有可能执行的操作。 每个按钮都有 name 和 handler 属性。 handler 属性包含与对按钮执行的操作相关的逻辑。 在此例中,有两个按钮,分别名为 Save 和 Reset。 Save 按钮处理程序执行窗体上的提交操作,并显示一条指示提交成功或失败的消息。 Reset 按钮处理程序重置窗体上的字段值。

在窗体创建过程中,最后一个步骤,同时也是很重要的步骤是验证。 为了指定必填字段,我们需要将 allowBlank 属性设置为 false,将 blankText 属性设置为所需的验证失败时显示的错误消息。 例如,下面是窗体的 Name 字段:

{ fieldLabel: 'Name', emptyText: 'Please, enter a name', name: 'name', allowBlank: false }

如果未在 Name 和 Age 字段中输入任何数据,当您运行应用程序并单击 Save 按钮时,您会收到一条错误消息,并且必填字段下会出现红色下划线。

要自定义针对这些字段的错误消息,在 Ext.onReady 函数下添加下面这行代码:

Ext.QuickTips.init();

现在,当用户将鼠标指针移动到字段上方时,就会显示一个提示框,其中包含用于说明错误的消息。

我设置了几条字段验证规则,如指定允许的最小和最大长度,将字段验证延迟至提交窗体后进行,为 URL、电子邮件地址和其他类型的数据创建验证函数。 您可以在代码下载中查看此验证的详细信息。

构建 Web 应用程序

现在,让我们使用 Ext JS 和 ASP.NET MVC 开发一个 Web 解决方案。 我使用的是 ASP.NET MVC 2,但是此解决方案应该也适用于 ASP.NET MVC 3。 我接下来要解决的情况是在人力资源管理系统中添加一个员工。

Add Employee 用例说明如下:屏幕提示用户为新员工输入有效信息,如员工 ID、全名、地址、年龄、工资和部门。 department 字段是一个用于从中选择部门的部门列表。

如您刚才所见,主要实施策略是在客户端创建一个 Ext JS 窗体,然后使用 ASP.NET MVC 处理数据。 持久性层将使用 LINQ 来代表业务实体,并将数据永久保存到数据库系统中。 后端数据库是 Microsoft SQL Server 2008。

首先打开 Visual Studio 2010,使用 ASP.NET MVC 2 Web 应用程序模板创建一个新项目。

接下来创建数据库架构。 对于此示例,架构将包含两个实体:员工和部门。 图 3 显示了我是如何创建 Human Resources 数据库和基础表及约束的。

图 3 创建 Human Resources 数据库

create table department(
  deptno varchar(20) primary key,
  deptname varchar(50) not null,
  location varchar(50)
);

create unique index undx_department_deptname on department(deptname);

insert into department
  values('HQ-200','Headquarter-NY','New York');
insert into department
  values('HR-200','Human Resources-NY','New York');
insert into department
  values('OP-200','Operations-NY','New York');
insert into department
  values('SL-200','Sales-NY','New York');
insert into department
  values('HR-300','Human Resources-MD','Maryland');
insert into department
  values('OP-300','Operations-MD','Maryland');
insert into department
  values('SL-300','Sales-MD','Maryland');

create table employee(
  empno varchar(20) primary key,
  fullname varchar(50) not null,
  address varchar(120),
  age int,
  salary numeric(8,2) not null,
  deptno varchar(20) not null,
  constraint fk_employee_department_belong_rltn foreign key(deptno)
    references department(deptno)
);
create unique index undx_employee_fullname on employee(fullname);

现在,让我们使用 LINQ to SQL 定义实体的结构和持久性机制。 首先创建一个 EmployeeRepository 类来管理员工表的数据访问逻辑。 在本例中,您仅需要实现创建操作:

public class EmployeeRepository {
  private HumanResourcesDataContext _ctxHumanResources = 
    new HumanResourcesDataContext();

  public void Create(employee employee) {
    this._ctxHumanResources.employees.InsertOnSubmit(employee);
    this._ctxHumanResources.SubmitChanges();
  }
}

您还需要一个 DepartmentRepository 类来管理部门表的数据访问逻辑。 同样,在这个简单的示例中,您仅需要实现读取操作以查找部门列表:

public class DepartmentRepository {
  private HumanResourcesDataContext _ctxHumanResources = 
    new HumanResourcesDataContext();

  public IQueryable<department> FindAll() {
    return from dept in this._ctxHumanResources.departments
           orderby dept.deptname
           select dept;
  }
}

现在让我们定义体系结构的另一重要部分:控制器。 要定义控制器,请在“解决方案资源管理器”窗口中右键单击 Controllers 文件夹,然后选择“添加”|“控制器”。 我使用 HumanResourcesController 作为控制器名称。

Ext JS 表示层

现在让我们回到 Ext JS,使用框架构建应用程序的表示层。 对于此解决方案,您仅需要导入 ext-all.js 和 \adapter 及 \resources 文件夹。

转到 Site.Master 页面,在 head 元素中添加对 Ext JS 文件的引用,并添加 <asp:ContentPlaceHolder> 标记元素作为各个页面的自定义 JavaScript 和 CSS 代码的容器,如图 4 所示。

图 4 Site.Master

<head runat="server">
  <title><asp:ContentPlaceHolder ID="TitleContent" 
    runat="server" /></title>
  <link href="../../Content/Site.css" rel="stylesheet" 
    type="text/css" />

  <!-- Include the Ext JS framework -->
  <link href="<%= Url.Content("~/Scripts/ext-3.2.1/resources/css/ext-all.css") %>" 
    rel="stylesheet" type="text/css" />
  <script type="text/javascript" 
    src="<%= Url.Content("~/Scripts/ext-3.2.1/adapter/ext/ext-base.js") %>">
  </script>
  <script type="text/javascript" 
    src="<%= Url.Content("~/Scripts/ext-3.2.1/ext-all.js") %>">
  </script>    
  <!-- Placeholder for custom JS and CSS and JS files 
    for each page -->
  <asp:ContentPlaceHolder ID="Scripts" runat="server" />
</head>

现在让我们添加 MVC 体系结构的其他重要部分:视图。 视图将显示窗体,以获取与某位员工相关的数据。 转到 HumanResourcesController,右键单击 Index 操作方法,然后选择 Add View。 单击 Add View 对话框中的 Add 按钮。

要实现之前在本文中创建的 Ext JS 窗体,您需要在 Scripts 目录中添加一个 JavaScript 文件,并在视图中添加一个对此 JavaScript 文件的引用。 然后将该引用添加到 employee_form.js 文件中,并将 div 元素添加到 Index.aspx 视图中(请参见图 5)。

图 5 添加员工窗体

<%@ Page Title="" Language="C#" 
  MasterPageFile="~/Views/Shared/Site.Master" 
  Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" 
  runat="server">
Index
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" 
  runat="server">
  <h2>Add a New Employee</h2>
  <div id="employeeform"></div>
</asp:Content>

<asp:Content ID="Content3" ContentPlaceHolderID="Scripts" 
  runat="server">
  <script type="text/javascript" 
    src="<%= Url.Content("~/Scripts/employee_form.js") %>">
  </script>
</asp:Content>

转到 employee_form.js 文件,添加一些代码来配置 ExtJS 窗体及其基础小组件。 第一步是定义 Ext.data.JsonStore 类的一个实例,以获取部门列表:

var departmentStore = new Ext.data.JsonStore({
  url: 'humanresources/departments',
  root: 'departments',
  fields: ['deptno', 'deptname']
});

url 属性指向 HumanResourceController 控制器中的 departments 操作方法。 此方法通过 HTTP POST 动词进行访问。 root 属性是部门列表的根元素。 fields 属性指定数据字段。 现在定义窗体。 这些属性均为自描述性属性:

var form = new Ext.FormPanel({
  title: 'Add Employee Form',
  renderTo: 'employeeform',
  width: 400,
  url: 'humanresources/addemployee',
  defaults: { xtype: 'textfield' },
  bodyStyle: 'padding: 10px',

在本例中,url 属性指向 HumanResourceController 控制器中的 AddEmployee 操作方法。 此方法也可通过 HTTP POST 动词进行访问。

items 属性提供代表窗体字段的小组件列表(图 6)。 此处的默认小组件是文本字段(这在 defaults 属性中指定)。 第一个字段是员工编号,该字段为必填字段(由 allowBlank 属性指定)。 第二个字段是全名,它也是一个必填文本字段。 Address 字段是一个可选的文本区域。 Age 字段是一个可选的数值字段。 Salary 字段是一个必填的数值字段。 最后,部门编号字段是一个是从部门列表中选择的标识符字符串。

图 6 窗体字段小组件

items: [
  { fieldLabel: 'Employee ID', name: 'empno', allowBlank: false },
  { fieldLabel: 'Fullname', name: 'fullname', allowBlank: false },
  { xtype: 'textarea', fieldLabel: 'Address', name: 'address', 
    multiline: true },
  { xtype: 'numberfield', fieldLabel: 'Age', name: 'age' },
  { xtype: 'numberfield', fieldLabel: 'Salary', name: 'salary', 
    allowBlank: false },
  { xtype: 'combo', fieldLabel: 'Department', name: 'deptno', 
    store: departmentStore, hiddenName: 'deptno', 
    displayField: 'deptname', valueField: 'deptno', typeAhead: true,
    mode: 'remote', forceSelection: true, triggerAction: 'all', 
    emptyText: 'Please, select a department...', editable: false }
],

最后,将 buttons 属性定义为处理对窗体执行的操作。 这里的配置和图 2 中的配置相似,但文本属性的值为“Add”。

现在 employee_form.js 文件已完成。 (到目前为止,我已经介绍了此文件的大部分元素。 有关此文件的完整源代码列表,请参见代码下载。)

现在让我们转到 HumanResourceController,并执行相应的操作方法,如图 7 所示。

图 7 HumanResourceController

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using HumanResources_ExtJS_ASPNETMVC.Models;

namespace HumanResources_ExtJSASPNETMVC.Models.BusinessObjects {
  public class HumanResourcesController : Controller {
    DepartmentRepository _repoDepartment = new DepartmentRepository();
    EmployeeRepository _repoEmployee = new EmployeeRepository();

    // GET: /HumanResources/
    public ActionResult Index() {
      return View();
    }

    // POST: /HumanResource/Departments
    [HttpPost]
    public ActionResult Departments() {
      var arrDepartment = this._repoDepartment.FindAll();
      var results = (new {
        departments = arrDepartment
      });
      return Json(results);
    }

    // POST: /HumanResource/AddEmployee
    [HttpPost]
    public ActionResult AddEmployee(employee employee) {
      string strResponse = String.Empty;
      try {
        this._repoEmployee.Create(employee);
        strResponse = "{success: true}";
      }
      catch {
        strResponse = "{success: false, error: \"An error occurred\"}";
      }
      return Content(strResponse);
    }
  }
}

大功告成!

现在运行解决方案。您将看到如图 8 所示的 Web 页面。在窗体中输入一些数据,然后单击 Add。您会看到一个确认消息框。您还会看到在数据库的 dbo.employee 表中插入的行。

图 8 运行应用程序

创建简单的 RIA 的确就这么简单。根据您想利用的功能,可以使用任何其他常见的 JavaScript 框架并同时使用 ASP.NET MVC 来构建相似的应用程序。您可以轻松地将实体框架替换为数据层,并使用 Windows Azure 存储或 SQL Azure 作为后端数据存储。这些简单的构造块使构建以数据为中心的基本 RIA 变得快速而简单。

Juan Carlos Olamendy是一名高级架构师、开发人员和顾问。他多次被公认为 Microsoft 最有价值专家 (MVP) 和 Oracle ACE。他是 Microsoft 的 Windows Communication Foundation 认证技术专家。您可以通过 johnx_olam@fastmail 与 Olamendy 联系。

衷心感谢以下技术专家对本文的审阅:Scott HanselmanEilon Lipton