Инфраструктуры для RIA

Создание ориентированных на данные веб-приложений с применением ASP.NET MVC и Ext JS

Хуан Карлос Оламенди

Загрузка примера кода

Полнофункциональное веб-приложение (rich Internet application, RIA) сочетает в себе удобство использования настольного приложения с гибкостью развертывания и обновления через Интернет. Существует два ключевых подхода к созданию RIA-приложений. Первый: плагины браузера, которые служат хостом для таких исполняющих сред, как Flash, Java и Silverlight. А второй заключается в использовании библиотек расширения на основе JavaScript, например Dojo, Ext JS, jQuery, MooTools, Prototype и YUI. У каждого подхода свои плюсы и минусы.

Библиотеки JavaScript — популярный выбор при создании RIA-приложений, так как JavaScript поддерживается всеми основными браузерами и нет нужды устанавливать какой-то плагин или исполняющую среду. Я экспериментировал с Ext JS и считаю, что это интересный выбор для реализации веб-приложений. Это расширение легко реализуется, хорошо документировано и совместимо с Selenium для тестирования. Ext JS также предоставляет заранее определенные элементы управления, упрощающие создание UI веб-приложения.

Увы, в большинстве примеров, демонстрирующих Ext JS, используется код на PHP, Python и Ruby on Rails, выполняемый на серверной стороне. Но это не значит, что разработчики, использующие технологии Microsoft, не могут задействовать преимущества Ext JS. Хотя Ext JS трудно интегрировать в разработки на основе Web Forms (из-за уровня абстракции, который скрывает в себе сущность Web, построенную на запросах и ответах, и который предоставляет модель на основе элементов управления с поддержкой состояний), вы могли бы применить инфраструктуру ASP.NET MVC, позволяющую использовать в одном приложении как Microsoft .NET Framework, так и Ext JS.

Считайте эту статью учебным материалом, который лично мне не удалось найти в готовом виде. Мы пошагово рассмотрим разработку реального веб-решения с применением ASP.NET MVC и Ext JS, позволяющего читать и писать информацию в серверную базу данных.

Основы форм Ext JS

Чтобы использовать Ext JS, сначала нужно скачать ее с сайта sencha.com. (Я работал с версией 3.2.1, но вы должны взять самую свежую на данный момент версию.) Заметьте, что это бесплатная версия Ext JS с открытым исходным кодом, доступная для проектов с открытым исходным кодом, некоммерческих организаций и в образовательных целях. Для других применений нужно покупать лицензию. Подробности см. по ссылке sencha.com/products/license.php.

Разархивируйте скачанный файл в какой-либо каталог файловой системы. В архиве содержится все, что нужно для разработки веб-решения на основе Ext JS, в частности основной файл 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 включены таблицы стилей для этих виджетов.

Начну с использования Ext JS в статической HTML-страницу, чтобы проиллюстрировать основы. Следующие строки содержатся в разделе head страницы и связывают файлы, необходимые для разработки решения с применением 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 важно для рендеринга виджетов. Оно называется изображением-распоркой (spacer image) (это прозрачное изображение размером 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-форматированием), который выводится по умолчанию.

Чтобы добавить поля (fields), нужно заменить свойство html свойством items:

items: [ nameTextField, ageNumberField ]

Первые два элемента (items), которые мы добавим, — текстовое и числовое поля:

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 определяет текст водяного знака (текст подсказки) (watermark text), который будет содержаться в поле, когда оно пустое. Свойство value — это значение по умолчанию для элемента управления.

Элементы управления также можно объявлять «на лету»:

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

Как видите, для поля name вам не нужно указывать тип, потому что он берется из свойств формы по умолчанию.

Я добавлю несколько дополнительных элементов на форму, и в конечном счете она будет выглядеть, как показано на рис. 1.

image: The Completed Form

Рис. 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 выполняет действие submit (передача на сервер) и сообщает об успехе или неудаче. Обработчик кнопки Reset сбрасывает значения полей на форме.

Последний (но важный!) этап при создании формы — проверка. Чтобы указать обязательные (для заполнения) поля, нам нужно установить свойство allowBlank в false, а в свойство blankText записать сообщение об ошибке, которое будет показываться при неудачном завершении проверки. Возьмем, к примеру, поле name на форме:

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

Когда вы запускаете приложение и щелкаете кнопку Save, не вводя никаких данных в поля Name и Age, вы получаете сообщение об ошибке и обязательные поля подчеркиваются красным.

Чтобы изменить сообщения об ошибках, выводимые для полей, добавьте сразу под функцией Ext.onReady следующую строку кода:

Ext.QuickTips.init();

Теперь, когда пользователь задержит курсор мыши над полем, появится всплывающее сообщение об ошибке.

Я задал несколько правил проверки для полей, в частности определил минимальную и максимальную допустимую длину, указал проверять поля перед самой отправкой формы на сервер и создал функции проверки для URL, адресов электронной почты и других типов данных. Детали см. в исходном коде, который можно скачать для этой статьи.

Создание веб-приложения

Теперь создадим веб-решение, используя Ext JS и ASP.NET MVC. Я воспользовался ASP.NET MVC 2, но это решение должно быть применимо и к ASP.NET MVC 3. Задача, которую я буду решать, — добавление сотрудника в системе управления кадрами.

Опишу сценарий применения Add Employee: на экране предлагается ввести допустимую информацию по новому сотруднику, например код сотрудника, полное имя, адрес, возраст, заработная плата и отдел. Последнее поле представляет собой список отделов в данной организации, из которого выбирается нужное.

Основная стратегия реализации — создание формы Ext JS на клиентской стороне (этот процесс вы уже видели) и обработка данных с помощью ASP.NET MVC. На уровне хранения будет использоваться LINQ для представления бизнес-сущностей и сохранения данных в СУБД. В качестве внутренней (серверной) базы данных применяется Microsoft SQL Server 2008.

Начнем с открытия Visual Studio 2010 и создания нового проекта по шаблону ASP.NET MVC 2 Web Application.

Далее создаем схему базы данных. В этом примере схема содержит две сущности: сотрудника (employee) и отдел (department). На рис. 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, который управляет логикой доступа к данным в таблице employee. В нашем случае достаточно реализовать операцию Create:

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

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

Нам также понадобится класс DepartmentRepository для управления логикой доступа к данным в таблице department. И вновь в этом простом случае достаточно реализовать операцию чтения для поиска списка отделов:

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 в окне Solution Explorer и выберите Add | Controller. Я присвоил контроллеру имя HumanResourcesController.

Презентационный уровень Ext JS

Теперь вернемся к Ext JS и используем эту инфраструктуру для создания презентационного уровня приложения. В данном решении нужно лишь импортировать ext-all.js и папки \adapter и \resources.

Перейдите на страницу Site.Master и добавьте ссылки на файлы Ext JS в элемент head, а также тег <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: представление (view). В представлении будет выводиться форма, позволяющая получить данные, которые относятся к одному сотруднику. Перейдите в HumanResourcesController, щелкните правой кнопкой мыши метод действия Index и выберите Add View. В диалоговом окне Add View щелкните кнопку Add.

Чтобы реализовать форму Ext JS, созданную ранее в этой статье, нужно добавить JavaScript-файл в каталог Scripts и ссылку на этот JavaScript-файл в представление. Затем включите ссылку на файл employee_form.js и добавьте элемент div в представление Index.aspx (рис. 5).

Рис. 5. Добавление формы Employee

<%@ 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 и добавьте код для конфигурирования формы Ext JS и ее нижележащих виджетов. Первый шаг — определение экземпляра класса Ext.data.JsonStore, который получает список отделов:

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

Свойство url указывает на метод действия departments в контроллере HumanResourceController. Этот метод доступен через 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 указывает на метод действия AddEmployee в контроллере HumanResourceController. Этот метод также доступен через HTTP POST.

Свойство items предоставляет список виджетов, представляющих поля формы (рис. 6). В данном случае виджетом по умолчанию является текстовое поле (это указывается в свойстве defaults). В первое поле вводится код сотрудника, который обязателен (задается свойством allowBlank). Второе поле — полное имя, и оно тоже является обязательным. Заполнять поля адреса и возраста не обязательно. Поле заработной платы является обязательным числовым полем. Наконец, поле кода отдела — это строка-идентификатор, которая выбирается из списка отделов.

Рис. 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, но свойству text присваивается значение «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. Введите на форме какие-нибудь данные и щелкните кнопку Add.Появится окно подтверждения. Кроме того, вы обнаружите, что в таблицу dbo.employee базы данных вставлена новая запись.

image: Running the Application

Рис. 8. Приложение в работе

Вот, собственно, все, что требуется для создания простого RIA-приложения. В зависимости от нужной вам функциональности аналогичное приложение можно было бы создать с помощью любой другой популярной инфраструктуры JavaScript в сочетании с ASP.NET MVC. Вы могли бы легко заменить на уровне данных Entity Framework и вместо этой инфраструктуры использовать хранилище Windows Azure или SQL Azure. Эти простые строительные блоки ускоряют и упрощают создание базового RIA-приложения, ориентированного на работу с данными.

Хуан Карлос Оламенди (Juan Carlos Olamendy) — старший архитектор, разработчик и консультант. Неоднократно получал награды Microsoft MVP и Oracle ACE. Является сертифицированным специалистом по технологиям Microsoft в области Windows Communication Foundation. С ним можно связаться по адресу johnx_olam@fastmail.

Выражаю благодарность за рецензирование статьи экспертам Скотту Ханселману (Scott Hanselman) и Эйлону Липтону (Eilon Lipton)