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

OSzone.net » Microsoft » Разработка приложений » .NET Framework » Введение в одностраничные приложения для .NET-разработчиков
Автор: Лонг Ле
Иcточник: MSDN Magazine
Опубликована: 19.09.2014

Большинство разработчиков, использующих 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-разработчикам, которые только знакомятся с миром 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.

Подготовка инфраструктуры 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 (и связанных файлов кода проекта). Итак, добавьте:

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

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

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

Примечание 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.

Теперь добавьте модули 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:

Поскольку вы используете инфраструктуру 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.

Когда 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.


Ссылка: http://www.oszone.net/24636/