Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
CheckBootSpeed - это диагностический пакет на основе скриптов PowerShell, создающий отчет о скорости загрузки Windows 7 ...
Вы когда-нибудь хотели создать установочный диск Windows, который бы автоматически установил систему, не задавая вопросо...
Если после установки Windows XP у вас перестала загружаться Windows Vista или Windows 7, вам необходимо восстановить заг...
Программа подготовки документов и ведения учетных и отчетных данных по командировкам. Используются формы, утвержденные п...
Red Button – это мощная утилита для оптимизации и очистки всех актуальных клиентских версий операционной системы Windows...
OSzone.net Microsoft Разработка приложений .NET Framework Введение в одностраничные приложения для .NET-разработчиков RSS

Введение в одностраничные приложения для .NET-разработчиков

Текущий рейтинг: 0 (проголосовало 0)
 Посетителей: 719 | Просмотров: 893 (сегодня 0)  Шрифт: - +

Большинство разработчиков, использующих Microsoft .NET Framework, большую часть своего времени занималось серверной стороной, создавая веб-приложения на C# или Visual Basic .NET. Конечно, JavaScript применяли для простых вещей вроде модальных окон, проверки, AJAX-вызовов и т. д. Однако JavaScript (в основном на клиентской стороне) использовался как вспомогательный язык, а приложения работали главным образом на серверной стороне.

Не так давно проявилась тенденция к миграции кода веб-приложений с серверной стороны на клиентскую (в браузеры), отвечающая ожиданиям в появлении гибких и отзывчивых пользовательских сред. А раз так, то множеству .NET-разработчиков (особенно корпоративных) пришлось резко озаботиться рекомендациями по программированию на JavaScript, архитектурой, модульным тестированием, поддержкой сопровождения и разбираться со всевозможными видами JavaScript-библиотек, которых становится все больше и больше. Отчасти тенденция с переходом на клиентскую сторону связана с растущим использованием одностраничных приложений (single-page applications, SPA). Сказать, что разработка SPA — это будущее, было бы сильным преуменьшением. Именно SPA являются примером некоторых из лучших веб-приложений, которые обеспечивают гибкие и отзывчивые пользовательские среды, в то же время сводя к минимуму объемы данных (трафик) и частоту полных циклов обмена ими между клиентом и сервером.

Сказать, что разработка SPA — это будущее, было бы сильным преуменьшением.

В этой статье я рассмотрю проблемы, которые могут возникнуть при переходе с серверной стороны в царство SPA. Лучший способ справиться с этими проблемами — принять язык JavaScript как полноценный и ничем не уступающий любым другим .NET-языкам, таким как C#, Visual Basic .NET, Python и др.

Ниже перечислены некоторые фундаментальные принципы .NET-разработки, которые иногда игнорируются или упускаются из виду при создании приложений на JavaScript:

  • ваша кодовая база является управляемой в .NET, поскольку вы четко определяете границы классов и то, где именно эти классы находятся в ваших проектах;
  • вы разделяете обязанности, поэтому у вас нет классов, отвечающих за сотни разных вещей с перекрытием обязанностей;
  • вы располагаете повторно используемыми репозитариями, очередями, сущностями (моделями) и источниками данных;
  • вы тщательно продумываете именование своих классов и файлов, чтобы их названия были более понятными;
  • вы практикуете использование проектировочных шаблонов, соглашений по кодированию и четкую организацию кода.

Поскольку эта статья адресована .NET-разработчикам, которые только знакомятся с миром SPA, я буду включать минимум инфраструктур — ровно столько, сколько необходимо для создания управляемого SPA с хорошей архитектурой.

Семь основных этапов создания SPA

Рассмотрим семь основных этапов преобразования нового ASP.NET Web Application, созданного с использованием готового шаблона ASP.NET MVC в Visual Studio 2013, в SPA (со ссылками на соответствующие файлы проекта, которые можно найти в сопутствующем этой статье пакете кода).

  1. Скачайте и установите NuGet-пакеты RequireJS, текстовый плагин RequireJS и Kendo UI Web.
  2. Добавьте модуль конфигурации (Northwind.Web/Scripts/app/main.js).
  3. Добавьте модуль приложения (Northwind.Web/Scripts/app/app.js).
  4. Добавьте модуль маршрутизатора (Northwind.Web/Scripts/app/router.js).
  5. Добавьте действие и представление (оба с именами Spa) (Northwind.Web/Controllers/HomeController.cs и Northwind.Web/Views/Home/Spa.cshtml).
  6. Модифицируйте файл _ViewStart.cshtml, чтобы MVC загружала представления без использования файла _Layout.cshtml по умолчанию (Northwind.Web/Views/_ViewStart.cshtml).
  7. Обновите навигационные ссылки (меню) в разметке, чтобы они соответствовали новым URL, дружественным к SPA (Northwind.Web/Views/Shared/_Layout.cshtml).
  8. После выполнения этих семи этапов структура вашего проекта веб-приложения должна выглядеть, как на Рис. 1.

*
Рис. 1. Структура проекта ASP.NET MVC

Я покажу, как создать впечатляющее SPA-приложение в ASP.NET MVC с использованием следующих JavaScript-библиотек, доступных через NuGet.

  • RequireJS (requirejs.org) Это JavaScript-файл и загрузчик модулей. RequireJS предоставляет #include/import/require API и возможность загрузки вложенных зависимостей через механизм встраивания зависимостей (dependency injection, DI). Подход к проектированию в RequireJS базируется на Asynchronous Module Definition (AMD) API для JavaScript-модулей, которые помогают инкапсулировать части кода в полезные блоки. Кроме того, он позволяет ссылаться на другие блоки кода (модули) интуитивно понятным способом. RequireJS-модули также следуют шаблону module (bit.ly/18byc2Q). Упрощенная реализация этого шаблона использует для инкапсуляции JavaScript-функции. Позже вы увидите этот шаблон в действии, когда все JavaScript-модули будут обернуты в функцию define или require. Те, кто знаком с концепциями DI и Inversion of Control (IoC), могут рассматривать эту реализацию как инфраструктуру DI на клиентской стороне. Если на данный момент вам ничего не понятно, не волнуйтесь: вскоре мы изучим некоторые примеры кода, где все это будет иметь смысл.
  • Текстовый плагин для RequireJS (bit.ly/1cd8lTZ) Будет использоваться для удаленной загрузки порций HTML (представлений) в SPA.
  • Entity Framework (bit.ly/1bKiZ9I) Здесь и так все понятно, а поскольку эта статья посвящена SPA, я не стану вдаваться в детали Entity Framework. Однако, если вы новичок в работе с этой инфраструктурой, в вашем распоряжении море документации, доступной в сети.
  • Kendo UI Web (bit.ly/t4VkVp) Это обширная инфраструктура JavaScript/HTML5, которая включает Web UI Widgets, DataSources, шаблоны, шаблон Model-View-ViewModel (MVVM), SPA, применение стилей и многое другое, что поможет вам в создании отзывчивого и адаптивного приложения с впечатляющим UI.

Подготовка инфраструктуры SPA

Чтобы показать, как подготовить инфраструктуру SPA, я сначала поясню, как создать модуль конфигурации RequireJS (Northwind.Web/Scripts/app/main.js). Этот модуль будет стартовой точкой входа в приложение. Если вы создали консольное приложение, можете рассматривать его как точку входа Main в Program.cs. В основном он содержит первый класс и метод, вызываемый при запуске SPA. Файл main.js главным образом служит манифестом SPA и является тем местом, где вы определяете все, что будет в SPA, и их зависимости, если таковые есть. Код для конфигурации RequireJS показан на Рис. 2.

Рис. 2. Конфигурация RequireJS

require.config({
  paths: {
    // Пакеты
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router'
  },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
});
require([
  'app'
], function (app) {
  app.initialize();
});

У вас есть два варианта представлений, которые будут загружаться в SPA: стандартные HTML-страницы (*.html) или страницы ASP.NET MVC Razor (*.cshtml).

На Рис. 2 свойство paths содержит список всех модулей с местами их расположения и именами. Shim — это имя ранее определенного модуля. Свойство shim включает любые зависимости, которые могут быть у модуля. В данном случае вы загружаете модуль kendo, и у него есть зависимость от модуля jquery, поэтому, если какой-то модуль требует модуля kendo, сначала загружайте jQuery.

Код «require([], function(){})» на Рис. 2 загрузит следующий модуль, каковым будет модуль с именем app. Заметьте, что я присваиваю модулям осмысленные имена.

Итак, каким образом ваше SPA узнает, какой модуль следует вызвать первым? Вы настраиваете это на первой посадочной странице (landing page) в SPA с помощью атрибута data-main в теге script со ссылкой на RequireJS. Я указал, что это должен быть модуль main (main.js). RequireJS возьмет на себя всю черновую работу, связанную с загрузкой этого модуля; вы просто сообщаете, какой модуль загружать первым.

У вас есть два варианта представлений, которые будут загружаться в SPA: стандартные HTML-страницы (*.html) или страницы ASP.NET MVC Razor (*.cshtml). Так как эта статья адресована .NET-разработчикам (и во многих организациях есть серверные библиотеки и инфраструктуры, которыми они хотели бы по-прежнему использовать в своих представлениях), я выбираю вариант с созданием Razor-представлений.

Начну с добавления представления и назову его Spa.cshtml, как уже упоминалось. Это представление будет в основном загружать оболочку или весь HTML для разметки SPA. Из этого представления я буду загружать другие представления (например, About.cshtml, Contact.cshtml, Index.cshtml и т. д.) с заменой по мере того, как пользователь будет переходить по SPA; при этом очередное представление заменяет весь HTML в теге div контента.

Создание посадочной страницы SPA (разметки) (Northwind.Web/Views/Spa.cshtml) Поскольку представление Spa.cshtml является посадочной страницей SPA, где вы загружаете все остальные представления, в нем не будет особой разметки, кроме ссылки на необходимые таблицы стилей и RequireJS. Обратите внимание на атрибут data-main в следующем коде, который сообщает RequireJS, какой модуль следует загрузить первым:

@{
  ViewBag.Title = "Spa";
  Layout = "~/Views/Shared/_Layout.cshtml";
}
<link href=
  "~/Content/kendo/2013.3.1119/kendo.common.min.css"
  rel="stylesheet" />
<link href=
  "~/Content/kendo/2013.3.1119/kendo.bootstrap.min.css"
  rel="stylesheet" />
<script src=
  "@Url.Content("~/scripts/require.js")"
  data-main="/scripts/app/main"></script>
<div id="app"></div>

Добавление действия для SPA Layout (Northwind.Web/Controllers/HomeController.cs) Чтобы создать и загрузить представление Spa.cshtml, добавьте действие (action) и представление:

public ActionResult Spa()
{
  return View();
}

Создание модуля приложения (Northwind.Web/Scripts/app/app.js) Вот модуль приложения, отвечающий за инициализацию и запуск Kendo UI Router:

define([
    'router'
  ], function (router) {
    var initialize = function() {
      router.start();
    };
    return {
      initialize: initialize
    };
  });

Создание модуля router (Northwind.Web/Scripts/app/router.js) Вызывается модулем app.js. Если вы уже знакомы с маршрутами в ASP.NET MVC, то ничего нового для вас здесь нет. Это маршруты SPA для ваших представлений. Я определю все маршруты для всех SPA-представлений, и, когда пользователь будет переходить по SPA, модуль router в Kendo UI будет знать, какие представления надо загружать в SPA (см. листинг 1 в пакете сопутствующего кода).

Класс Router в Kendo UI отвечает за отслеживание состояния приложения и навигацию между его состояниями. Этот модуль (router) интегрируется в журнал браузера, используя элемент фрагмента в URL (#page), что позволяет создавать закладки и ссылки на состояния приложения. При щелчке маршрутизируемого URL в дело вступает модуль router и сообщает приложению перевести себя обратно в то состояния, которое закодировано в маршруте. Определение маршрута — это строка, представляющая путь для идентификации состояния приложения, которое хочет увидеть пользователь. Когда определение маршрута совпадает с фрагментом хеша URL браузера, вызывается обработчик маршрута (табл. 1).

Табл. 1. Определения зарегистрированных маршрутов и соответствующие URL

Зарегистрированный маршрут (определение)Полный URL (который можно помещать в закладки)
/localhost:25061/home/spa/home/index
/home/indexlocalhost:25061/home/spa/#/home/index/home/about
/home/aboutlocalhost:25061/home/spa/#/home/about/home/contact
/home/contactlocalhost:25061/home/spa/#/home/contact/customer/index
/customer/indexlocalhost:25061/home/spa/#/customer/index

Что касается виджета разметки Kendo UI, то его название говорит само за себя. По-видимому, вы знакомы с ASP.NET Web Forms MasterPage или MVC-разметкой, включаемой при создании нового проекта ASP.NET MVC Web Application. В проекте SPA разметка помещается в Northwind.Web/Views/Shared/_Layout.cshtml. Разметка в Kendo UI и MVC отличается немного — за исключением того, что разметка Kendo UI выполняется на клиентской стороне. Как и разметка, работающая на серверной стороне, где исполняющая среда MVC заменяет содержимое разметки другими представлениями, разметка в Kendo UI действует точно так же, но на клиентской стороне. Вы выгружаете представление (контент) разметки Kendo UI, используя метод showIn. Содержимое представления (HTML) будет помещаться в div с идентификатором content, который передается в разметку Kendo UI при ее инициализации. После инициализации разметки выполняется ее рендеринг в div с идентификатором app, который является div в посадочной странице (Northwind.Web/Views/Home/Spa.cshtml). Я вкратце опишу это.

Класс Router в Kendo UI отвечает за отслеживание состояния приложения и навигацию между его состояниями.

Вспомогательный метод loadView принимает модель представления, представление и при необходимости обратный вызов, запускаемый, как только происходит связывание представления и модели представления. В методе loadView вы используете библиотеку Kendo UI FX, позволяющую улучшить эстетическое восприятие UI за счет добавления некоторых простых анимаций в процесс замены представлений. Это осуществляется скольжением текущего загруженного представления влево, удаленной загрузкой нового представления и его последующим скольжением обратно в центр. Очевидно, что эту анимацию можно легко заменять на любые другие, используя библиотеку Kendo UI FX. Одно из важнейших преимуществ применения разметки Kendo UI проявляется при вызове метода showIn для выгрузки представлений. Он гарантирует выгрузку представления, его корректное уничтожение и удаление из DOM браузера, что обеспечивает масштабируемость и производительность SPA.

Редактирование представления _ViewStart.cshtml (Northwind.Web/Views/_ViewStart.cshtml) Вот как настроить все представления, чтобы по умолчанию они не использовали разметку ASP.NET MVC:

@{
  Layout = null;
}

К этому моменту SPA должно работать. Щелкнув любую из навигационных ссылок в меню, вы увидите, что текущий контент выгружается через AJAX благодаря модулю router из Kendo UI и RequireJS.

Эти семь этапов, необходимых для преобразования нового ASP.NET Web Application в SPA, не так уж и страшны, не правда ли?

Теперь, когда SPA подготовлено и работает, я займусь тем, что делает большинство разработчиков с такими приложениями, а именно добавлю в него CRUD-функциональность.

Добавление CRUD-функциональности в SPA

Ниже перечислены основные этапы, необходимые для добавления представления сетки Customer в SPA (и связанных файлов кода проекта). Итак, добавьте:

  • MVC-контроллер CustomerController (Northwind.Web/Controllers/CustomerController.cs);
  • контроллер REST OData Customer Web API (Northwind.Web/Api/CustomerController.cs);
  • представление сетки Customer (Northwind.Web/Views/Customer/Index.cshtml);
  • модуль CustomerModel (Northwind.Web/Scripts/app/models/CustomerModel);
  • модуль customerDatasource для сетки Customer (Northwind.Web/Scripts/app/datasources/customerDatasource.js);
  • модуль indexViewModel для сетки Customer (Northwind.Web/Scripts/app/viewModels/indexViewModel.js).

Разметка в Kendo UI и MVC отличается немного — за исключением того, что разметка Kendo UI выполняется на клиентской стороне.

Подготовка структуры решения с помощью Entity Framework На рис. 3 показана структура решения, где выделены три проекта: Northwind.Data (1), Northwind.Entity (2) и Northwind.Web (3). Я вкратце рассмотрю каждый из них наряду с Entity Framework Power Tools.

*
Рис. 3. Рекомендованная структура решения

  • Northwind.Data Содержит все, что относится к Entity Framework Object-Relational Mapping (ORM), и обеспечивает хранение данных.
  • Northwind.Entity Включает сущности предметной области, состоящие из POCO-классов (Plain Old CLR Object). Здесь находятся все объекты предметной области, ничего не знающие о механизме сохранения.
  • Northwind.Web Содержит ASP.NET MVC 5 Web Application, презентационный уровень, где вы создаете SPA с помощью двух ранее упомянутых библиотек — Kendo UI и RequireJS, и остальной серверный стек: Entity Framework, Web API и OData.
  • Entity Framework Power Tools Чтобы создать все POCO-сущности и сопоставления (модель Database First), я использовал Entity Framework Power Tools от группы Entity Framework (bit.ly/1cdobhk). После генерации кода я лишь скопировал сущности в отдельный проект (Northwind.Entity), чтобы позаботиться о разделении обязанностей.

Примечание SQL-скрипт установки Northwind и резервная копия базы данных включены в пакет сопутствующего кода в папку Northwind.Web/App_Data (bit.ly/1cph5qc).

Теперь, когда решение настроено на доступ к базе данных, напишем MVC-класс CustomerController.cs, который будет обслуживать представления Index и Edit. Поскольку единственная обязанность контроллера — обслуживать HTML-представление для SPA, здесь код будет минимальным.

Создание MVC-контроллера Customer (Northwind.Web/Controllers/CustomerController.cs) Вот как создать контроллер Customer с действиями для представлений Index и Edit:

public class CustomerController : Controller
{
  public ActionResult Index()
  {
    return View();
  }
  public ActionResult Edit()
  {
    return View();
  }
}

Создание представления с сеткой Customers (Northwind.Web/Views/Customers/Index.cshtml) На рис. 4 показано, как создать представление с сеткой Customers.

Если разметка на Рис. 4 вам не знакома, не паникуйте: это разметка Kendo UI MVVM (HTML). Она просто конфигурирует HTML-элемент, в данном случае — div с идентификатором grid. Позднее, когда вы свяжете это представление с моделью представления, используя инфраструктуру Kendo UI MVVM, эта разметка будет преобразована в виджеты Kendo UI. Подробнее на эту тему см. по ссылке bit.ly/1d2Bgfj.

Рис. 4. Разметка представления сетки Customer с помощью виджета MVVM и привязки событий

<div class="demo-section">
  <div class="k-content" style="width: 100%">
    <div id="grid"
      data-role="grid"
      data-sortable="true"
      data-pageable="true"
      data-filterable="true"
      data-editable="inline"
      data-selectable="true"
      data-toolbar='[ { template: kendo.template($("#toolbar").html()) } ]'
      data-columns='[
        { field: "CustomerID", title: "ID", width: "75px" },
        { field: "CompanyName", title: "Company"},
        { field: "ContactName", title: "Contact" },
        { field: "ContactTitle", title: "Title" },
        { field: "Address" },
        { field: "City" },
        { field: "PostalCode" },
        { field: "Country" },
        { field: "Phone" },
        { field: "Fax" } ]'
      data-bind="source: dataSource, events:
        { change: onChange, dataBound: onDataBound }">
    </div>
    <style scoped>
    #grid .k-toolbar {
      padding: 15px;
    }
    .toolbar {
      float: right;
    }
    </style>
  </div>
</div>
<script type="text/x-kendo-template" id="toolbar">
  <div>
    <div class="toolbar">
      <span data-role="button" data-bind="click: edit">
        <span class="k-icon k-i-tick"></span>Edit</span>
      <span data-role="button" data-bind="click: destroy">
        <span class="k-icon k-i-tick"></span>Delete</span>
      <span data-role="button" data-bind="click: details">
        <span class="k-icon k-i-tick"></span>Edit Details</span>
    </div>
    <div class="toolbar" style="display:none">
      <span data-role="button" data-bind="click: save">
        <span class="k-icon k-i-tick"></span>Save</span>
      <span data-role="button" data-bind="click: cancel">
        <span class="k-icon k-i-tick"></span>Cancel</span>
    </div>
  </div>
</script>

Создание контроллера Customer MVC (OData) Web API (Northwind.Web/Api/CustomerController.cs) Теперь я покажу, как создать контроллер Customer. OData — это протокол доступа к данным для Web, который предоставляет единообразный способ запроса и манипулирования наборами данных через CRUD-операции. С помощью ASP.NET Web API создать конечную точку OData довольно легко. Вы можете контролировать то, какие OData-операции будут доступны. Вы можете разместить несколько конечных точек OData наряду с конечными точками, не имеющими отношения к OData. Вы получаете полный контроль над своей моделью данных, серверной бизнес-логикой и уровнем данных. Код для контроллера Customer Web API OData приведен на Рис. 5.

Рис. 5. Контроллер Customer Web API OData

public class CustomerController : EntitySetController<Customer, string>
{
  private readonly NorthwindContext _northwindContext;
  public CustomerController()
  {
    _northwindContext = new NorthwindContext();
  }
  public override IQueryable<Customer> Get()
  {
    return _northwindContext.Customers;
  }
  protected override Customer GetEntityByKey(string key)
  {
    return _northwindContext.Customers.Find(key);
  }
  protected override Customer UpdateEntity(string key, Customer update)
  {
    _northwindContext.Customers.AddOrUpdate(update);
    _northwindContext.SaveChanges();
    return update;
  }
  public override void Delete(string key)
  {
    var customer = _northwindContext.Customers.Find(key);
    _northwindContext.Customers.Remove(customer);
    _northwindContext.SaveChanges();
  }
}

Код на Рис. 5 просто создает контроллер OData Web API для предоставления данных Customer из базы данных Northwind. После его создания вы можете запустить проект, а с помощью таких средств, как Fiddler (бесплатный веб-отладчик на fiddler2.com) или LINQPad, можно запрашивать сами данные по клиенту (customer).

Настройка и предоставление OData из таблицы Customer для сетки (Northwind.Web/App_Start/WebApiConfig.cs) На рис. 6 настраивается и предоставляется OData из таблицы Customer для сетки.

Рис. 6. Настройка маршрутов ASP.NET MVC Web API для OData

public static void Register(HttpConfiguration config)
{
  // Конфигурация и сервисы Web API
  ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
  var customerEntitySetConfiguration =
    modelBuilder.EntitySet<Customer>("Customer");
  customerEntitySetConfiguration.EntityType.Ignore(t => t.Orders);
  customerEntitySetConfiguration.EntityType.Ignore(t =>
     t.CustomerDemographics);
  var model = modelBuilder.GetEdmModel();
  config.Routes.MapODataRoute("ODataRoute", "odata", model);
  config.EnableQuerySupport();
  // Маршруты Web API
  config.MapHttpAttributeRoutes();
  config.Routes.MapHttpRoute(
    "DefaultApi", "api/{controller}/{id}",
    new {id = RouteParameter.Optional});
}

Запросы OData Web API с помощью LINQPad Если вы еще не пользовались LINQPad (linqpad.net), добавьте это средство в свой инструментарий разработки; это необходимый инструмент, доступный в бесплатной версии. На рис. 7 показан LINQPad с подключением к Web API OData (localhost:2501/odata), отображающий результаты LINQ-запроса «Customer.Take (100)».

*
Рис. 7. Запрос контроллера Customer Web API OData через LINQPad

Создание (наблюдаемой) модели Customer (Northwind.Web/Scripts/app/models/customerModel.js) Далее мы создаем модель Customer (Kendo UI Observable). Ее можно считать моделью сущности Customer на клиентской стороне. Я создал такую модель, поэтому ее можно будет повторно использовать в представлениях сетки и редактирования Customer. Код приведен на Рис. 8.

Рис. 8. Создание модели Customer (Kendo UI Observable)

define(['kendo'],
  function (kendo) {
    var customerModel = kendo.data.Model.define({
      id: "CustomerID",
      fields: {
        CustomerID: { type: "string", editable: false, nullable: false },
        CompanyName: { title: "Company", type: "string" },
        ContactName: { title: "Contact", type: "string" },
        ContactTitle: { title: "Title", type: "string" },
        Address: { type: "string" },
        City: { type: "string" },
        PostalCode: { type: "string" },
        Country: { type: "string" },
        Phone: { type: "string" },
        Fax: { type: "string" },
        State: { type: "string" }
      }
    });
    return customerModel;
  });

OData — это протокол доступа к данным для Web, который предоставляет единообразный способ запроса и манипулирования наборами данных через CRUD-операции.

Создание DataSource для сетки Customers (Northwind.Web/Scripts/app/datasources/customersDatasource.js) Если вы знакомы с источниками данных из ASP.NET Web Forms, то знайте, что здесь используется та же концепция, где вы создаете источник данных для сетки Customers (Northwind.Web/Scripts/app/datasources/customersDatasource.js). Компонент Kendo UI DataSource (bit.ly/1d0Ycvd) — это абстракция для использования локальных данных (массивов JavaScript-объектов) или удаленных (XML, JSON или JSONP). Он полностью поддерживает CRUD-операции над данными и предоставляет как локальную, так и серверную поддержку сортировки, разбиения на страницы, фильтрации, группирования и агрегации.

Создание ViewModel для представления сетки Customers Это та же концепция, что и MVVM в Windows Presentation Foundation (WPF) или Silverlight, но на клиентской стороне (находится в этом проекте в Northwind.Web/Scripts/ViewModels/Customer/indexViewModel.cs). MVVM — архитектурный шаблон, отделяющий представление от его данных и бизнес-логики. Вскоре вы увидите, что все данные, бизнес-логика и прочее содержатся в модели представления и что представление является чистым HTML (презентационным уровнем). Код для представления сетки Customer показан на Рис. 9.

Рис. 9. Модель представления сетки Customer

define(['kendo', 'customerDatasource'],
  function (kendo, customerDatasource) {
    var lastSelectedDataItem = null;
    var onClick = function (event, delegate) {
      event.preventDefault();
      var grid = $("#grid").data("kendoGrid");
      var selectedRow = grid.select();
      var dataItem = grid.dataItem(selectedRow);
      if (selectedRow.length > 0)
        delegate(grid, selectedRow, dataItem);
      else
        alert("Please select a row.");
      };
      var indexViewModel = new kendo.data.ObservableObject({
        save: function (event) {
          onClick(event, function (grid) {
            grid.saveRow();
            $(".toolbar").toggle();
          });
        },
        cancel: function (event) {
          onClick(event, function (grid) {
            grid.cancelRow();
            $(".toolbar").toggle();
          });
        },
        details: function (event) {
          onClick(event, function (grid, row, dataItem) {
            router.navigate('/customer/edit/' + dataItem.CustomerID);
          });
        },
        edit: function (event) {
          onClick(event, function (grid, row) {
            grid.editRow(row);
            $(".toolbar").toggle();
          });
        },
        destroy: function (event) {
          onClick(event, function (grid, row, dataItem) {
            grid.dataSource.remove(dataItem);
            grid.dataSource.sync();
          });
        },
        onChange: function (arg) {
          var grid = arg.sender;
          lastSelectedDataItem = grid.dataItem(grid.select());
        },
        dataSource: customerDatasource,
        onDataBound: function (arg) {
          // Проверяем, выбрана ли строка
          if (lastSelectedDataItem == null) return;
          // Получаем все строки
          var view = this.dataSource.view();
          // Перебираем строки в цикле
          for (var i = 0; i < view.length; i++) {
          // Находим строку с lastSelectedProduct
            if (view[i].CustomerID == lastSelectedDataItem.CustomerID) {
              // Получаем сетку
              var grid = arg.sender;
              // Задаем выбранную строку
              grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']"));
              break;
            }
          }
        },
      });
      return indexViewModel;
  });

Я кратко опишу различные компоненты кода на рис. 9.

  • onClick (вспомогательный метод) Этот метод является вспомогательной функцией, которая получает экземпляр сетки Customer, текущую выбранную строку и JSON-модель представления Customer для выбранной строки.
  • save Сохраняет изменения при построчном редактировании Customer.
  • cancel Отменяет режим построчного редактирования.
  • details Переключает SPA на представление редактирования Customer, дописывая идентификатор Customer к URL.
  • edit Активирует построчное редактирование для текущего выбранного Customer.
  • destroy Удаляет текущий выбранный Customer.
  • onChange (событие) Срабатывает при каждом выборе Customer. Вы сохраняете последний выбранный Customer, чтобы можно было поддерживать состояние. После выполнения любых обновлений или ухода из сетки Customer вы вновь выделяете последний выбранный Customer, если возвращаетесь в сетку.

Теперь добавьте модули customerModel, indexViewModel и customersDatasource в свою конфигурацию RequireJS (Northwind.Web/Scripts/app/main.js). Код показан на рис. 10.

Рис. 10. Добавления в конфигурацию RequireJS

paths: {
  // Пакеты
  'jquery': '/scripts/jquery-2.0.3.min',
  'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
  'text': '/scripts/text',
  'router': '/scripts/app/router',
  // Модели
  'customerModel': '/scripts/app/models/customerModel',
  // Модели представления
  'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
  'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
  // Источники данных
  'customerDatasource': '/scripts/app/datasources/customerDatasource',
  // Вспомогательные методы
  'util': '/scripts/util'
}

Добавление маршрута для нового представления сетки Customers Заметьте, что в обратном вызове loadView (в Northwind.Web/Scripts/app/router.js) вы связываете панель инструментов сетки после ее инициализации и выполнения MVVM-привязки. Дело в том, что при первом связывании сетки панель инструментов не инициализируется, поскольку она уже существует в сетке. При первой инициализации сетки через MVVM она будет загружать панель инструментов из шаблона Kendo UI. После ее загрузки в сетку вы связываете панель инструментов только со своей моделью представления, чтобы кнопки на панели инструментов были связаны с методами save и cancel в вашей модели представления. Вот как выглядит код, регистрирующий определение маршрута для представления редактирования Customer:

router.route("/customer/index", function () {
  require(['customer-indexViewModel', 'text!/customer/index'],
    function (viewModel, view) {
      loadView(viewModel, view, function () {
        kendo.bind($("#grid").find(".k-grid-toolbar"), viewModel);
      });
    });
});

Теперь у вас есть полнофункциональное представление сетки Customers. Загрузите localhost:25061/Home/Spa#/customer/index (номер порта на вашем компьютере скорее всего будет другим) в браузер и вы увидите то, что показано на рис. 11.

*
Рис. 11. Представление сетки Customers, где MVVM использует ViewModel с именем index

Подключение представления редактирования Customer Основные этапы добавления представления редактирования Customer в SPA:

  • создайте представление редактирования клиента, связанное с вашей моделью Customer через MVVM (Northwind.Web/Views/Customer/Edit.cshtml);
  • добавьте модуль модели представления редактирования для этого представления Customer (Northwind.Web/Scripts/app/viewModels/editViewModel.js);
  • добавьте вспомогательный модуль, чтобы получать идентификаторы из URL (Northwind.Web/Scripts/app/util.js).

Поскольку вы используете инфраструктуру Kendo UI, примените стили Kendo UI к своему представлению редактирования. Узнать больше на эту тему можно по ссылке bit.ly/1f3zWuC. На Рис. 12 показана разметка представления редактирования с помощью виджета MVVM и связывания с событиями.

Рис. 12. Разметка представления редактирования с помощью виджета MVVM Widget и связывания с событиями

<div class="demo-section">
  <div class="k-block" style="padding: 20px">
    <div class="k-block k-info-colored">
      <strong>Note: </strong>Please fill out all of the fields in this form.
    </div>
    <div>
      <dl>
        <dt>
          <label for="companyName">Company Name:</label>
        </dt>
        <dd>
          <input id="companyName" type="text"
            data-bind="value: Customer.CompanyName" class="k-textbox" />
        </dd>
        <dt>
          <label for="contactName">Contact:</label>
        </dt>
        <dd>
          <input id="contactName" type="text"
            data-bind="value: Customer.ContactName" class="k-textbox" />
        </dd>
        <dt>
          <label for="title">Title:</label>
        </dt>
        <dd>
          <input id="title" type="text"
            data-bind="value: Customer.ContactTitle" class="k-textbox" />
        </dd>
        <dt>
          <label for="address">Address:</label>
        </dt>
        <dd>
          <input id="address" type="text"
            data-bind="value: Customer.Address" class="k-textbox" />
        </dd>
        <dt>
          <label for="city">City:</label>
        </dt>
        <dd>
          <input id="city" type="text"
            data-bind="value: Customer.City" class="k-textbox" />
        </dd>
        <dt>
          <label for="zip">Zip:</label>
        </dt>
        <dd>
          <input id="zip" type="text"
            data-bind="value: Customer.PostalCode" class="k-textbox" />
        </dd>
        <dt>
          <label for="country">Country:</label>
        </dt>
        <dd>
          <input id="country" type="text"
          data-bind="value: Customer.Country" class="k-textbox" />
        </dd>
        <dt>
          <label for="phone">Phone:</label>
        </dt>
        <dd>
          <input id="phone" type="text"
            data-bind="value: Customer.Phone" class="k-textbox" />
        </dd>
        <dt>
          <label for="fax">Fax:</label>
        </dt>
        <dd>
          <input id="fax" type="text"
            data-bind="value: Customer.Fax" class="k-textbox" />
        </dd>
      </dl>
      <button data-role="button"
        data-bind="click: saveCustomer"
        data-sprite-css-class="k-icon k-i-tick">Save</button>
      <button data-role="button" data-bind="click: cancel">Cancel</button>
      <style scoped>
        dd
        {
          margin: 0px 0px 20px 0px;
          width: 100%;
        }
        label
        {
          font-size: small;
          font-weight: normal;
        }
        .k-textbox
        {
          width: 100%;
        }
        .k-info-colored
        {
          padding: 10px;
          margin: 10px;
        }
      </style>
    </div>
  </div>
</div>

Создание модуля util для получения идентификатора Customer из URL  Поскольку вы создаете модули с четкими границами, чтобы добиться хорошего разделения обязанностей, я продемонстрирую, как создать модуль util, где будут находиться все ваши вспомогательные функции (методы). Начну с метода, который может извлекать идентификатор клиента из URL для Customer DataSource (Northwind.Web/Scripts/app/datasources/customerDatasource.js), как показано на рис. 13.

Рис. 13. Модуль util

define([],
  function () {
    var util;
    util = {
      getId:
      function () {
        var array = window.location.href.split('/');
        var id = array[array.length - 1];
        return id;
      }
    };
    return util;
  });

Добавление модели представления edit и модулей util в конфигурацию RequireJS (Northwind.Web/Scripts/app/main.js) Код на рис. 14 показывает добавления в конфигурацию RequireJS для модулей редактирования Customer.

Рис. 14. Добавления в конфигурацию RequireJS для модулей редактирования Customer

require.config({
  paths: {
    // Пакеты
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router',
    // Модели
    'customerModel': '/scripts/app/models/customerModel',
    // Модели представлений
    'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
    'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
    // Источники данных
    'customerDatasource': '/scripts/app/datasources/customerDatasource',
    // Вспомогательные методы
    'util': '/scripts/util'
    },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
  });
require([
  'app'
], function (app) {
  app.initialize();
});

Добавление модели представления для редактирования Customer (Northwind.Web/Scripts/app/viewModels/editViewModel.js) Код на рис. 15 демонстрирует, как добавить модель представления для редактирования Customer.

Рис. 15. Модуль модели представления для редактирования Customer

define(['customerDatasource', 'customerModel', 'util'],
  function (customerDatasource, customerModel, util) {
    var editViewModel = new kendo.data.ObservableObject({
      loadData: function () {
        var viewModel = new kendo.data.ObservableObject({
          saveCustomer: function (s) {
            customerDatasource.sync();
            customerDatasource.filter({});
            router.navigate('/customer/index');
          },
          cancel: function (s) {
            customerDatasource.filter({});
            router.navigate('/customer/index');
          }
        });
        customerDatasource.filter({
          field: "CustomerID",
          operator: "equals",
          value: util.getId()
        });
        customerDatasource.fetch(function () {
          console.log('editViewModel fetching');
          if (customerDatasource.view().length > 0) {
            viewModel.set("Customer", customerDatasource.at(0));
          } else
            viewModel.set("Customer", new customerModel());
        });
        return viewModel;
      },
    });
    return editViewModel;
  });

Я кратко опишу различные компоненты кода на рис. 15.

  • saveCustomer Этот метод отвечает за сохранение любых изменений в Customer. Кроме того, он сбрасывает фильтр DataSource, чтобы сетка снова заполнялась всеми Customer.
  • cancel Этот метод выполняет переход в SPA обратно в представление сетки Customers. Кроме того, он сбрасывает фильтр DataSource, чтобы сетка снова заполнялась всеми Customer.
  • filter Вызывает метод фильтра DataSource и запрашивает конкретный Customer по идентификатору в URL.
  • fetch Вызывает метод fetch объекта DataSource после настройки фильтра. В обратном вызове fetch вы записываете в свойство Customer своей модели представления тот Customer, который был возвращен методом fetch объекта DataSource и который будет использоваться для связывания с вашим представлением редактирования Customer.

Когда RequireJS загружает какой-то модуль, код в теле метода define будет вызван только раз, поэтому вы предоставляете некий метод (loadData) в своей модели представления редактирования, чтобы иметь механизм загрузки данных после того, как модуль модели представления редактирования уже загружен (это можно увидеть в Northwind.Web/Scripts/app/router.js).

Добавление маршрута для нового представления редактирования Customer (Northwind.Web/Scripts/app/router.js) Вот как добавляется маршрут:

router.route("/customer/edit/:id",
        function () {
    require(['customer-editViewModel',
          'text!/customer/edit'],
      function (viewModel, view) {
      loadView(viewModel.loadData(), view);
    });
  });

Заметьте: когда модель представления редактирования Customer запрашивается от RequireJS, вы можете извлечь Customer, вызвав метод loadData из этой модели представления. Это позволяет загружать корректные данные Customer по идентификатору в URL всякий раз, когда загружается представление редактирования Customer. Маршрут не обязательно «зашивать» в код как строку. Кроме того, он может содержать параметры, такие как серверный маршрутизатор (Ruby on Rails, ASP.NET MVC, Django и др.). Для этого укажите в сегменте маршрута двоеточие перед именем нужной переменной.

Теперь вы можете загрузить представление редактирования Customer в браузер (localhost:25061/Home/Spa#/customer/edit/ANATR) и увидеть экран, как на рис. 16.

*
Рис. 16. Представление редактирования Customer

Примечание Хотя функциональность delete в представлении сетки Customer была подключена, при щелчке кнопки Delete на панели инструментов (рис. 17) появится исключение, показанное на Рис. 18.

*
Рис. 17. Представление сетки Customer

*
Рис. 18. Ожидаемое исключение при удалении Customer из-за ссылочной целостности внешнего ключа CustomerID

Это исключение так и задумано, поскольку большинство идентификаторов Customer являются внешними ключами в других таблицах, например Orders, Invoices и т. д. Вы должны были бы подключить каскадное удаление, которое удаляло бы все записи изо всех таблиц, где данный Customer ID является внешним ключом. И хотя вы не в состоянии удалить что-либо, я все же хотел показать этапы создания функциональности удаления и ее код.

Ну, вот и все. Я продемонстрировал, как легко и быстро преобразовывать готовое ASP.NET Web Application в SPA с помощью RequireJS и Kendo UI. Затем я показал, насколько легко добавить CRUD-подобную функциональность в SPA.

Автор: Лонг Ле  •  Иcточник: MSDN Magazine  •  Опубликована: 19.09.2014
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:   .Net.


Оценить статью:
Вверх
Комментарии посетителей
Комментарии отключены. С вопросами по статьям обращайтесь в форум.