ASP.NET

Aplicaciones de una sola página: cree aplicaciones web modernas con ASP.NET que respondan bien

Mike Wasson

Descargar el ejemplo de código

Las aplicaciones de una sola página (SPA) son aplicaciones web que cargan una única página HTML y la actualizan en forma dinámica a medida que el usuario interactúa con la aplicación.

Las SPA emplean AJAX y HTML5 para crear aplicaciones web fluidas y que responden bien, sin necesidad de volver a cargar continuamente la página. Sin embargo, esto significa que gran parte del trabajo se realiza del lado cliente, en JavaScript. A los desarrolladores ASP.NET tradicionales este salto les puede costar trabajo. Afortunadamente existen muchos marcos JavaScript de código abierto que facilitan la creación de aplicaciones SPA.

En este artículo recorreré paso a paso la creación de una aplicación SPA sencilla. Sobre la marcha explicaré algunos conceptos fundamentales de la creación de SPA, como los patrones Model-View-Controller (MVC) y Model-View-ViewModel (MVVM), el enrutamiento y enlace de datos.

Acerca de la aplicación de ejemplo

La aplicación de ejemplo que creé es una simple base de datos de películas que se muestra en la figura 1. La columna del extremo izquierdo de la página presenta un listado con géneros. Al hacer clic en uno de estos, aparece un listado de películas de ese género. Al hacer clic en el botón Edit al lado de una entrada se puede editar esa entrada. Después de realizar las ediciones, basta con hacer clic en Save para enviar la actualización al servidor o en Cancel para revertir los cambios.

The Single-Page Application Movie Database App
Figura 1 Aplicación en una sola página para una base de datos de películas

Creé dos versiones diferentes de la aplicación, una con la biblioteca Knockout.js y otra con la biblioteca Ember.js. Las dos bibliotecas tienen enfoques diferentes, así que resulta instructivo compararlas. En ambos casos, la aplicación cliente resulta tener menos de 150 líneas de JavaScript. En el lado servidor, usé ASP.NET Web API para entregar JSON al cliente. Encontrará el código fuente de ambas versiones de la aplicación en github.com/MikeWasson/MoviesSPA.

(Observación: creé la aplicación con el candidato de versión comercial [RC] de Visual Studio 2013. Algunas cosas podrían cambiar en la versión released to manufacturing [RTM], pero no deberían afectar el código.)

Información previa

En una aplicación web tradicional, cada vez que esta llama al servidor, el servidor presenta una página HTML nueva. Esto desencadena una actualización de la página en el explorador. Si alguna vez escribió una aplicación con Web Forms o PHP, este ciclo de vida de página debería resultarle conocido.

En una SPA, una vez que se carga la primera página, toda la interacción con el servidor ocurre a través de llamadas AJAX. Estas llamadas AJAX devuelven datos, no marcado, generalmente en formato JSON. La aplicación usa los datos JSON para actualizar la página en forma dinámica, sin volver a cargarla. En la figura 2 se ilustra la diferencia entre ambos métodos.

The Traditional Page Lifecycle vs. the SPA Lifecycle
Figura 2 Ciclo de vida de la página tradicional frente a las SPA

Una ventaja de las SPA salta a la vista: las aplicaciones son más fluidas y sensibles, sin el efecto discordante cuando la página se vuelve a cargar y presentar. Otra ventaja puede resultar menos obvia y tiene que ver con la arquitectura de la aplicación. Al enviar los datos de la aplicación en forma de JSON, se crea una separación entre la presentación (marcado HTML) y la lógica de la aplicación (solicitudes AJAX más respuestas JSON).

Esta separación simplifica el diseño y el desarrollo de cada nivel. En una SPA con una buena arquitectura, podemos cambiar el marcado HTML sin tocar el código que implementa la lógica de la aplicación (al menos en el caso ideal). Veremos esto en marcha cuando analice el enlace de datos más adelante.

En una aplicación SPA pura, toda la interacción de la interfaz de usuario se produce en el lado cliente, mediante JavaScript y CSS. Después de la carga inicial de la página, el servidor actúa meramente como nivel de servicio. El cliente solo necesita saber qué solicitudes HTTP debe enviar. No le importa cómo implementa el servidor las cosas en el back end.

Con esta arquitectura, el cliente y el servicio son independientes. Podríamos reemplazar todo el back end que ejecuta el servicio y, siempre y cuando no cambiemos la API, no echaremos a perder el cliente. También se cumple el caso contrario: podemos reemplazar toda la aplicación cliente sin realizar cambios en el nivel de servicio. Por ejemplo, podríamos escribir un cliente móvil nativo que consuma el servicio.

Creación del proyecto en Visual Studio

Visual Studio 2013 tiene un solo tipo de proyecto de Aplicación web ASP.NET. El asistente de proyectos permite seleccionar los componentes de ASP.NET que se incluirán en el proyecto. Comencé con la plantilla Vacío y luego agregué ASP.NET Web API al proyecto al activar Web API bajo “Agregar carpetas y referencias centrales para:”, tal como se aprecia en la figura 3.

Creating a New ASP.NET Project in Visual Studio 2013
Figura 3 Creación de un proyecto ASP.NET nuevo en Visual Studio 2013

El nuevo proyecto cuenta con todas las bibliotecas necesarias para Web API, además de un poco de código de configuración. No establecí ninguna dependencia con Web Forms ni con ASP.NET MVC.

Observe en la figura 3 que Visual Studio 2013 incluye una sola plantilla de Aplicación de una sola página. Esta plantilla instala el esqueleto de una SPA basada en Knockout.js. Permite un inicio de sesión mediante una base de datos de membrecía o algún proveedor de autenticación externo. Sin embargo, no usé la plantilla en mi aplicación, ya que quería mostrar un ejemplo más sencillo al partir de cero. No obstante, la plantilla de SPA es un recurso excelente, especialmente cuando queremos agregar código de autenticación a la aplicación.     

Creación del nivel de servicio

Usé ASP.NET Web API para crear una API basada en REST para la aplicación. No entraré en detalles sobre Web API aquí; puede informarse más en asp.net/web-api.

Primero creé una clase llamada Movie que representa una película. Esta clase hace dos cosas:

  • Le indica a Entity Framework (EF) cómo crear las tablas de la base de datos para almacenar los datos de la película.
  • Le indica a Web API cómo dar formato JSON a la carga útil.

Usted no tiene por qué usar necesariamente el mismo modelo para las dos. Por ejemplo, podría querer un esquema de la base de datos diferente a las cargas de trabajo en JSON. Para esta aplicación, simplifiqué las cosas al máximo:

namespace MoviesSPA.Models
{
  public class Movie
  {
    public int ID { get; set; }
    public string Title { get; set; }
    public int Year { get; set; }
    public string Genre { get; set; }
    public string Rating { get; set; }
  }
}

Luego usé el scaffolding (generación automática de UI) de Visual Studio para crear un controlador de Web API que emplee EF como el nivel de datos. Para usar el scaffolding, haga clic con el botón secundario en la carpeta Controladores en el Explorador de soluciones y seleccione Agregar | Nuevo elemento de scaffolding. En el asistente Agregar scaffolding, seleccione “Controlador Web API 2 con acciones, con Entity Framework”, tal como se ilustra en la figura 4.

Adding a Web API Controller
Figura 4 Adición de un controlador Web API

En la figura 5 vemos el asistente Agregar controlador. Le puse el nombre MoviesController al controlador. El nombre es importante, ya que las URI de la API REST se basan en el nombre del controlador. También activé “Usar acciones de controlador asincrónicas” para aprovechar la nueva característica async de EF 6. Seleccioné la clase Movie como el modelo y seleccioné “Nuevo contexto de datos” para crear un contexto de datos EF nuevo.

The Add Controller Wizard
Figura 5 Asistente Agregar controlador

El asistente agrega dos archivos:

  • MoviesController.cs define el controlador Web API que implementa la API REST de la aplicación.
  • MovieSPAContext.cs es básicamente pegamento de EF que proporciona los métodos necesarios para realizar consultas en la base de datos subyacente.

En la figura 6 se ilustra la API REST predeterminada creada por el scaffolding.

Figura 6 API REST predeterminada creada por el scaffolding de Web API

Verbo HTTP URI Descripción
GET /api/movies Obtener un listado de todas las películas
GET /api/movies/{id} Obtener la película con el identificador igual a {id}
PUT /api/movies/{id} Actualizar la película con el identificador igual a {id}
POST /api/movies Agregar una película nueva a la base de datos
DELETE /api/movies/{id} Eliminar una película de la base de datos

Los valores en las llaves son marcadores de posición. Para obtener una película con un identificador igual a 5, por ejemplo, la URI es /api/movies/5.

Amplié esta API con un método nuevo que encuentra todas las películas que sean de un género específico:

public class MoviesController : ApiController
{
  public IQueryable<Movie> GetMoviesByGenre(string genre)
  {
    return db.Movies.Where(m =>
      m.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase));
  }
  // Other code not shown

El cliente coloca el género en la cadena de consulta de la URI. Por ejemplo, para obtener todas las películas con el género Drama, el cliente envía una consulta GET a /api/movies?genre=drama. Web API enlaza automáticamente el parámetro de consulta con el parámetro genre en el método GetMoviesByGenre.

Creación del cliente web

Hasta aquí, solamente creé una API REST. Si enviamos una solicitud GET a /api/movies?genre=drama, la respuesta HTTP sin procesar se ve así:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Date: Tue, 10 Sep 2013 15:20:59 GMT
Content-Length: 240
[{"ID":5,"Title":"Forgotten Doors","Year":2009,"Genre":"Drama","Rating":"R"}, {"ID":6,"Title":"Blue Moon June","Year":1998,"Genre":"Drama","Rating":"PG-13"},{"ID":7,"Title":"The Edge of the Sun","Year":1977,"Genre":"Drama","Rating":"PG-13"}]

Ahora debo escribir una aplicación cliente que haga algo útil con esto. El flujo de trabajo básico es:

  • La interfaz de usuario desencadena una solicitud AJAX.
  • Actualizar el HTML para presentar la carga de trabajo de respuesta.
  • Controlar los errores de AJAX.

Podríamos codificar todo esto a mano. Por ejemplo, con este código en jQuery se podría crear una lista con los títulos de las películas:

$.getJSON(url)
  .done(function (data) {
    // On success, "data" contains a list of movies
    var ul = $("<ul></ul>")
    $.each(data, function (key, item) {
      // Add a list item
      $('<li>', { text: item.Title }).appendTo(ul);
    });
  $('#movies').html(ul);
});

Este código tiene algunos problemas. Mezcla la lógica de la aplicación con la lógica de la presentación y está enlazado estrechamente al HTML. Además resulta tedioso para escribir. En vez de concentrarnos en la aplicación, perdemos tiempo en escribir controladores de eventos y código para manipular el DOM.

La solución está en construir sobre la base de un marco JavaScript. Afortunadamente, podemos escoger entre un amplio surtido de marcos JavaScript de código abierto. Algunos de los más populares son Backbone, Angular, Ember, Knockout, Dojo y JavaScriptMVC. La mayoría tiene alguna variante de los patrones MVC o MVVM, así que podría resultar conveniente que repasemos estos patrones.

Los patrones MVC y MVVM

El patrón MVC data de los años 80 y las primeras interfaces gráficas. El objetivo de MVC es factorizar el código en tres responsabilidades independientes, tal como se ilustra en la figura 7. Esto es lo que hacen:

  • El modelo representa los datos del dominio y la lógica de negocios.
  • La vista representa el modelo.
  • El controlador recibe las entradas del usuario y actualiza el modelo.

The MVC Pattern
Figura 7 Patrón MVC

Una variante más actual de MVC es el patrón MVVM (ver figura 8). En MVVM:

  • El modelo sigue representando los datos del dominio.
  • El modelo de vista es una representación abstracta de la vista.
  • La vista presenta el modelo de vista y envía las entradas del usuario al modelo de vista.

The MVVM Pattern
Figura 8 Patrón MVVM

En un marco MVVM JavaScript, la vista consiste en marcado y el modelo de vista es código.

MVC tiene muchas variantes y la literatura sobre MVC a menudo es confusa y contradictoria. Quizás esto no resulte sorprendente en un patrón de diseño que comenzó con Smalltalk-76 y se sigue usando en las aplicaciones web modernas. Así que aun cuando es bueno saberse la teoría, lo más importante es entender el marco MVC puntual que estamos usando.

Creación del cliente web con Knockout.js

Para la primera versión de mi aplicación usé la biblioteca Knockout.js. Knockout sigue el patrón MVVM y conecta la vista con el modelo de vista por medio de enlace de datos.

Para crear los enlaces de datos, agregamos un atributo de enlace de datos especial a los elementos HTML. Por ejemplo, el siguiente marcado enlaza el elemento span a una propiedad llamada genre en el modelo de vista. Cada vez que cambia el valor de genre, Knockout actualiza automáticamente el HTML:

<h1><span data-bind="text: genre"></span></h1>

Los enlaces también funcionan en la dirección contraria: si el usuario escribe texto en un cuadro de texto, por ejemplo, entonces Knockout actualiza la propiedad correspondiente en el modelo de vista.

Lo bueno es que el enlace de datos es declarativo. No tenemos que conectar el modelo de vista con los elementos de la página HTML. Simplemente agregamos el atributo de enlace de datos y Knockout se encarga de todo lo demás.

Comencé por crear una página HTML con el diseño básico, sin enlace de datos, tal como se aprecia en la figura 9.

(Observación: usé la biblioteca Bootstrap para aplicar los estilos a la aplicación, así que la aplicación real tiene muchos elementos <div> adicionales y clases CSS para controlar el formato. Los omití en los ejemplos por motivos de claridad.)

Figura 9 Diseño inicial del HTML

<!DOCTYPE html>
<html>
<head>
  <title>Movies SPA</title>
</head>
<body>
  <ul>
    <li><a href="#"><!-- Genre --></a></li>
  </ul>
  <table>
    <thead>
      <tr><th>Title</th><th>Year</th><th>Rating</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><!-- Title --></td>
        <td><!-- Year --></td>
        <td><!-- Rating --></td></tr>
    </tbody>
  </table>
  <p><!-- Error message --></p>
  <p>No records found.</p>
</body>
</html>

Creación del modelo de vista

Los objetos observables son el elemento central del sistema de enlace de datos de Knockout. Un objeto observable es un objeto que almacena un valor y que puede notificar a los suscriptores cuando este valor cambia. El siguiente código convierte a la representación en JSON de una película en el objeto equivalente con propiedades observables:

function movie(data) {
  var self = this;
  data = data || {};
  // Data from model
  self.ID = data.ID;
  self.Title = ko.observable(data.Title);
  self.Year = ko.observable(data.Year);
  self.Rating = ko.observable(data.Rating);
  self.Genre = ko.observable(data.Genre);
};

En la figura 10 vemos la implementación inicial del modelo de vista. Esta versión solo permite obtener la lista de películas. Luego agregaré las funciones para editar. El modelo de vista contiene objetos observables para la lista de películas, una cadena de error y el género actual.

Figura 10 Modelo de vista

var ViewModel = function () {           
  var self = this;
  // View model observables
  self.movies = ko.observableArray();
  self.error = ko.observable();
  self.genre = ko.observable();  // Genre the user is currently browsing
  // Available genres
  self.genres = ['Action', 'Drama', 'Fantasy', 'Horror', 'Romantic Comedy'];
  // Adds a JSON array of movies to the view model
  function addMovies(data) {
    var mapped = ko.utils.arrayMap(data, function (item) {
      return new movie(item);
    });
    self.movies(mapped);
  }
  // Callback for error responses from the server
  function onError(error) {
    self.error('Error: ' + error.status + ' ' + error.statusText);
  }
  // Fetches a list of movies by genre and updates the view model
  self.getByGenre = function (genre) {
    self.error(''); // Clear the error
    self.genre(genre);
    app.service.byGenre(genre).then(addMovies, onError);
  };
  // Initialize the app by getting the first genre
  self.getByGenre(self.genres[0]);
}
// Create the view model instance and pass it to Knockout
ko.applyBindings(new ViewModel());

Observe que movies es un observableArray. Tal como lo dice su nombre, un observableArray actúa como una matriz que notifica a los suscriptores cuando cambia el contenido de la matriz.

La función getByGenre realiza una solicitud AJAX al servidor para obtener la lista de películas y luego rellena la matriz self.movies con los resultados.

Cuando consumimos una API REST, una de las partes más difíciles es controlar la naturaleza asincrónica de HTTP. La función ajax de jQuery devuelve un objeto que implementa la API Promises. Podemos usar el método then de Promise para establecer una devolución de llamada que se invoca cuando la llamada AJAX finaliza correctamente y otra devolución de llamada cuando la llamada AJAX genera un error:

app.service.byGenre(genre).then(addMovies, onError);

Enlaces de datos

Ahora que tengo un modelo de vista, puedo enlazarlo por datos al HTML. Para la lista de géneros que aparece al lado izquierdo de la pantalla, usé los siguientes enlaces de datos:

<ul data-bind="foreach: genres">
  <li><a href="#"><span data-bind="text: $data"></span></a></li>
</ul>

El atributo data-bind contiene una o más declaraciones de enlaces, donde cada enlace tiene la forma “enlace: expresión”. En este ejemplo, el enlace foreach le indica a Knockout que recorra el contenido de la matriz genres en el modelo de vista. Para cada elemento en la matriz, Knockout crea un nuevo elemento <li>. El enlace text en el elemento <span> establece el texto del elemento span en el valor del elemento de la matriz: en este caso, el nombre del género.

Ahora mismo, al hacer clic en los nombres de género no pasa nada, así que agregué un enlace del tipo click para controlar los eventos de clic:

<li><a href="#" data-bind="click: $parent.getByGenre">
  <span data-bind="text: $data"></span></a></li>

Esto enlaza el evento click a la función getByGenre en el modelo de vista. Tuve que usar $parent aquí, ya que este enlace se produce dentro del contexto del enlace foreach. De manera predeterminada, los enlaces dentro de un foreach se refieren al elemento actual en el bucle.

Para mostrar la lista de películas, agregué los enlaces a la tabla, tal como se aprecia en la figura 11.

Figura 11 Adición de enlaces a la tabla para presentar una lista de películas

<table data-bind="visible: movies().length > 0">
  <thead>
    <tr><th>Title</th><th>Year</th><th>Rating</th><th></th></tr>
  </thead>
  <tbody data-bind="foreach: movies">
    <tr>
      <td><span data-bind="text: Title"></span></td>
      <td><span data-bind="text: Year"></span></td>
      <td><span data-bind="text: Rating"></span></td>
      <td><!-- Edit button will go here --></td>
    </tr>
  </tbody>
</table>

En la figura 11, el enlace foreach recorre los objetos movie de una matriz. Dentro del enlace foreach, los enlaces text se refieren a propiedades del objeto actual.

El enlace visible en el elemento <table> controla si la tabla se presenta o no. Esto ocultará la tabla si la matriz movies está vacía.

Finalmente, estos son los enlaces del mensaje de error y el mensaje “No records found” (observe que podemos usar expresiones complejas en un enlace):

<p data-bind="visible: error, text: error"></p>
<p data-bind="visible: !error() && movies().length == 0">No records found.</p>

Registros editables

La última parte de esta aplicación es permitir que el usuario edite los registros de la tabla. Esto implica varios trozos de funcionalidad diferentes:

  • Alternar entre el modo de vista (texto sin formato) y modo de edición (controles de entrada).
  • Enviar las actualizaciones al servidor.
  • Permitir que el usuario cancele una edición y vuelva a los datos originales.

Para registrar el modo de visión/edición, agregué una marca booleana al objeto movie, en forma de propiedad observable:

function movie(data) {
  // Other properties not shown
  self.editing = ko.observable(false);
};

Quería que la tabla de películas presentara texto cuando la propiedad editing fuera false, pero cambiara a controles de entrada cuando editing fuera true. Para lograr esto, usé los enlaces if e ifnot de Knockout, tal como se ilustra en la figura 12. La sintaxis “<!-- ko -->” nos permite incluir enlaces if e ifnot sin colocarlos dentro un elemento contenedor de HTML.

Figura 12 Edición de registros de películas

<tr>
  <!-- ko if: editing -->
  <td><input data-bind="value: Title" /></td>
  <td><input type="number" class="input-small" data-bind="value: Year" /></td>
  <td><select class="input-small"
    data-bind="options: $parent.ratings, value: Rating"></select></td>
  <td>
    <button class="btn" data-bind="click: $parent.save">Save</button>
    <button class="btn" data-bind="click: $parent.cancel">Cancel</button>
  </td>
  <!-- /ko -->
  <!-- ko ifnot: editing -->
  <td><span data-bind="text: Title"></span></td>
  <td><span data-bind="text: Year"></span></td>
  <td><span data-bind="text: Rating"></span></td>
  <td><button class="btn" data-bind="click: $parent.edit">Edit</button></td>
  <!-- /ko -->
</tr>

El enlace value establece el valor de un control de entrada. Esto es un enlace bidireccional, así que cuando el usuario escribe algo en el campo de texto o cambia la selección de la lista desplegable, el cambio se propaga automáticamente al modelo de vista.

Enlacé los controladores click de los botones a las funciones llamadas save, cancel y edit en el modelo de vista.

La función edit es fácil. Simplemente establecemos la marca editing en true:

self.edit = function (item) {
  item.editing(true);
};

Las funciones save y cancel son un poco más difíciles. Para poder cancelar, necesito una forma para almacenar en caché el valor original durante la edición. Afortunadamente, Knockout permite extender fácilmente el comportamiento de los objetos observables. El código de la figura 13 agrega una función llamada store a la clase observable. Al llamar la función store en un objeto observable este recibe dos funciones nuevas: revert y commit.

Figura 13 Extensión de la clase observable ko.observable con revert y commit

Ahora puedo llamar la función store para agregar este comportamiento al modelo:

function movie(data) {
  // ...
  // New code:
  self.Title = ko.observable(data.Title).store();
  self.Year = ko.observable(data.Year).store();
  self.Rating = ko.observable(data.Rating).store();
  self.Genre = ko.observable(data.Genre).store();
};

En la figura 14 se ilustran las funciones save y cancel en el modelo de vista.

Figura 14 Adición de las funciones save y cancel

self.cancel = function (item) {
  revertChanges(item);
  item.editing(false);
};
self.save = function (item) {
  app.service.update(item).then(
    function () {
      commitChanges(item);
    },
    function (error) {
      onError(error);
      revertChanges(item);
    }).always(function () {
      item.editing(false);
  });
}
function commitChanges(item) {
  for (var prop in item) {
    if (item.hasOwnProperty(prop) && item[prop].commit) {
      item[prop].commit();
    }
  }
}
function revertChanges(item) {
  for (var prop in item) {
    if (item.hasOwnProperty(prop) && item[prop].revert) {
      item[prop].revert();
    }
  }
}

Creación del cliente web con Ember

Para comparar, escribí otra versión de la aplicación con la biblioteca Ember.js.

Una aplicación Ember comienza con una tabla de enrutamiento que define la forma en que el usuario se desplazará por la aplicación:

window.App = Ember.Application.create();
App.Router.map(function () {
  this.route('about');
  this.resource('genres', function () {
    this.route('movies', { path: '/:genre_name' });
  });
});

La primera línea crea una aplicación Ember. La llamada a Router.map crea tres rutas. Cada ruta corresponde a una dirección URI o a un patrón URI:

/#/about
/#/genres
/#/genres/genre_name

Para cada ruta, podemos crear una plantilla HTML mediante la biblioteca de plantillas Handlebars.

Ember tiene una plantilla de primer nivel para la aplicación completa. Esta plantilla se presenta para todas las rutas. En la figura 15 se muestra la plantilla de aplicación de mi aplicación. Como puede observar, la plantilla consiste esencialmente en HTML, ubicado dentro de una etiqueta script con el atributo type=“text/x-handlebars”. La plantilla contiene marcado especial de Handlebars dentro de llaves dobles: {{ }}. Este marcado tiene una finalidad similar al atributo de enlace de datos en Knockout. Por ejemplo {{#linkTo}} crea un vínculo a una ruta.

Figura 15 Plantilla de nivel de aplicación de Handlebars

ko.observable.fn.store = function () {
  var self = this;
  var oldValue = self();
  var observable = ko.computed({
    read: function () {
      return self();
    },
    write: function (value) {
      oldValue = self();
      self(value);
    }
  });
  this.revert = function () {
    self(oldValue);
  }
  this.commit = function () {
    oldValue = self();
  }
  return this;
}
<script type="text/x-handlebars" data-template-name="application">
  <div class="container">
    <div class="page-header">
      <h1>Movies</h1>
    </div>
    <div class="well">
      <div class="navbar navbar-static-top">
        <div class="navbar-inner">
          <ul class="nav nav-tabs">
            <li>{{#linkTo 'genres'}}Genres{{/linkTo}} </li>
            <li>{{#linkTo 'about'}}About{{/linkTo}} </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="container">
      <div class="row">{{outlet}}</div>
    </div>
  </div>
  <div class="container"><p>&copy;2013 Mike Wasson</p></div>
</script>

Ahora supongamos que el usuario navega a /#/about. Esto invoca la ruta “about”. Ember primero presenta la plantilla de aplicación de nivel superior. Luego presenta la plantilla about dentro de {{outlet}} en la plantilla de aplicación. Esta es la plantilla about:

 

<script type="text/x-handlebars" data-template-name="about">
  <h2>Movies App</h2>
  <h3>About this app...</h3>
</script>

En la figura 16 vemos cómo se representa la plantilla about dentro de la plantilla de la aplicación.

Rendering the About Template
Figura 16 Representación de la plantilla about

Como cada ruta tiene su propia dirección URI, se conserva el historial del explorador. El usuario puede desplazarse con el botón Atrás. El usuario también puede actualizar la página sin perder el contexto o marcarla y volver a cargar la misma página.

Controladores y modelos de Ember

En Ember, cada ruta tiene un modelo y un controlador. El modelo contiene los datos del dominio. El controlador actúa como proxy del modelo y almacena todo el estado de la aplicación para la vista. (Esto no coincide exactamente con la definición clásica de MVC. En ciertos aspectos, el controlador se comporta más como un modelo de vista.)

Así es como definí el modelo movie:

App.Movie = DS.Model.extend({
  Title: DS.attr(),
  Genre: DS.attr(),
  Year: DS.attr(),
  Rating: DS.attr(),
});

El controlador se deriva de Ember.ObjectController, tal como se aprecia en la figura 17.

Figura 17 El controlador movie se deriva de Ember.ObjectController

App.MovieController = Ember.ObjectController.extend({
  isEditing: false,
  actions: {
    edit: function () {
      this.set('isEditing', true);
    },
    save: function () {
      this.content.save();
      this.set('isEditing', false);
    },
    cancel: function () {
      this.set('isEditing', false);
      this.content.rollback();
    }
  }
});

Aquí suceden algunas cosas interesantes. Primero, no especifiqué el modelo en la clase del controlador. De manera predeterminada, la ruta establece el modelo automáticamente en el controlador. Segundo, las funciones save y cancel emplean las funciones de transacción integradas en la clase DS.Model. Para revertir las ediciones, llamamos simplemente la función rollback en el modelo.

Ember emplea muchas convenciones de nomenclatura para conectar los diferentes componentes. La ruta genres habla con GenresController, que representa la plantilla genres. De hecho, Ember creará automáticamente un objeto GenresController si no lo definimos nosotros. Sin embargo, podemos reemplazar el comportamiento predeterminado.

En mi aplicación configuré la ruta genres/movies para que use un controlador diferente al implementar el enlace renderTemplate. De este modo, varias rutas pueden compartir el mismo controlador (ver figura 18).

Figura 18 Varias rutas pueden compartir el mismo controlador

App.GenresMoviesRoute = Ember.Route.extend({
  serialize: function (model) {
    return { genre_name: model.get('name') };
  },
  renderTemplate: function () {
    this.render({ controller: 'movies' });
  },
  afterModel: function (genre) {
    var controller = this.controllerFor('movies');
    var store = controller.store;
    return store.findQuery('movie', { genre: genre.get('name') })
    .then(function (data) {
      controller.set('model', data);
  });
  }
});

Una ventaja de Ember es que permite hacer cosas con muy poco código. Mi aplicación de ejemplo tiene aproximadamente 110 líneas de JavaScript. Esto es menos que la versión de Knockout y más encima recibo un historial de explorador de regalo. Por otro lado, Ember también es un marco “tendencioso”. Si no escribimos el código “a la manera de Ember”, probablemente nos tropezaremos con impedimentos serios. Al elegir un marco, debemos tener en cuenta si el conjunto de características y el diseño general del marco calzan con nuestras necesidades y estilo de programación.

Información adicional

En este artículo mostré cómo los marcos para JavaScript simplifican la creación de aplicaciones SPA. En el camino, presenté algunas características comunes de estas bibliotecas, como el enlace de datos y los patrones MVC y MVVM. Puede obtener más información sobre la creación de SPA con ASP.NET en asp.net/single-page-application.

Mike Wasson es escritor programador en Microsoft. Durante muchos años ha documentado las API de multimedia de Win32. Actualmente escribe sobre ASP.NET, con un enfoque en Web API. Puede ponerse en contacto con él en mwasson@microsoft.com.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Xinyang Qiu (Microsoft)
Xinyang Qiu es ingeniero de diseño de software sénior de pruebas en el equipo de Microsoft ASP.NET, además de bloguero activo para blogs.msdn.com/b/webdev. Con mucho gusto responderá preguntas sobre ASP.NET o dirigirá a otros expertos para que respondan a sus preguntas. Puede encontrarlo en xinqiu@microsoft.com.