ASP.NET

Aplicativos de página única: Crie aplicativos Web dinâmicos e modernos com o ASP.NET

Mike Wasson

Baixar o código de exemplo

SPAs (Aplicativos de página única) são aplicativos Web que carregam uma única página HTML e atualizam essa página dinamicamente à medida que o usuário interage com o aplicativo.

Os SPAs usam o AJAX e o HTML5 para criar aplicativos Web fluidos e dinâmicos, sem recarregar a página constantemente. No entanto, isso significa que muito do trabalho acontece no lado do cliente, no JavaScript. Para desenvolvedores tracionais do ASP.NET, pode ser difícil fazer a mudança. Felizmente, há muitas estruturas JavaScript que facilitam a criação de SPAs.

Neste artigo, explicarei como criar um aplicativo SPA simples. Ao longo do caminho, apresentarei alguns conceitos fundamentais para criar SPAs, incluindo os padrões MVC (Model-View-Controller) e MVVM (Model-View-ViewModel), vinculação e roteamento de dados.

Sobre o aplicativo de exemplo

O aplicativo de exemplo que criei é um banco de dados de filmes, mostrado na Figura 1. A coluna da extrema esquerda da página exibe uma lista de gêneros. Um clique em um gênero abre uma lista de filmes dentro daquele gênero. Um clique no botão Editar ao lado de uma entrada permite alterar aquela entrada. Depois de fazer as edições, você pode clicar em Salvar para enviar a atualização ao servidor ou em Cancelar para reverter as alterações.

The Single-Page Application Movie Database App
Figura 1 O aplicativo de banco de dados de filmes do Aplicativo de Página Única

Criei duas versões diferentes do aplicativo, uma usando a biblioteca Knockout.js e outra usando a biblioteca Ember.js. Essas duas bibliotecas têm abordagens diferentes, portanto, é instrutivo compará-las. Nos dois casos, o aplicativo cliente tinha menos do que 150 linhas de JavaScript. No lado do servidor, usei a API Web ASP.NET para atender o JSON para o cliente. Você pode localizar o código fonte para as duas versões do aplicativo em github.com/MikeWasson/MoviesSPA.

Observação: criei o aplicativo usando a versão RC [Release Candidate] do Visual Studio 2013. Algumas coisas podem ser alteradas na versão RTM [Released to Manufacturing], mas não devem afetar o código.

Segundo plano

Em um aplicativo Web tradicional, toda vez que o aplicativo chama o servidor, o servidor renderiza uma nova página HTML. Isso dispara uma atualização de página no navegador. Se você já escreveu um aplicativo Web Forms ou PHP, o ciclo de vida dessa página deverá ser familiar.

Em um SPA, depois que a primeira página é carregada, toda a interação com o servidor ocorre por meio de chamadas AJAX. Essas chamadas AJAX retornam dados, não marcação, normalmente no forma JSON. O aplicativo usa os dados JSON para atualizar a página dinamicamente, sem recarregar a página. A Figura 2 ilustra a diferença entre as duas abordagens.

The Traditional Page Lifecycle vs. the SPA Lifecycle
Figura 2 O ciclo de vida de páginas tradicionais versus o ciclo de vida do SPA

Um benefício dos SPAs é óbvio: Os aplicativos são mais fluidos e dinâmicos, sem o efeito dissonante de recarregamento e nova renderização da página. Outro benefício pode ser menos óbvio e se refere a como você arquiteta um aplicativo Web. O envio de dados do aplicativo como JSON cria uma separação entre a apresentação (marcação HTML) e a lógica do aplicativo (solicitações AJAX mais respostas JSON).

Essa separação facilita o design e a evolução de cada camada. Em um SPA bem-arquitetado, você pode alterar a marcação HTML sem tocar no código que implementa a lógica do aplicativo (pelo menos, esse é o ideal). Você verá isso em ação quando eu discutir a vinculação de dados mais tarde.

Em um SPA puro, toda a interação da interface do usuário ocorre no lado do cliente, por meio do JavaScript e do CSS. Depois do carregamento da página inicial, o servidor age puramente como uma camada de serviço. O cliente precisa apenas saber quais solicitações HTTP enviar. Ele não se preocupa com a maneira como o servidor implementa coisas no back-end.

Com essa arquitetura, o cliente e o serviço são independentes. Você pode substituir todo o back-end que executa o serviço e, se não alterar a API, você não interromperá o cliente. O reverso também é verdadeiro, você pode substituir todo o aplicativo cliente sem alterar a camada de serviço. Por exemplo, você pode escrever um cliente móvel nativo que consome o serviço.

Criando o projeto do Visual Studio

O Visual Studio 2013 tem um único tipo de projeto de aplicativo Web ASP.NET. O assistente de projetos permite selecionar os componentes do ASP.NET a serem incluídos no seu projeto. Comecei com o Modelo vazio e adicionei a API Web ASP.NET ao projeto selecionando API Web em “Add folders and core references for:”, conforme mostrado na Figura 3.

Creating a New ASP.NET Project in Visual Studio 2013
Figura 3 Criando um novo projeto ASP.NET no Visual Studio 2013

O novo projeto tem todas as bibliotecas necessárias para a API Web, mais algum código de configuração da API Web. Não usei nenhuma dependência no Web Forms ou no ASP.NET MVC.

Na Figura 3, observe que o Visual Studio 2013 inclui um modelo de Aplicativo de Página Única. Esse modelo instala um SPA esqueleto criado no Knockout.js. Oferece suporte a logon usando um banco de dados de associação ou um provedor de autenticação externo. Não usei o modelo em meu aplicativo porque queria mostrar um exemplo mais simples a partir do zero. O modelo do SPA é um grande recurso, de qualquer maneira, principalmente se você desejar adicionar autenticação a seu aplicativo.     

Criando a camada de serviço

Use a API Web ASP.NET para criar uma API REST simples para o aplicativo. Não entrarei em detalhes sobre a API Web aqui, você pode obter mais informações em asp.net/web-api.

Primeiro, criei uma classe Movie que representa um filme. Essa classe faz duas coisas:

  • informa ao Entity Framework (EF) como criar as tabelas do banco de dados para armazenar os dados do filme.
  • Informa à API Web como formatar o conteúdo JSON.

Você não precisa usar o mesmo modelo para ambos. Por exemplo, você pode desejar que o esquema de seu banco de dados tenha uma aparência diferente da do conteúdo JSON. Neste aplicativo, simplifiquei as coisas:

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

Em seguida, usei o scaffolding do Visual Studio para criar um controlador da API Web que usa o EF como a camada de dados. Para usar o scaffolding, clique com o botão direito do mouse na pasta Controladores no Gerenciador de Soluções e selecione Add | New Scaffolded Item. No assistente Add Scaffold, selecione “Web API 2 Controller with actions, using Entity Framework”, conforme mostrado na Figura 4.

Adding a Web API Controller
Figura 4 Adicionando um controlador de API Web

A Figura 5 mostra o assistente Add Controller. Denominei o controlador como MoviesController. O nome é importante porque as URIs do API REST são baseadas no nome do controlador. Também selecionei “Use async controller actions” para tirar proveito do novo recurso assíncrono do EF 6. Selecionei a classe Movie para o modelo e “New data context” para criar um novo contexto de dados do EF.

The Add Controller Wizard
Figura 5 O Add Controller Wizard

O assistente adiciona dois arquivos:

  • MoviesController.cs define o controlador da API Web que implementa a API REST do aplicativo.
  • MovieSPAContext.cs é basicamente uma cola EF que fornece métodos para consultar o banco de dados subjacente.

A Figura 6 mostra a API REST padrão criada pelo scaffolding.

Figura 6 A API REST padrão criada pelo scaffolding da API Web.

Verbo HTTP URI Descrição
GET /api/movies Obter uma lista de todos os filmes
GET /api/movies/{id} Obter o filme com ID igual a {id}
PUT /api/movies/{id} Atualizar o filme com ID igual a {id}
POST /api/movies Adicionar um novo filme ao banco de dados
DELETE /api/movies/{id} Excluir um filme do banco de dados

Os valores entre colchetes são espaços reservados. Por exemplo, para obter um filme com ID igual a 5, a URI é /api/movies/5.

Estendi essa API adicionando um método que localiza todos os filmes de um gênero especificado:

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

O cliente coloca o gênero na sequência de caracteres da consulta da URI. Por exemplo, para obter todos os filmes do gênero Drama, o cliente envia uma solicitação de GET a /api/movies?genre=drama. A API Web vincula automaticamente o parâmetro query ao parâmetro genre no método GetMoviesByGenre.

Criando o cliente Web

Até agora, apenas criei uma API REST. Se você enviar uma solicitação GET a /api/movies?genre=drama, a resposta HTTP bruta será semelhante a esta:

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"}]

Agora, preciso escrever um aplicativo cliente que dê algum sentido a isso. O fluxo de trabalho básico é:

  • a interface do usuário dispara uma solicitação AJAX
  • Atualizar o HTML para exibir o conteúdo da resposta
  • Manipular os erros do AJAX

Você pode codificar tudo isso manualmente. Por exemplo, este é um código jQuery que cria uma lista de títulos de filmes:

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

Esse código tem alguns problemas. Mistura a lógica do aplicativo com a lógica da apresentação e está fortemente vinculado a seu HTML. Além disso, é tedioso escrevê-lo. Em vez de se concentrar no aplicativo, você gasta tempo escrevendo manipuladores de eventos e código para manipular o DOM.

A solução é criar com base em uma estrutura JavaScript. Felizmente, você pode escolher entre muitas estruturas JavaScript de software livre. Algumas das mais populares incluem Backbone, Angular, Ember, Knockout, Dojo e JavaScriptMVC. A maioria usa uma variação dos padrões MVC ou MVVM, portanto, pode ser útil rever esses padrões.

Os padrões MVC e MVVM

O padrão MVC é da década de 80 e das primeiras interfaces gráficas do usuário. A meta do MVC é fatorar o código em três responsabilidades separadas, mostradas na Figura 7. Elas fazem o seguinte:

  • o modelo representa os dados do domínio e a lógica dos negócios.
  • A exibição mostra o modelo.
  • O controlador recebe a entrada do usuário e atualiza o modelo.

The MVC Pattern
Figura 7 O padrão MVC

Uma variante mais recente do MVC é o padrão MVVM (consulte a Figura 8). No MVVM:

  • o modelo ainda representa os dados do domínio.
  • O modelo de exibição é uma representação abstrata da exibição.
  • A exibição mostra o modelo de exibição e envia a entrada do usuário ao modelo de exibição.

The MVVM Pattern
Figura 8 O padrão MVVM

Em uma estrutura MVVM do JavaScript, a exibição é marcação e o modelo de exibição é código.

O MVC tem muitas variantes, e a literatura sobre o MVC costuma ser confusa e contraditória. Talvez isso não seja uma surpresa para um padrão de design que começou com o Smalltalk-76 e ainda está sendo usado em aplicativos Web modernos. Portanto, embora seja bom conhecer a teoria, o principal é entender a estrutura MVC específica que você usará.

Criando o cliente Web com Knockout.js.

Para a primeira versão de meu aplicativo, usei a biblioteca Knockout.js. O Knockout segue o padrão MVVM, usando a vinculação de dados para conectar a exibição com o modelo de exibição.

Para criar vinculações de dados, você adiciona um atributo de vinculação de dados especial aos elementos HTML. Por exemplo, a marcação a seguir vincula o elemento span a uma propriedade denominada genre no modelo de exibição. Sempre que o valor de genre for alterado, o Knockout atualiza o HTML automaticamente:

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

As vinculações também podem funcionar na outra direção, por exemplo, se o usuário inserir texto em uma caixa de texto, o Knockout atualizará a propriedade correspondente no modelo de exibição.

A parte boa é que a vinculação de dados é declarativa. Você não precisa conectar o modelo de exibição aos elementos da página HTML. Basta adicionar o atributo de vinculação de dados e o Knockout fará o restante.

Comecei criando uma página HTML com o layout básico, sem vinculação de dados, conforme mostrado na Figura 9.

Observação: Usei a biblioteca do Bootstrap para aplicar estilo ao aplicativo, portanto, o aplicativo real tem muitos elementos <div> extras e classes CSS para controlar a formatação. Deixei isso fora dos exemplos de código para maior clareza.

Figura 9 Layout HTML inicial

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

Criando o modelo de exibição

Os observáveis são o núcleo do sistema de vinculação de dados do Knockout. Um observável é um objeto que armazena um valor e pode notificar os assinantes quando o valor é alterado. O código a seguir converte a representação do JSON de um filme no objeto equivalente com observáveis:

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

A Figura 10 mostra a minha implementação inicial do modelo de exibição. Essa versão oferece suporte apenas à obtenção da lista de filmes. Adicionarei os recursos de edição mais tarde. O modelo de exibição contém observáveis para a lista de filmes, uma sequência de caracteres de erro e o gênero atual.

Figura 10 O modelo de exibição

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 é uma observableArray. Como o nome sugere, um observableArray funciona como uma matriz que notifica os assinantes quando o conteúdo da matriz é alterado.

A função getByGenre faz uma solicitação AJAX ao servidor para obter a lista de filmes e popula a matriz self.movies com os resultados.

Quando você consome uma API REST, uma das partes mais complicadas é manipular a natureza assíncrona do HTTP. A função jQuery do AJAX retorna um objeto que implementa a API Promises. Você pode usar um método then do objeto Promise para definir um retorno de chamada que é invocado quando a chamada AJAX é concluída com êxito e outro retorno de chamada que será invocado se houver falha na chamada AJAX:

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

Vinculações de dados

Agora que tenho um modelo de exibição, posso vincular dados do HTML a ele. Para a lista de gêneros que aparece no lado esquerdo da tela, usei as seguintes vinculações de dados:

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

O atributo data-bind contém uma ou mais declarações de vinculação, onde cada vinculação tem a forma “binding: expressão”. Neste exemplo, a vinculação de foreach informa ao Knockout para fazer loop pelo conteúdo da matriz de gêneros no modelo de exibição. Para cada item da matriz, o Knockout cria um novo elemento <li>. A vinculação de texto no <span> define o texto de expansão igual ao valor do item da matriz que, neste caso, é o nome do gênero.

No momento, clicar nos nomes dos gêneros não faz nada, portanto, adicionei uma vinculação de clique para manipular eventos de cliques:

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

Isso vincula o evento de clique à função getByGenre no modelo de exibição. Eu precisava usar $parent aqui porque essa vinculação ocorre dentro do contexto de foreach. Por padrão, as vinculações dentro de um foreach se referem ao item atual no loop.

Para exibir a lista de filmes, adicionei vinculações à tabela, como mostrado na Figura 11.

Figura 11 Adicionando vinculações à tabela para exibir uma lista de filmes

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

Na Figura 11, a vinculação foreach executa loops em uma matriz de objetos movie. Dentro do foreach, as vinculações de texto se referem a propriedades no objeto atual.

A vinculação visível no elemento <table> controla se a tabela é renderizada. Isso ocultará a tabela se a matriz de filmes estiver vazia.

Finalmente, aqui estão as vinculações para a mensagem de erro e a mensagem “No records found” (observe que você pode colocar expressões complexas em uma vinculação):

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

Tornando os registros editáveis

A última parte deste aplicativo é dar ao usuário a capacidade de editar os registros da tabela. Isso envolve vários bits de funcionalidade:

  • alternar entre o modo de exibição (texto sem formatação) e o modo de edição (controles de entrada).
  • Enviar atualizações ao servidor.
  • Permitir que o usuário cancele uma edição e reverta para os dados originais.

Para acompanhar o modo de exibição/edição, adicionei um sinalizador booliano ao objeto movie, como um observável:

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

eu queria que a tabela de filmes exibisse texto quando a propriedade editing fosse false, mas alternasse para os controles de entrada quando a edição fosse true. Para tanto, usei as vinculações if e ifnot do Knockout, conforme mostrado na Figura 12. A sintaxe “<!-- ko -->” permite incluir as vinculações if e ifnot sem colocá-las dentro de um elemento contêiner HTML.

Figura 12 Permitindo a edição de registros de filmes

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

A vinculação de valor define o valor de um controle de entrada. Essa é uma vinculação bidirecional, portanto, quando o usuário digita algo no campo de texto ou altera a seleção do menu suspenso, a alteração é propagada automaticamente para o modelo de exibição.

Vinculei os manipuladores de cliques de botão a funções denominadas save, cancel e edit no modelo de exibição.

A função de edição é fácil. Basta definir o sinalizador de editing como true:

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

Save e cancel foram um pouco mais complicadas. Para oferecer suporte a cancel, eu precisava de uma maneira de armazenar o valor original em cache durante a edição. Felizmente, o Knockout facilita estender o comportamento de observáveis. O código na Figura 13 adiciona uma função store à classe observable. Chamar a função store em um observável dá a observable duas novas funções: revert e commit.

Figura 13 Estendendo ko.observable com revert e commit

Agora, posso chamar a função store para adicionar essa funcionalidade ao 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();
};

A Figura 14 mostra as funções save e cancel no modelo de exibição.

Figura 14 Adicionando as funções save e 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();
    }
  }
}

Criando o cliente Web com Ember

Para comparação, escrevi outra versão de meu aplicativo usando a biblioteca Ember.js.

Um aplicativo Ember começa com uma tabela de roteamento que define como o usuário navegará pelo aplicativo:

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

A primeira linha de código cria um aplicativo Ember. A chamada para Router.map cria três rotas. Cada rota corresponde a uma URI ou a um padrão de URI:

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

Para cada rota, você cria um modelo HTML usando a biblioteca de modelos Handlebars.

O Ember tem um modelo de nível superior para todo o aplicativo. Esse modelo é renderizado para cada rota. A Figura 15 mostra o modelo do meu aplicativo. Como você pode ver, o modelo é basicamente HTML colocado em uma marca de script com type=“text/x-handlebars”. O modelo contém marcação especial de Handlebars dentro de colchetes duplos: {{ }}. Essa marcação atende a um objetivo semelhante, como o atributo data-bind no Knockout. Por exemplo, {{#linkTo}} cria um link para uma rota.

Figura 15 O modelo Handlebars em nível de aplicativo.

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>

Agora suponha que o usuário navegue para /#/about. Isso invoca a rota “about”. O Ember primeiro renderiza o modelo do aplicativo de nível superior. Em seguida, renderiza o modelo about dentro de {{outlet}} do modelo do aplicativo. Este é o modelo about:

 

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

A Figura 16 mostra como o modelo about é renderizado dentro do modelo de aplicativo.

Rendering the About Template
Figura 16 Renderizando o modelo about

Como cada rota tem sua própria URI, o histórico do navegador é preservado. O usuário pode navegar com o botão Voltar. O usuário também pode atualizar a página sem perder o contexto ou marcar e recarregar a mesma página.

Controladores e modelos do Ember

No Ember, cada rota tem um modelo e um controlador. O modelo contém os dados do domínio. O controlador atua como um proxy para o modelo e armazena os dados de estado de qualquer aplicativo para exibição. Isso não corresponde exatamente à definição clássica do MVC. De certa forma, o controlador é mais parecido com um modelo de exibição.

Veja como defini o modelo movie:

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

O controlador deriva de Ember.ObjectController, conforme mostrado na Figura 17.

Figura 17 O controlador movie 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();
    }
  }
});

Há algumas coisas interessantes ocorrendo aqui. Primeiro, não especifiquei o modelo na classe do controlador. Por padrão, a rota define o modelo no controlador automaticamente. Segundo, as funções save e cancel usam os recursos da transação criada na classe DS.Model. Para reverter as edições, chamei a função rollback no modelo.

O Ember usa muitas das convenções de nomenclatura para se conectar a diferentes componentes. A rota de gêneros fala com o GenresController que renderiza o modelo genres. Na verdade, o Ember criará um objeto GenresController automaticamente se você não definir um. No entanto, você pode substituir os padrões.

Em meu aplicativo, configurei a rota genres/movies para usar um controlador diferente implementando o gancho renderTemplate. Dessa maneira, várias rotas podem compartilhar o mesmo controlador (consulte a Figura 18).

Figura 18 Várias rotas podem compartilhar o mesmo 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);
  });
  }
});

Uma coisa boa sobre o Ember é que você pode fazer coisas com muito pouco código. Meu aplicativo de exemplo é de cerca de 110 linhas de JavaScript. Menor do que a versão do Knockout e ainda ganho o histórico do navegador. Por outro lado, o Ember também é uma estrutura altamente “teimosa”. Se você não escrever seu código da “maneira do Ember”, você provavelmente vai encontrar alguns obstáculos. Ao escolher uma estrutura, você deve considerar se o conjunto de recursos e o design geral da estrutura corresponde às suas necessidades e estilo de codificação.

Saiba mais

Neste artigo, mostrei como as estruturas JavaScript facilitam a criação de SPAs. Ao longo do caminho, apresentei alguns recursos comuns dessas bibliotecas, incluindo vinculação de dados, roteamento e os padrões MVC e MVVM. Você pode aprender mais sobre a criação de SPAs com o ASP.NET em asp.net/single-page-application.

Mike Wasson trabalha como programador e redator na Microsoft. Por muitos anos, documentou as APIs multimídia do Win32. Atualmente, escreve sobre o ASP.NET, com foco em API da Web. Você pode entrar em contato com ele pelo email mwasson@microsoft.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Xinyang Qiu (Microsoft)
Xinyang Qiu é engenheiro de design de software sênior de teste da equipe do Microsoft ASP.NET e um blogueiro ativo no blogs.msdn.com/b/webdev. Ele tem prazer em responder dúvidas sobre o ASP.NET ou em direcionar suas dúvidas para especialistas. Entre em contato com ele pelo email xinqiu@microsoft.com.