ASP.NET MVC 5

Instruções elementares para um desenvolvedor de aplicativos de única página do .NET

Long Le

Baixar o código de exemplo

A grande maioria dos desenvolvedores do Microsoft .NET Framework gastou muito tempo de suas vidas profissionais no lado do servidor, codificando com o C# ou Visual Basic .NET ao criar aplicativos Web. Naturalmente, o JavaScript foi usado para coisas simples, como janelas modais, validação, chamadas do AJAX, etc. No entanto, o JavaScript (código do lado do cliente para a maioria) foi aproveitado como uma linguagem utilitária e os aplicativos foram amplamente impulsionados do lado do servidor.

Recentemente, houve uma grande tendência do código de aplicativo Web de migrar do lado do servidor para o lado do cliente (navegador) para atender às expectativas dos usuários por uma experiência de usuário fluida e ágil nas respostas. Assim sendo, muitos desenvolvedores do .NET (especialmente na empresa) estão tendo que lidar com uma enorme ansiedade em relação às práticas recomendadas, à arquitetura, ao teste de unidade, à capacidade de manutenção do JavaScript e à recente explosão de diferentes tipos de bibliotecas JavaScript. Parte da tendência de passar para o lado do cliente é o aumento do uso de SPAs (aplicativos de única página). Dizer que o desenvolvimento do SPA é o futuro, é um eufemismo extremo. Os SPAs são como alguns dos melhores aplicativos na Web que proporcionam experiência de usuário fluida e ágil nas respostas e que, ao mesmo tempo, minimizam as cargas de trabalho (tráfego) e viagens de ida e volta ao servidor.

Neste artigo, falarei da ansiedade que você pode sentir ao fazer a transição do lado do servidor para o realm do SPA. A melhor maneira de lidar com essa ansiedade é adotar o JavaScript como uma linguagem de primeira classe exatamente como qualquer linguagem .NET, como C#, Visual Basic .NET, Python, etc.

Veja a seguir alguns princípios fundamentais do desenvolvimento do .NET que, às vezes, são ignorados ou esquecidos no desenvolvimento de aplicativos no JavaScript:

  • Sua base de código é gerenciável no .NET porque você é decisivo com os limites de classe e com o local em que as classes residem nos seus projetos.
  • Você separa as preocupações para que não haja classes que sejam responsáveis por centenas de diferentes tarefas com responsabilidades sobrepostas.
  • Você tem repositórios, consultas, entidades (modelos) e fontes de dados reutilizáveis.
  • Você reflete em como nomear seus arquivos e classes para que sejam mais significativos.
  • Você pratica o bom uso dos padrões de design, das convenções de codificação e da organização.

Como este artigo é para desenvolvedores do .NET que estão sendo apresentados ao mundo do SPA, incorporarei o menor número de estruturas possível para criar um SPA gerenciável com arquitetura sólida.

Criando um SPA em sete etapas principais

Veja a seguir as sete etapas principais para converter um novo aplicativo Web ASP.NET que foi criado com o modelo pronto para uso do Visual Studio 2013 ASP.NET MVC em um SPA (com referências aos arquivos de projeto apropriados que podem ser encontrados no download do código de acompanhamento).

  1. Baixe e instale os pacotes RequireJS do NuGet, o plug-in de texto RequireJS e o Kendo UI Web.
  2. Adicione um módulo de configuração (Northwind.Web/Scripts/app/main.js).
  3. Adicione um módulo de aplicativo (Northwind.Web/Scripts/app/app.js).
  4. Adicione um módulo de roteador (Northwind.Web/Scripts/app/router.js).
  5. Adicione uma ação e um modo de exibição denominados Spa (Northwind.Web/Controllers/HomeController.cs e Northwind.Web/Views/Home/Spa.cshtml).
  6. Modifique o arquivo _ViewStart.cshtml para que o MVC carregue modos de exibição sem usar o arquivo _Layout.cshtml por padrão (Northwind.Web/Views/_ViewStart.cshtml).
  7. Atualize os links de navegação (menu) do layout para que correspondam às novas URLs amigáveis ao SPA (Northwind.Web/Views/Shared/_Layout.cshtml).

Depois que essas sete etapas forem executadas, a estrutura do projeto do aplicativo Web deverá ser parecida com a Figura 1.

ASP.NET MVC Project Structure
Figura 1 Estrutura do projeto do ASP.NET MVC

Mostrarei como criar um SPA impressionante no ASP.NET MVC com as seguintes bibliotecas JavaScript, disponíveis pelo NuGet:

  • RequireJS (requirejs.org): esse é um arquivo e carregador de módulo Java­Script. O RequireJS fornecerá APIs #include/import/require e a capacidade de carregar dependências aninhadas com DI (injeção de dependência). A abordagem de design do RequireJS usa a API AMD (Definição de Módulo Assíncrono) para módulos JavaScript, que ajuda a encapsular fragmentos do código em unidades úteis. Ela também é uma maneira intuitiva de fazer referência a outras unidades de código (módulos). Os módulos do RequireJS também seguem o padrão do módulo (bit.ly/18byc2Q). Uma implementação simplificada desse padrão usa funções do JavaScript para encapsulamento. Você verá esse padrão em ação posteriormente, pois todos os módulos do JavaScript serão encerrados em uma função "define" ou "require".
  • As pessoas familiarizadas com os conceitos de DI e IoC (Inversão de Controle) podem pensar nisso como uma estrutura de DI do lado do cliente. Se isso estiver tão claro quanto a noite no momento, não se preocupe; logo vou mostrar algumas ilustrações codificadas em que tudo isso faz sentido.
  • Plug-in de texto do RequireJS (bit.ly/1cd8lTZ): ele será usado para carregar remotamente parte do HTML (exibições) no SPA.
  • Entity Framework (bit.ly/1bKiZ9I): ele é bastante autoexplicativo. E como o foco deste artigo é o SPA, não falarei muito do Entity Framework. No entanto, se você não tiver experiência com ele, há bastante documentação disponível.
  • Kendo UI Web (bit.ly/t4VkVp): essa é uma abrangente estrutura do JavaScript/­HTML5 que engloba widgets de interface do usuário da Web, fontes de dados, modelos, o padrão MVVM (Model-View-ViewModel), SPAs, definição de estilo, etc., para ajudar a fornecer um aplicativo adaptável e ágil nas respostas com excelente aparência.

Configurando a infraestrutura do SPA

Para mostrar como configurar a infraestrutura do SPA, primeiramente explicarei como criar o módulo RequireJS (config) (Northwind.Web/Scripts/app/main.js). Esse módulo será o ponto de entrada de inicialização do aplicativo. Se você criou um aplicativo de console, pense nisso como o ponto de entrada Main no Program.cs. Ele basicamente contém a primeira classe e o método que é chamado quando o SPA é inicializado. O arquivo main.js, basicamente, atua como o manifesto do SPA e é onde você definirá onde todas as coisas no SPA estão e suas dependências, se houver. O código de configuração do RequireJS é mostrado na Figura 2.

Figura 2 Configuração do 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();
});

Na Figura 2, as propriedades de caminho contêm uma lista de onde todos os módulos estão localizados e seus nomes. Shim é o nome de um módulo definido anteriormente. A propriedade do shim inclui todas as dependências que o módulo pode ter. Nesse caso, você está carregando um módulo denominado kendo e ele tem uma dependência em um módulo denominado jquery. Desse modo, se um módulo exigir o módulo kendo, siga em frente e carregue jQuery primeiro, pois jQuery foi definido como uma dependência do módulo kendo.

Na Figura 2, o código “require([], function(){})” será carregado no próximo módulo, que é o módulo que chamei de aplicativo. Observe que, deliberadamente, dei nomes significativos aos módulos.

Mas como seu SPA sabe invocar esse módulo primeiro? Você configura isso na primeira página de aterrissagem do SPA com o atributo principal de dados na marca de referência do script para RequireJS. Especifiquei que ele executa o módulo principal (main.js). O RequireJS tratará de todo o trabalho pesado envolvido no carregamento desse módulo; você tem apenas que informar a ele qual módulo carregar primeiro.

Você tem duas opções para exibições do SPA que serão carregadas no SPA: páginas HTML padrão (*.html) ou ASP.NET MVC Razor (*.cshtml). Como este artigo é destinado aos desenvolvedores do .NET (e muitas empresas possuem bibliotecas do lado do servidor e estruturas que gostariam de continuar usando em suas exibições), vou usar a segunda opção de criar exibições Razor.

Começarei adicionando uma exibição e denominando-a Spa.cshtml, conforme mencionado anteriormente. Basicamente, essa exibição carregará o shell ou todo o HTML para o layout do SPA. Nesse exibição, carregarei as outras exibições (por exemplo, About.cshtml, Contact.cshtml, Index.cshtml, etc.) à medida que o usuário navega pelo SPA, alternando as exibições que substituem todo o HTML no div "content".

Criando a página de aterrissagem do SPA (layout) (Northwind.Web/Views/Spa.cshtml) Como a exibição Spa.cshtml é a página de aterrissagem do SPA, onde você carregará todas as outras exibições, não haverá muita marcação aqui, exceto a referência às folhas de estilo exigidas e ao RequireJS. Observe o atributo principal de dados no código a seguir, que informa ao RequireJS qual módulo carregar primeiro:

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

Adicionando uma ação para o layout do SPA (Northwind.Web/­Controllers/HomeController.cs) Para criar e carregar a exibição Spa.cshtml, adicione uma ação e exibição:

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

Crie o módulo do aplicativo (Northwind.Web/Scripts/app/app.js) Esse é o módulo Application, responsável por inicializar o roteador do Kendo UI:

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

Crie o módulo do roteador (Northwind.Web/Scripts/app/router.js) Ele é chamado por app.js. Se já estiver familiarizado com as rotas do ASP.NET MVC, a mesma noção se aplica aqui. Essas são as rotas do SPA para suas exibições. Definirei todas as rotas para todas as exibições do SPA, de modo que quando o usuário navegar pelo SPA, o roteador do Kendo UI saberá quais exibições carregar no SPA. Veja a Listagem 1 no download de acompanhamento.

A classe Router do Kendo UI é responsável por rastrear o estado do aplicativo e navegar entre os estados do aplicativo. O roteador se integra ao histórico do navegador usando a parte do fragmento da URL (#page), o que torna os estados do aplicativo possíveis de indicação e vinculação. Quando uma URL roteável é clicada, o roteador entra em ação e informa o aplicativo para se colocar de volta no estado que foi codificado na rota. A definição de rota é uma cadeia de caracteres que representa um caminho usado para identificar o estado do aplicativo que o usuário quer ver. Quando uma definição de rota é correspondida no fragmento de hash da URL do navegador, o manipulador de rotas é chamado (veja a Figura 3).

Figura 3 Definições de rota registradas e URLs correspondentes

Rota registrada (Definição) URL completa real (possível de indicação)
/ 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

Quanto ao widget do layout do Kendo UI, seu nome fala por si só. Provavelmente, você está familiarizado com o layout do MVC ou MasterPage do ASP.NET Web Forms incluído no projeto quando você cria um novo aplicativo Web ASP.NET MVC. Nesse projeto do SPA, ele está localizado no caminho Northwind.Web/Views/Shared/_Layout.cshtml. Há pouca diferença entre o layout do Kendo UI e do MVC, exceto o layout do Kendo UI que é executado no lado do cliente. Assim como o layout funcionava do lado do servidor, onde o tempo de execução do MVC substituía o conteúdo do layout por outras exibições, o layout do Kendo UI funciona exatamente da mesma forma. Você alterna a exibição (conteúdo) do layout do Kendo UI usando o método showIn. Os conteúdos da exibição (HTML) serão colocados no div com a ID "content", que foi passado ao layout do Kendo UI quando ele foi inicializado. Após inicializar o layout, você o renderiza dentro do div com a ID "app", que é um div na página de aterrissagem (Northwind.Web/Views/Home/Spa.cshtml). Analisarei isso brevemente.

O método auxiliar loadView aceita um modelo de exibição, uma exibição e, se necessário, um retorno de chamada para invocar uma vez a exibição e ocorre a associação do modelo de exibição. Dentro do método loadView, você aproveita a biblioteca FX do Kendo UI para aprimorar esteticamente a experiência do usuário adicionando alguma animação simples ao processo de alternação de exibição. Isso é feito deslizando a exibição carregada atual para a esquerda, carregando remotamente a nova exibição e, em seguida, deslizando a nova exibição carregada de volta ao centro. Obviamente, você pode alterar isso com facilidade para uma variedade de animações diferentes usando a biblioteca FX do Kendo UI. Um dos principais benefícios de usar o layout do Kendo UI é mostrado quando você invoca o método showIn para alterar as exibições. Ele garantirá que a exibição será descarregada, destruída adequadamente e removida do DOM do navegador, assegurando, assim, que o SPA pode ser dimensionado e tem desempenho.

Edite a exibição _ViewStart.cshtml (Northwind.Web/Views/­_ViewStart.cshtml) Veja como configurar todas as exibições para não usar o layout do ASP.NET MVC por padrão:

    @{
      Layout = null;
    }

Nessa fase, o SPA deve estar funcionando. Ao clicar em qualquer um dos links de navegação no menu, você verá que o conteúdo atual está sendo alternado por meio do AJAX, graças ao roteador do Kendo UI e ao RequireJS.

Essas sete etapas necessárias para converter um novo aplicativo Web ASP.NET em um SPA não são tão ruins, são?

Agora que o SPA está funcionando, seguirei adiante e farei o que a maioria dos desenvolvedores acabará fazendo com um SPA, que é adicionar alguma funcionalidade CRUD (criação, leitura, atualização e exclusão).

Adicionando funcionalidade CRUD ao SPA

Veja as principais etapas necessárias para adicionar uma exibição de grade Customer ao SPA (e os arquivos de código do projeto relacionado):

  • Adicione um controlador MVC CustomerController (Northwind.Web/Controllers/CustomerController.cs).
  • Adicione o controlador API Web REST OData Customer (Northwind.Web/Api/CustomerController.cs).
  • Adicione uma exibição de grade Customer (Northwind.Web/Views/­Customer/Index.cshtml).
  • Adicione um módulo CustomerModel (Northwind.Web/Scripts/app/models/CustomerModel).
  • Adicione um módulo customerDatasource para a grade Customer (Northwind.Web/Scripts/app/datasources/customer­Datasource.js).
  • Adicione um módulo indexViewModel para a exibição de grade Customer (Northwind.Web/Scripts/app/viewModels/­indexViewModel.js).

Configurando a estrutura da solução com o Entity Framework A Figura 4 mostra a estrutura da solução, destacando três projetos: Northwind.Data (1), Northwind.Entity (2) e Northwind.Web (3). Discutirei cada um deles brevemente, juntamente com o Entity Framework Power Tools.

  • Northwind.Data: inclui tudo relacionado à ferramenta ORM (Mapeamento Relacional de Objeto) do Entity Framework, para persistência.
  • Northwind.Entity: inclui entidades de domínio, compostas por classes POCO (Plain Old CLR Object). Esses são todos os objetos de domínio que desconhecem a persistência.
  • Northwind.Web: inclui o aplicativo Web ASP.NET MVC 5, a camada de apresentação, onde você criará o SPA com duas bibliotecas
  • mencionadas anteriormente, Kendo UI e RequireJS, e o restante da pilha do lado do servidor: Entity Framework, API Web e OData.
  • Entity Framework Power Tools: para criar todas as entidades e mapeamentos POCO (primeiro do banco de dados), usei o Entity Framework Power Tools da equipe do Entity Framework (bit.ly/1cdobhk). Depois da geração do código, tudo que fiz aqui foi simplesmente copiar as entidades em um projeto distinto (Northwind.Entity) para sanar as questões de separação.

A Best-Practice Solution Structure
Figura 4 Uma prática recomendada de estrutura de solução

Observação: tanto o script de instalação do Northwind SQL quanto um backup do banco de dados estão incluídos no código-fonte que pode ser baixado na pasta Northwind.Web/App_Data (bit.ly/1cph5qc).

Agora que a solução foi configurada para acessar o banco de dados, seguirei adiante e gravarei a classe CustomerController.cs do MVC para servir às exibições de edição e índice. Como a única responsabilidade do controlador é servir a uma exibição HTML para o SPA, o código aqui será mínimo.

Criando um controlador Customer do MVC (Northwind.Web/­Controllers/CustomerController.cs) Veja como criar o controlador Customer com as ações para as exibições de edição e índice:

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

Criando a exibição com a grade Customers (Northwind.Web/­Views/Customers/Index.cshtml) A Figura 5 mostra como criar a exibição com a grade Customers.

Se a marcação na Figura 5 não for familiar, não se desespere, é apenas a marcação MVVM (HTML) do Kendo UI. Ela simplesmente configura um elemento HTML; nesse caso, o div com uma ID de "grid". Posteriormente, quando você vincular essa exibição a um modelo de exibição com a estrutura MVVM do Kendo UI, essa marcação será convertida em widgets do Kendo UI. Você pode ler mais sobre isso em bit.ly/1d2Bgfj.

Figura 5 Marcação de exibição de grade Customer com um widget MVVM e associações 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>

Criando um controlador Customer API Web MVC (OData) (Northwind.Web/Api/CustomerController.cs) Agora mostrarei como criar o controlador Customer API Web MVC (OData). OData é um protocolo de acesso a dados para a Web que fornece uma maneira uniforme de consultar e manipular dados definidos por meio de operações CRUD. Usando a API Web ASP.NET, é fácil criar um ponto de extremidade OData. Você pode controlar quais operações OData serão expostas. Você pode hospedar vários pontos de extremidade OData ao longo dos pontos de extremidade não OData. Você tem controle total sobre seu modelo de dados, da lógica de negócios do back-end e da camada de dados. A Figura 6 mostra o código para o controlador Customer API Web OData.

O código na Figura 6 apenas cria um controlador API Web OData para expor os dados de Customer do banco de dados Northwind. Depois que ele é criado, é possível executar o projeto, e com ferramentas como o Fiddler (um depurador da Web gratuito que pode ser encontrado em fiddler2.com) ou o LINQPad, você pode consultar de fato os dados do cliente.

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

Configurando e expondo OData da tabela Customer para a grade (Northwind.Web/App_Start/WebApiConfig.cs) A Figura 7 configura e expõe o OData da tabela Customer para a grade.

Consultando a API Web OData com o LINQPad Se você não usou o LINQPad (linqpad.net) ainda, adicione essa ferramenta ao kit de ferramentas do desenvolvedor; ele é indispensável e está disponível em uma versão gratuita. A Figura 8 mostra o LINQPad com uma conexão à API Web OData (localhost:2501/odata), exibindo os resultados da consulta do LINQ, "Customer.Take (100)".

Figura 7 Configurando as rotas API Web ASP.NET MVC 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 Consultando a API Web OData do controlador Customer por meio de uma consulta do LINQPad

Criando o modelo (Observable) Customer (Northwind.Web/­Scripts/app/models/customerModel.js) Em seguida, veja a criação do modelo (Kendo UI Observable) Customer. Você pode pensar nisso como um modelo de domínio da entidade Customer do lado do cliente. Criei o modelo Customer, de modo que ele pode ser reutilizado com facilidade pela exibição de edição e exibição de grade Customer. O código é mostrado na Figura 9.

Figura 9 Criando o modelo Customer (Kendo UI Observable)

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

Criando um DataSource para a grade Customers (Northwind.Web/Scripts/app/datasources/customersDatasource.js) Se você estiver familiarizado com as fontes de dados do ASP.NET Web Forms, o conceito aqui é o mesmo, onde você cria uma fonte de dados para a grade Customers (Northwind.Web/Scripts/app/datasources/customersDatasource.js). O componente Kendo UI DataSource (bit.ly/1d0Ycvd) é uma abstração para usar dados locais (matrizes de objetos JavaScript) ou remotos (XML, JSON ou JSONP). Ele oferece suporte por completo às operações de dados CRUD e fornece suporte local e do lado do servidor para classificação, paginação, filtragem, agrupamento e agregações.

Criando o View Model para a exibição de grade Customers Se você estiver familiarizado com o MVVM do Windows Presentation Foundation (WPF) ou Silverlight, esse é exatamente o mesmo conceito, apenas no lado do cliente (encontrado neste projeto em Northwind.Web/Scripts/ViewModels/­Customer/indexViewModel.cs). O MVVM é um padrão de separação arquitetônico usado para separar a exibição e seus dados e a lógica de negócios. Você verá rapidamente que todos os dados, a lógica de negócios, etc. estão no modelo de exibição e que a exibição é puramente HTML (apresentação). A Figura 10 mostra o código para a exibição de grade Customer.

Figura 10 O modelo de exibição de grade 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;
  });

Descreverei brevemente vários componentes do código na Figura 10:

  • onClick (auxiliar): esse método é uma função auxiliar, que obtém uma instância da grade Customer, a linha selecionada atual e um modelo JSON da representação do Customer da linha selecionada.
  • save: salva as alterações ao fazer uma edição embutida de um Customer.
  • cancel: cancela o modo de edição embutida.
  • details: navega pelo SP para editar a exibição Customer, acrescentando a ID de Customer à URL.
  • edit: ativa a edição embutida para o Customer atual selecionado.
  • destroy: exclui o Customer atual selecionado.
  • onChange (evento): dispara toda vez que um Customer é selecionado. Você armazena o último Customer selecionado para que possa manter o estado. Após executar qualquer atualização ou sair da grade Customer, ao navegar de volta à grade, você seleciona novamente o último Customer selecionado.

Agora adicione os módulos customerModel, indexViewModel e customersDatasource à sua configuração de RequireJS (Northwind.Web/Scripts/app/main.js). O código é mostrado na Figura 11.

Figura 11 Adições da configuração do 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'
}

Adicione uma rota para a nova exibição de grade Customers Observe que no retorno de chamada de loadView (em Northwind.Web/Scripts/app/router.js), você está associando a barra de ferramentas da grade depois que ela foi inicializada e depois que associação do MVVM ocorreu. Isso porque na primeira vez em que você associou sua grade, a barra de ferramentas não tinha sido inicializada, pois ela está na grade. Quando a grade é inicializada pela primeira vez via MVVM, ela será carregada na barra de ferramentas do modelo do Kendo UI. Quando ela é carregada na grade, você associa apenas a barra de ferramentas à exibição para que os botões na barra de ferramentas sejam associados aos métodos save e cancel no seu modelo de exibição. Veja o código relevante para registrar a definição da rota para a exibição de edição 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);
      });
    });
});

Agora você tem uma exibição de grade Customers funcional. Carregue localhost:25061/Home/Spa#/customer/index (o número da porta provavelmente variará de acordo com seu computador) em um navegador e você verá a Figura 12.

The Customer Grid View with MVVM Using the Index View Model
Figura 12 A exibição de grade Customer com MVVM usando o modelo de exibição de índice

Conectando a exibição de edição Customers Veja as principais etapas para adicionar uma exibição de edição Customer ao SPA:

  • Crie uma exibição de edição Customer associada ao seu modelo Customer via MVVM (Northwind.Web/Views/Customer/Edit.cshtml).
  • Adicione um módulo do modelo para a exibição de edição Customer (Northwind.Web/Scripts/app/viewModels/­editViewModel.js).
  • Adicione um módulo auxiliar utilitário para obter IDs da URL (Northwind.Web/Scripts/app/util.js).

Como você está usando a estrutura Kendo UI, vá em frente e molde sua exibição de edição com os estilos do Kendo UI. Você pode saber mais sobre isso em bit.ly/1f3zWuC. A Figura 13 mostra a marcação de exibição de edição com um widget MVVM e associação de evento.

Figura 13 Marcação da exibição de edição com um widget MVVM e associação 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>

Crie um utilitário para obter da URL a ID do Customer Como você está criando módulos concisos com limites limpos para criar uma boa separação de preocupações, demonstrarei como criar um módulo Util onde todos os auxiliares utility residirão. Começarei com um método utility que pode recuperar a ID do cliente na URL para Customer DataSource (Northwind.Web/Scripts/app/datasources/customerDatasource.js), conforme mostrado na Figura 14.

Figura 14 O 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;
  });

Adicione o modelo de exibição de edição e módulos Util à configuração do RequireJS (Northwind.Web/Scripts/app/main.js) O código na Figura 15 mostra as adições de configuração do RequireJS para os módulos de edição Customer.

Figura 15 Adições da configuração do RequireJS para os módulos de edição 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();
});

Adicione o modelo de exibição de edição Customer (Northwind.Web/Scripts/app/viewModels/editViewModel.js) O código na Figura 16 mostra como adicionar um modelo de exibição de edição Customer.

Figura 16 Módulo do modelo de exibição de edição Customer para a exibição 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;
  });

Descreverei brevemente vários componentes do código na Figura 16:

  • saveCustomer: esse método é responsável por salvar todas as alterações em Customer. Ele também redefine o filtro de DataSource, assim a grade será hidratada com todos os Customers.
  • cancel: esse método navegará pelo SPA de volta à exibição de grade Customer. Ele também redefine o filtro de DataSource para que a grade seja hidratada com todos os Customers.
  • filter: invoca o método de filtro de DataSource e consulta um Customer específico pela ID que está na URL.
  • fetch: invoca o método de busca do DataSource após configurar o filtro. No retorno de chamada da busca, você define a propriedade Customer do modelo de exibição com o Customer que foi retornado da busca de DataSource, que será usado para associação à sua exibição de edição Customer.

Quando RequireJS carrega um módulo, o código no corpo do método "define" será invocado apenas uma vez, que é quando RequireJS carrega o módulo, de modo que você expõe um método (loadData) no seu modelo de exibição de edição para ter um mecanismo para carregar dados depois que o módulo do modelo de exibição de edição já tiver sido carregado (veja isso em Northwind.Web/­Scripts/app/router.js).

Adicione uma rota para a nova exibição de edição Customer (Northwind.Web/Scripts/app/router.js) Veja o código relevante para adicionar o roteador:

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

Observe que quando o modelo de exibição de edição Customer é solicitado de RequireJS, você pode recuperar o Customer invocando o método loadData do modelo de exibição. Desse modo você pode carregar os dados de Customer com base na ID que está na URL cada e toda vez que a exibição de edição Customer for carregada. Uma rota não precisa ser apenas uma cadeia de caracteres inserida em código. Ela também pode conter parâmetros, como um roteador de servidor back-end (Ruby on Rails, ASP.NET MVC, Django, etc.). Para isso, você nomeia um segmento da rota com dois pontos antes do nome da variável que deseja.

Agora você pode carregar a exibição de edição Customer no navegador (localhost:25061/Home/Spa#/customer/edit/ANATR) e ver a tela representada na Figura 17.

The Customer Edit View
Figura 17 A exibição de edição Customer

Observação: embora a funcionalidade de exclusão (destruição) na exibição de grade Customer tenha sido conectada, ao clicar no botão "Delete" na barra de ferramentas (veja a Figura 18), você verá uma exceção, conforme mostrado na Figura 19.

The Customer Grid View
Figura 18 A exibição de grade Customer

Expected Exception When Deleting a Customer Due to CustomerID Foreign Key Referential Integrity
Figura 19 Exceção esperada ao excluir um Customer devido à integridade referencial de chave estrangeira CustomerID

Essa exceção é intencional, pois a maioria das IDs de Customer são chaves estrangeiras em outras tabelas, por exemplo, Orders, Invoices, etc. Você teria que conectar uma exclusão em cascata que excluísse todos os registros de todas as tabelas onde ID de Customer fosse uma chave estrangeira. Embora você não possa excluir nada, ainda quero mostrar as etapas e o código para a funcionalidade de exclusão.

Então é isso. Demonstrei o quanto pode ser rápido e fácil converter um aplicativo Web ASP.NET pronto para uso em um SPA usando o RequireJS e Kendo UI. Em seguida, mostrei como é fácil adicionar funcionalidade semelhante à CRUD ao SPA.

Você pode ver uma demonstração ao vivo do projeto em bit.ly/1bkMAlK e pode ver o site do projeto CodePlex (e o código que pode ser baixado) em easyspa.codeplex.com.

Boa codificação.

Long Le é o arquiteto chefe de aplicativo/desenvolvimento do .NET na CBRE Inc. e um MVP da Telerik/Kendo UI. Ele passa a maior parte do seu tempo desenvolvendo estruturas e blocos de aplicativos, fornecendo orientação para práticas recomendadas e padrões, bem como padronizando a pilha tecnológica corporativa. Ele trabalha com as tecnologias da Microsoft há mais de 10 anos. No seu tempo livre, ele aproveita para escrever no seu blog (blog.longle.net) e jogar Call of Duty. Você pode entrar em contato com ele e segui-lo no Twitter, em twitter.com/LeLong37.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Derick Bailey (Telerik) e Mike Wasson (Microsoft)