ASP.NET MVC 5

Introducción para desarrolladores .NET a las aplicaciones de una sola página

Long Le

Descargar el código de ejemplo

La mayoría de los desarrolladores Microsoft .NET Framework pasó gran parte de su vida profesional en el lado servidor, desarrollando aplicaciones web en C# o Visual Basic .NET. Por supuesto que han usado JavaScript para cosas sencillas como ventanas modales, validación, llamadas AJAX, y otras por el estilo. Pero han empleado JavaScript (principalmente código del lado cliente) como lenguaje utilitario solamente y las aplicaciones han operado en gran medida en el lado servidor.

Recientemente ha surgido una tendencia enorme de migración de código de aplicaciones web desde el lado servidor al lado cliente (explorador) para satisfacer las expectativas de los usuarios con respecto a una experiencia de usuario fluida y que responda bien. En estas circunstancias, muchos desarrolladores .NET (especialmente en el sector empresarial) sienten una ansiedad extrema por los procedimientos recomendados de JavaScript, la arquitectura, las pruebas unitarias, la facilidad de mantenimiento y la explosión reciente de diferentes tipos de bibliotecas para JavaScript. Una parte de esta tendencia de pasar hacia el lado cliente es el uso creciente de aplicaciones de una sola página (SPA). Decir que el desarrollo de SPA es el futuro es poco. SPA es la forma que emplean algunas de las mejores aplicaciones de Internet para ofrecer una capacidad de respuesta y experiencia de usuario fluida y para minimizar al mismo tiempo las cargas (tráfico) y los recorridos de ida y vuelta al servidor.

En este artículo abordaré las preocupaciones que usted podría experimentar al realizar la transición desde el lado servidor al dominio de las SPA. La mejor forma de lidiar con estas preocupaciones es adoptar JavaScript como un lenguaje de primera clase, igual que los demás lenguajes de .NET como C#, Visual Basic .NET, Python, etcétera.

A continuación entregaré algunos principios fundamentales de desarrollo para .NET que a veces se obvian u olvidan en el desarrollo de aplicaciones en JavaScript:

  • En .NET, la base de código se puede administrar, ya que reflexionamos y tomamos decisiones acerca de los límites de las clases y dónde residen realmente las clases dentro de los proyectos.
  • Separamos los conceptos para evitar las clases responsables de cientos de cosas diferentes y con responsabilidades superpuestas.
  • Empleamos repositorios, consultas, entidades (modelos) y orígenes de datos reutilizables.
  • Hacemos un gran esfuerzo por encontrar nombres significativos para nuestras clases y archivos.
  • Aplicamos patrones de diseño, convenciones de codificación y formas de organización razonables.

Como este artículo va dirigido a los desarrolladores .NET que están dando sus primeros pasos en el mundo de las SPA, incorporaré la menor cantidad de marcos posible para crear una SPA administrable con una arquitectura sólida.

Creación de una SPA en siete pasos claves

A continuación se muestran siete pasos claves para convertir una aplicación web ASP.NET nueva tal como la crea la plantilla de Visual Studio 2013 ASP.NET MVC en una SPA (con referencias a los archivos de proyecto pertinentes que se encuentran en el código descargable complementario).

  1. Descargue e instale los paquetes NuGet de RequireJS, del complemento de texto de RequireJS y de Kendo UI Web.
  2. Agregue un módulo de configuración (Northwind.Web/Scripts/app/main.js).
  3. Agregue un módulo para la aplicación (Northwind.Web/Scripts/app/app.js).
  4. Agregue un módulo para el enrutador (Northwind.Web/Scripts/app/router.js).
  5. Agregue una acción y vista, ambas llamadas Spa (Northwind.Web/Controllers/HomeController.cs y Northwind.Web/Views/Home/Spa.cshtml).
  6. Modifique el archivo _ViewStart.cshtml para que MVC cargue las vistas sin usar el archivo _Layout.cshtml de manera predeterminada (Northwind.Web/Views/_ViewStart.cshtml).
  7. Actualice los vínculos de navegación del diseño (menú) para que coincidan con las URL aptas para SPA (Northwind.Web/Views/Shared/_Layout.cshtml).

Después de realizar estos siete pasos, la estructura del proyecto de la aplicación web debería parecerse a lo que vemos en la figura 1.

ASP.NET MVC Project Structure
Figura 1 Estructura del proyecto ASP.NET MVC

Mostraré cómo crear una SPA increíble en ASP.NET MVC con las siguientes bibliotecas JavaScript que están disponibles por medio de NuGet:

  • RequireJS (requirejs.org): este es un cargador de archivos y módulos para Java­Script. RequireJS proporcionará las API #include/import/require y la capacidad de cargar dependencias anidadas con inserción de dependencias (DI). El modelo de diseño de RequireJS emplea la API de definición asincrónica de módulos (Asynchronous Module Definition, AMD) para crear módulos de JavaScript, lo que permite encapsular trozos de código en unidades útiles. También brinda una manera intuitiva para referirse a otras unidades de código (módulos). Los módulos de RequireJS además se orientan en el patrón de módulo (bit.ly/18byc2Q). Una implementación simplificada de este patrón emplea funciones de JavaScript para la encapsulación. Veremos este patrón en la práctica más adelante cuando todos los módulos de JavaScript se encapsularán dentro de una función “define” o “require”.
  • Los lectores familiarizados con los conceptos de la inserción de dependencias y la inversión del control (IoC) pueden imaginar esto como un marco de DI del lado cliente. Si por el momento esto le parece chino, no se preocupe: pronto veremos algunos ejemplos de código donde todo cobrará sentido.
  • Complemento de texto para RequireJS (bit.ly/1cd8lTZ): se empleará para cargar trozos de HTML (vistas) de manera remota en la SPA.
  • Entity Framework (bit.ly/1bKiZ9I): esto no requiere de mayor explicación y, como este artículo está enfocado en las SPA, no me detendré mayormente en Entity Framework. Sin embargo, si cuenta con relativamente poca experiencia, hay documentación abundante disponible.
  • Kendo UI Web (bit.ly/t4VkVp): este es un marco JavaScript/­HTML5 completo que comprende widgets para la interfaz web de usuario, DataSources, plantillas, el patrón Model-View-ViewModel (MVVM), las SPA, estilos, etcétera; esto permite entregar aplicaciones enormemente atractivas, flexibles y que responden bien.

Configuración de la infraestructura de la SPA

Para mostrar cómo configurar la infraestructura de la SPA, primero explicaré cómo crear el módulo (de configuración) RequireJS (Northwind.Web/Scripts/app/main.js). Este módulo será el punto de entrada de la aplicación al iniciar. Si creó una aplicación para la consola, puede imaginarlo como el punto de entrada Main en Program.cs. Contiene básicamente la primera clase y el método que se llama cuando se inicia la SPA. El archivo main.js sirve básicamente como el manifiesto de la SPA y es donde definimos en qué lugar se encuentran todas las cosas en la SPA, junto con las dependencias, cuando las hay. El código de configuración de RequireJS se muestra en la figura 2.

Figura 2 Configuración de RequireJS

require.config({
  paths: {
    // Packages
    '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();
});

En la figura 2, la propiedad paths contiene un listado que indica dónde se ubican todos los módulos, junto con sus nombres. Shim es el nombre de un módulo que se definió previamente. La propiedad shim incluye todas las dependencias que podría tener el módulo. En este caso, cargamos un módulo llamado kendo que a su vez tiene una dependencia hacia el módulo llamado jquery, así que si algún módulo requiere el módulo kendo, cargue jQuery primero, ya que jQuery se definió como dependencia del módulo kendo.

En la figura 2, el código “require([], function(){})” se cargará en el siguiente módulo, que es el módulo que llamé app. Observe que deliberadamente le puse nombres significativos a los módulos.

Por lo tanto, ¿cómo sabe la SPA que debe invocar primero este módulo? Esto se configura en la primera página de aterrizaje de la SPA mediante el atributo data-main en la etiqueta de referencia script de RequireJS. En este caso especifiqué que se ejecute el módulo main (main.js). RequireJS se hará cargo de todo el trabajo pesado de cargar este módulo; nosotros solo tenemos que indicarle el módulo que hay que cargar primero.

Tenemos dos opciones para las vistas de las SPA que se cargarán en la SPA: páginas HTML estándar (*.html) o ASP.NET MVC Razor (*.cshtml). Como este artículo está pensado para desarrolladores .NET, y muchas empresas tienen bibliotecas y marcos del lado servidor que desean seguir usando para sus vistas, elegiré la segunda opción y crearé las vistas con Razor.

Comenzaré por agregar una vista y llamarla Spa.cshtml, tal como mencioné previamente. Esta vista cargará esencialmente el shell o todo el HTML para el diseño de la SPA. Desde esta vista, cargaré las otras vistas (por ejemplo About.cshtml, Contact.cshtml, Index.cshtml, etc.) a medida que el usuario se desplaza por la SPA, para lo cual intercambio las vistas que reemplazan todo el contenido HTML del div llamado “content”.

Creación de la página de aterrizaje de la SPA (diseño) (Northwind.Web/Views/Spa.cshtml) Como la vista Spa.cshtml es la página de aterrizaje donde vamos a cargar todas las vistas, no tendrá mucho marcado, aparte de las referencias a las hojas de estilos requeridas y RequireJS. Preste atención al atributo data-main en el siguiente código, que le indica a RequireJS qué módulo hay que cargar primero:

    @{
      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>

Adición de una acción al diseño de la SPA (Northwind.Web/­Controllers/HomeController.cs) Para crear y cargar la vista Spa.cshtml, agregue una acción y vista:

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

Creación del módulo Application (Northwind.Web/Scripts/app/app.js) Este es el módulo Application, responsable de inicializar e iniciar Kendo UI Router:

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

Creación del módulo Router (Northwind.Web/Scripts/app/router.js) Este se llama desde app.js. Si ya está familiarizado con las rutas de ASP.NET MVC, aquí la idea es la misma. Estas son las rutas de la SPA para las vistas. Definiré todas las rutas para todas las vistas de la SPA para que cuando el usuario se desplace por la SPA, el enrutador de Kendo UI sepa qué vistas debe cargar en la SPA. Consulte el listado 1 en la descarga complementaria.

La clase Router de Kendo UI es la responsable de registrar el estado de la aplicación y de navegar entre los estados de la aplicación. El enrutador se integra en el historial del explorador mediante la parte del fragmento de la URL (#página), de modo que la aplicación es compatible con los marcadores y vínculos. Cuando hacemos clic en una URL enrutable, el enrutador entra en acción y le indica a la aplicación que vuelva al estado que se codificó en la ruta. La definición de la ruta es una cadena que representa una ruta de acceso que se emplea para identificar el estado de la aplicación que el usuario desea ver. Cuando la definición de una ruta coincide con el fragmento de hash de la URL del explorador, se llama el controlador de rutas (ver figura 3).

Figura 3 Definiciones de rutas registradas y las URL correspondientes

Ruta registrada (definición) URL completa propiamente tal (apta para marcadores)
/ localhost:25061/home/spa/home/index
/home/index localhost:25061/home/spa/#/home/index/home/about
/home/about localhost:25061/home/spa/#/home/about/home/contact
/home/contact localhost:25061/home/spa/#/home/contact/customer/index
/customer/index localhost:25061/home/spa/#/customer/index

En cuanto al widget layout de Kendo UI, el nombre lo dice todo. Probablemente ya está familiarizado con MasterPage de ASP.NET Web Forms o el diseño de MVC que se incluye en el proyecto cuando creamos una aplicación web ASP.NET MVC nueva. En este proyecto de SPA, se ubica en la ruta de acceso Northwind.Web/Views/Shared/_Layout.cshtml. Prácticamente no hay diferencias entre el diseño de Kendo UI y el diseño de MVC, excepto que el diseño de Kendo UI se ejecuta en el lado cliente. Así como funcionaba el diseño del lado servidor, donde el tiempo de ejecución de MVC intercambiaba el contenido del diseño con otras vistas, el diseño de Kendo UI funciona exactamente del mismo modo. Cambiamos la vista (contenido) del diseño de Kendo UI mediante el método showIn. El contenido de la vista (HTML) se ubicará en el div con el identificador “content” que se pasó al diseño de Kendo UI durante la inicialización. Después de inicializar el diseño, lo representamos dentro del div identificado como “app”, que es un elemento div en la página de aterrizaje (Northwind.Web/Views/Home/Spa.cshtml). Revisaremos esto en breve.

El método auxiliar loadView recibe un modelo de vista, una vista y (si hiciera falta) una devolución de llamada para invocar una vez la vista, y luego se produce el enlace con el modelo de vista. Dentro del método loadView empleamos la biblioteca Kendo UI FX para mejorar estéticamente la experiencia de usuario al agregar algunas animaciones sencillas a la vista para la visualización del proceso de intercambio. De este modo, la vista que está cargada actualmente se desplaza hacia la izquierda, la vista nueva se carga en forma remota y luego la vista cargada se desplaza nuevamente de vuelta al centro. Por supuesto, la biblioteca Kendo UI FX nos permite cambiar esto fácilmente con animaciones diferentes. Una de las principales ventajas de usar el diseño de Kendo UI se aprecia cuando invocamos el método showIn para intercambiar las vistas. Nos garantiza que la vista se descargará, destruirá y eliminará del DOM del explorador, de modo que la SPA podrá escalar y tendrá un rendimiento satisfactorio.

Edición de la vista _ViewStart.cshtml (Northwind.Web/Views/­_ViewStart.cshtml) Todas las vistas que no emplearán el diseño de ASP.NET MVC de manera predeterminada se configuran del siguiente modo:

    @{
      Layout = null;
    }

Llegados a este punto, la SPA debería funcionar. Al hacer clic en cualquiera de los vínculos de navegación del menú, deberíamos ver cómo el contenido actual se reemplaza por medio de AJAX, gracias al enrutador de Kendo UI y RequireJS.

¿Verdad que estos siete pasos necesarios para convertir una aplicación web ASP.NET nueva en una SPA no son tan terribles?

Ahora que la SPA funciona, seguiré adelante y haré lo que terminará haciendo la mayoría de los desarrolladores con la SPA, que es agregar funcionalidad de creación, lectura, actualización y eliminación (CRUD).

Adición de la funcionalidad de CRUD a la SPA

Estos son los pasos claves necesarios para agregar una vista de cuadrícula Customer a la SPA (y los archivos de código de proyecto relacionados):

  • Agregar un controlador MVC CustomerController (Northwind.Web/Controllers/CustomerController.cs).
  • Agregar un controlador REST de OData Web API para Customer (Northwind.Web/Api/CustomerController.cs).
  • Agregar una vista de cuadrícula Customer (Northwind.Web/Views/­Customer/Index.cshtml).
  • Agregar un módulo CustomerModel (Northwind.Web/Scripts/app/models/CustomerModel).
  • Agregar un módulo customerDatasource para la cuadrícula Customer (Northwind.Web/Scripts/app/datasources/customer­Datasource.js).
  • Agregar un módulo indexViewModel para la vista de cuadrícula Customer (Northwind.Web/Scripts/app/viewModels/­indexViewModel.js).

Configuración de la estructura de la solución con Entity Framework En la figura 4 vemos la estructura de la solución, donde se destacan tres proyectos: Northwind.Data (1), Northwind.Entity (2) y Northwind.Web (3). Me referiré brevemente a cada uno, además de Entity Framework Power Tools.

  • Northwind.Data: esto incluye todo lo relacionado con la herramienta de asignación relacional de objetos (ORM) de Entity Framework, para la persistencia.
  • Northwind.Entity: esto incluye las entidades de dominio, compuestas de clases de objeto CLR estándar (POCO). Estos son todos los objetos de dominio que desconocen la persistencia.
  • Northwind.Web: esto incluye la aplicación web ASP.NET MVC 5, la capa de presentación, donde completamos la SPA con dos bibliotecas
  • que mencionamos previamente, Kendo UI y RequireJS, y el resto de la pila del lado servidor: Entity Framework, Web API y OData.
  • Entity Framework Power Tools: para crear todas las entidades y asignaciones POCO (con prioridad en la base de datos), usé Entity Framework Power Tools del equipo de Entity Framework (bit.ly/1cdobhk). Después de la generación del código, todo lo que hice fue copiar simplemente las entidades a un proyecto independiente (Northwind.Entity) para abordar la separación de conceptos.

A Best-Practice Solution Structure
Figura 4 Estructura de una solución basada en los procedimientos recomendados

Nota: el script de instalación de Northwind SQL y la copia de seguridad de la base de datos se incluyen en el código fuente descargable en la carpeta Northwind.Web/App_Data (bit.ly/1cph5qc).

Ahora que la solución está configurada para acceder a la base de datos, escribiré la clase MVC CustomerController.cs para servir las vistas del índice y de edición. Como la única responsabilidad del controlador es servir una vista HTML para la SPA, el código es mínimo.

Creación del controlador Customer de MVC (Northwind.Web/­Controllers/CustomerController.cs) Así es como creamos el controlador Customer con las acciones necesarias para las vistas del índice y de edición:

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

Creación de la vista con la cuadrícula Customers (Northwind.Web/­Views/Customers/Index.cshtml) En la figura 5 vemos cómo se crea la vista con la cuadrícula Customers.

Si el marcado de la figura 5 no le resulta familiar, no se preocupe, es simplemente el marcado de Kendo UI MVVM (HTML). Configura simplemente un elemento HTML, en este caso el elemento div con el identificador “grid”. Más adelante, cuando enlacemos esta vista con un modelo de vista mediante el marco Kendo UI MVVM, este marcado se convertirá en los widgets de Kendo UI. Puede obtener más información en bit.ly/1d2Bgfj.

Figura 5 Marcado de la vista de cuadrícula Customer con un widget MVVM y enlaces de evento

    <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>

Creación del controlador de MVC (OData) Web API para Customer (Northwind.Web/Api/CustomerController.cs) Ahora mostraré cómo crear el controlador MVC (OData) Web API para Customer. OData es un protocolo de acceso a datos para la Web que ofrece una manera uniforme de consultar y manipular conjuntos de datos mediante operaciones CRUD. Al usar ASP.NET Web API, resulta fácil crear un extremo compatible con OData. Podemos controlar qué operaciones de OData se exponen. Podemos hospedar diferentes extremos OData junto con extremos incompatibles con OData. Tenemos un control total sobre el modelo de datos, la lógica de negocio del back-end y la capa de datos. En la figura 6 vemos el código del controlador Customer de Web API OData.

El código de la figura 6 solamente crea un controlador Web API OData para exponer los datos de Customer de la base de datos Northwind. Una vez que se creó, podemos ejecutar el proyecto y, con una herramienta como Fiddler (un depurador gratuito para la Web en fiddler2.com) o LINQPad, podemos consultar los datos de los clientes.

Figura 6 Controlador Customer de 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();
  }
}

Configuración y exposición de OData desde la tabla Customer para la cuadrícula (Northwind.Web/App_Start/WebApiConfig.cs) La figura 7 configura y expone OData desde la tabla Customer para la cuadrícula.

Consulta de OData Web API con LINQPad si nunca usó LINQPad (linqpad.net), agregue esta herramienta a su caja de herramientas: es absolutamente imprescindible y está disponible en una versión gratuita. En la figura 8 vemos LINQPad con una conexión a Web API OData (localhost:2501/odata), junto con los resultados de la consulta LINQ “Customer.Take (100)”.

Figura 7 Configuración de las rutas de ASP.NET MVC Web API para OData

public static void Register(HttpConfiguration config)
{
  // Web API configuration and services
  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 routes
  config.MapHttpAttributeRoutes();
  config.Routes.MapHttpRoute(
    "DefaultApi", "api/{controller}/{id}",
    new {id = RouteParameter.Optional});
}

Querying the Customer Controller Web API OData Via a LINQPad Query
Figura 8 Consulta del controlador de OData Web API para Customer por medio de una consulta de LINQPad

Creación del modelo (observable) Customer (Northwind.Web/­Scripts/app/models/customerModel.js) A continuación, hay que crear el modelo Customer (observable de Kendo UI). Podemos imaginarlo como un modelo de dominio de la entidad Customer del lado cliente. Creé el modelo de Customer para reutilizarlo fácilmente en la vista de cuadrícula y la vista de edición de Customer. El código se muestra en la figura 9.

Figura 9 Creación del modelo de Customer (observable de Kendo UI)

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

Creación de un DataSource para la cuadrícula Customers (Northwind.Web/Scripts/app/datasources/customersDatasource.js) Si está familiarizado con los orígenes de datos de ASP.NET Web Forms, el concepto es el mismo aquí, donde creamos un origen de datos para la cuadrícula Customers (Northwind.Web/Scripts/app/datasources/customersDatasource.js). El componente DataSource de Kendo UI (bit.ly/1d0Ycvd) es una abstracción para usar datos locales (matrices de objetos en JavaScript) o remotos (XML, JSON o JSONP). Es plenamente compatible con las operaciones de datos CRUD y entrega funciones locales y del lado servidor para ordenar, separar en páginas, filtrar, agrupar y agregar.

Creación del modelo de vista para la vista de cuadrícula Customers Si está familiarizado con MVVM de Windows Presentation Foundation (WPF) o Silverlight, este es exactamente el mismo concepto, simplemente en el lado cliente (en este proyecto se encuentra en Northwind.Web/Scripts/ViewModels/­Customer/indexViewModel.cs). MVVM es un patrón de separación arquitectónico que se emplea para separar la vista y los datos de la lógica de negocio. Verá dentro de poco que todos los datos, la lógica de negocio, etcétera, se encuentra en el modelo de vista y que la vista está escrita solamente en HTML (presentación). En la figura 10 vemos el código de la vista de cuadrícula Customer.

Figura 10 el modelo de la vista de cuadrícula 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) {
          // Check if a row was selected
          if (lastSelectedDataItem == null) return;
          // Get all the rows     
          var view = this.dataSource.view();
          // Iterate through rows
          for (var i = 0; i < view.length; i++) {
          // Find row with the lastSelectedProduct
            if (view[i].CustomerID == lastSelectedDataItem.CustomerID) {
              // Get the grid
              var grid = arg.sender;
              // Set the selected row
              grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']"));
              break;
            }
          }
        },
      });
      return indexViewModel;
  });

Describiré brevemente diferentes componentes del código que aparece en la figura 10:

  • onClick (auxiliar): este método es una función auxiliar que recibe una instancia de la cuadrícula Customer, la fila seleccionada actualmente y un modelo JSON con la representación de Customer de la fila seleccionada.
  • save: esto guarda los cambios al realizar una edición en línea de un Customer.
  • cancel: esto cancela el modo de edición en línea.
  • details: esto navega la SPA a la vista de edición de Customer, al anexar el identificador de Customer a la URL.
  • edit: esto activa la edición en línea para el Customer seleccionado actualmente.
  • destroy: esto elimina al Customer seleccionado actualmente.
  • onChange (evento): esto se activa cada vez que se selecciona un Customer. Almacenamos el último Customer que se seleccionó, para poder conservar el estado. Después de realizar cualquier actualización o de navegar para salir de la cuadrícula de Customer, al volver a la cuadrícula seleccionamos nuevamente el último Customer seleccionado.

Ahora agregue los módulos customerModel, indexViewModel y customersDatasource en la configuración de RequireJS (Northwind.Web/Scripts/app/main.js). El código se muestra en la figura 11.

Figura 11 Adiciones en la configuración de RequireJS

paths: {
  // Packages
  'jquery': '/scripts/jquery-2.0.3.min',
  'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
  'text': '/scripts/text',
  'router': '/scripts/app/router',
  // Models
  'customerModel': '/scripts/app/models/customerModel',
  // View models
  'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
  'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
  // Data sources
  'customerDatasource': '/scripts/app/datasources/customerDatasource',
  // Utils
  'util': '/scripts/util'
}

Adición de una ruta para la nueva vista de cuadrícula de Customers Observe que en la devolución de llamada loadView (en Northwind.Web/Scripts/app/router.js) enlazamos la barra de herramientas de la cuadrícula después de haberla inicializado y una vez que ocurrió el enlace de MVVM. Esto se debe a que la primera vez que enlazamos la cuadrícula, la barra de herramientas no está inicializada, ya que existe en la cuadrícula. Cuando la cuadrícula se inicializa por primera vez mediante MVVM, se cargará en la barra de herramientas desde la plantilla de Kendo UI. Al cargarla en la cuadrícula, luego solo podemos enlazar la barra de herramientas al modelo de vista, de modo que los botones de la barra de herramientas están enlazados a los métodos save y cancel del modelo de vista. Este es el código pertinente para registrar la definición de la ruta para la vista de edición de 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);
      });
    });
});

Ahora tenemos una vista de cuadrícula completamente funcional para Customers. Cargue localhost:25061/Home/Spa#/customer/index (el número del puerto probablemente será diferente en su equipo) en un explorador y podrá ver la figura 12.

The Customer Grid View with MVVM Using the Index View Model
Figura 12 Vista de cuadrícula de Customer con MVVM mediante el modelo de vista Index

Conexión de la vista de edición de Customers Estos son los pasos claves para agregar una vista de edición para Customer a la SPA:

  • Cree una vista de edición customer enlazada al modelo Customer mediante MVVM (Northwind.Web/Views/Customer/Edit.cshtml).
  • Agregue un módulo con el modelo de la vista de edición para la vista de edición de Customer (Northwind.Web/Scripts/app/viewModels/­editViewModel.js).
  • Agregue un módulo auxiliar utility para obtener los identificadores desde la URL (Northwind.Web/Scripts/app/util.js).

Como usamos el marco Kendo UI, puede usar los estilos de Kendo UI para el diseño de la vista de edición. Puede obtener más información sobre esto en bit.ly/1f3zWuC. En la figura 13 vemos el marcado de la vista de edición con un widget MVVM y los enlaces de evento.

Figura 13 Marcado de la vista de edición con un widget de MVVM y los enlaces de evento

    <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>

Creación de una utilidad para obtener el identificador de Customer desde la URL Como creamos módulos concisos con límites nítidos para establecer una separación de conceptos clara, demostraré cómo crear un módulo llamado Util donde residirán todos los auxiliares utilitarios. Comenzaré con un método utilitario que puede recuperar el identificador del cliente en la URL para el DataSource Customer (Northwind.Web/Scripts/app/datasources/customerDatasource.js), tal como se aprecia en la figura 14.

Figura 14 Módulo Utility

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

Adición de los módulos del modelo de la vista de edición y de utilidades a la configuración de RequireJS (Northwind.Web/Scripts/app/main.js) El código de la figura 15 muestra las adiciones en la configuración de RequireJS para los módulos de edición de Customer.

Figura 15 Adiciones en la configuración de RequireJS para los módulos de edición de Customer

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router',
    // Models
    'customerModel': '/scripts/app/models/customerModel',
    // View models
    'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
    'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
    // Data sources
    'customerDatasource': '/scripts/app/datasources/customerDatasource',
    // Utils
    'util': '/scripts/util'
    },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
  });
require([
  'app'
], function (app) {
  app.initialize();
});

Adición del modelo de la vista de edición para Customer (Northwind.Web/Scripts/app/viewModels/editViewModel.js) En el código de la figura 16 se ilustra cómo podemos agregar un modelo de la vista de edición para Customer.

Figura 16 Módulo del modelo de la vista de edición para la vista 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;
  });

Describiré brevemente diferentes componentes del código que aparece en la figura 16:

  • saveCustomer: este método es responsable de guardar todos los cambios en Customer. También restablece el filtro de DataSource para hidratar (rellenar) la cuadrícula con todos los Customers.
  • cancel: este método navegará la SPA nuevamente a la vista de cuadrícula de Customer. También restablece el filtro de DataSource para hidratar la cuadrícula con todos los Customers.
  • filter: esto invoca el método filter de DataSource y consulta un Customer específico mediante el identificador que se encuentra en la URL.
  • fetch: esto invoca el método fetch de DataSource después de configurar el filtro. En la devolución de llamada de fetch, establecemos la propiedad Customer del modelo de vista con el Customer que devolvió el método fetch de DataSource, el cual se empleará para enlazar con la vista de edición de Customer.

Cuando RequireJS carga un módulo, el código dentro del cuerpo del método “define” se invoca una sola vez (cuando RequireJS carga el módulo), de modo que exponemos un método (loadData) en el modelo de la vista de edición para cargar datos una vez que el módulo del modelo de la vista de edición ya se cargó (verlo en Northwind.Web/­Scripts/app/router.js).

Adición de una ruta para la nueva vista de edición de Customer (Northwind.Web/Scripts/app/router.js) Este es el código pertinente para agregar el enrutador:

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

Observe que cuando el modelo de la vista de edición de Customer se solicita desde RequireJS, podemos recuperar a Customer al invocar el método loadData desde el modelo de vista. De este modo, podemos cargar los datos correctos de Customer en función del identificador que se encuentra en la URL cada vez que se carga la vista de edición de Customer. No es necesario que la ruta sea una cadena estática. También puede contener parámetros, por ejemplo un enrutador de servidor back-end (Ruby on Rails, ASP.NET MVC, Django, etc.). Para esto, señalamos un segmento de ruta con dos puntos antes del nombre de la variable que queremos.

Así podemos cargar la vista de edición de Customer en el explorador (localhost:25061/Home/Spa#/customer/edit/ANATR) y ver la pantalla que aparece en la figura 17.

The Customer Edit View
Figura 17 Vista de edición de Customer

Nota: aunque la funcionalidad de eliminación (destrucción) se conectó en la vista de cuadrícula de Customer, al hacer clic en el botón “Delete” de la barra de herramientas (ver figura 18), aparecerá una excepción, tal como se aprecia en la figura 19.

The Customer Grid View
Figura 18 Vista de cuadrícula de Customer

Expected Exception When Deleting a Customer Due to CustomerID Foreign Key Referential Integrity
Figura 19 Excepción esperada al eliminar un Customer debido a la integridad referencial de la clave externa CustomerID

Esta excepción se genera por diseño, ya que la mayoría de los identificadores de Customer son claves externas de otras tablas, por ejemplo Orders, Invoices etcétera. Tendríamos que configurar una eliminación en cascada para eliminar todos los registros de todas las tablas donde el identificador de Customer es una clave externa. Aunque no podamos eliminar nada, quería mostrar los pasos y el código necesario para la función de eliminación.

Ahí está. Demostré lo fácil y rápido que es convertir una aplicación web tal como la entrega ASP.NET en una SPA mediante RequireJS y Kendo UI. Luego ilustré lo fácil que es agregar funcionalidades similares a CRUD a la SPA.

Puede apreciar una demostración en vivo del proyecto en bit.ly/1bkMAlK y puede ver el sitio del proyecto en CodePlex (junto con el código descargable) en easyspa.codeplex.com.

¡Que disfrute programando!

Long Le es el arquitecto de aplicaciones y desarrollo .NET principal en CBRE Inc. y MVP de Telerik y Kendo UI. Dedica la mayor parte de su tiempo a desarrollar marcos y bloques de aplicación, a entregar orientación para los procedimientos y patrones recomendados y a estandarizar la pila de tecnologías para la empresa. Lleva más de diez años trabajando con tecnologías de Microsoft. En sus tiempos libres le gusta escribir en su blog (blog.longle.net) y jugar Call of Duty. Puede encontrarlo y seguirlo en Twitter en twitter.com/LeLong37.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Derick Bailey (Telerik) y Mike Wasson (Microsoft)