Pontos de dados

Vínculo de dados OData em aplicativos da Web com Knockout.js

Julie Lerman

Baixar o código de exemplo

Julie LermanComo geek em dados passo muito tempo escrevendo código de back-end e perco muitas das coisas divertidas no lado do cliente. John Papa, que escrevia essa coluna, está agora escrevendo uma coluna sobre tecnologias de cliente para essa revista. Ele está trabalhando arduamente em uma nova e importante tecnologia do lado do cliente chamada Knockout.js. Graças à paixão com que Papa e outros se expressam sobre Knockout.js, eu aproveitei uma oferta de uma apresentação sobre o Knockout no meu grupo de usuários local, VTdotNET, por Jon Hoguet da MyWebGrocer.com. A reunião arrastou um público muito maior do que o normal e incluiu desenvolvedores de fora da comunidade .NET. Enquanto Hoguet fazia sua apresentação, tornou-se óbvio para mim por que tantos desenvolvedores da Web estão atraídos pelo Knockout: ela simplifica a vinculação de dados do lado do cliente em aplicativos Web aproveitando o padrão Model-View-ViewModel (MVVM). Vinculação de dados … Agora, isso é algo que posso fazer com dados! No dia seguinte eu já estava aprendendo como usar Knockout.js com o acesso a dados, e agora posso compartilhar minhas descobertas com vocês.

Devo prevenir que meus conhecimentos de JavaScript estão bastante em falta, portanto demorou um pouco mais para mim do que deveria. No entanto, imagino que muitos de vocês que estão lendo essa coluna estão no mesmo barco, portanto devem apreciar meus primeiros passos. Vocês podem obter uma entendimento muito mais profundo do Knockout nas colunas do Papa, além de seu excelente curso em Pluralsight.com.

Meu objetivo era ver como eu poderia usar Knockout.js para vincular e depois atualizar dados recuperados de um WCF Data Service. O que farei nesse artigo será mostrar as partes funcionais críticas. Então poderão ver como elas se encaixam no exemplo de download fornecido.

Comecei com um WCF Data Service existente. De fato, é o mesmo serviço que usei em minha coluna Pontos de dados de dezembro de 2011, “Lidando com validações do Entity Framework no WCF Data Services” (msdn.microsoft.com/magazine/hh580732), que atualizei agora para o WCF Data Services 5, lançado recentemente.

Como lembrete, meu modelo de demonstração consistia em uma única classe:

public class Person
{
  public int PersonId { get; set; }
  [MaxLength(10)]
  public string IdentityCardNumber { get; set; }
  public string FirstName { get; set; }
  [Required]
  public string LastName { get; set; }
}

Uma classe DbContext apresentava a classe Person em um DbSet:

public class PersonModelContext : DbContext
{
  public DbSet<Person> People { get; set; }
}

O Data Service então apresentava o DbSet para leitura e gravação:

public class DataService : DataService<PersonModelContext>
{
  public static void InitializeService(DataServiceConfiguration config)
  {
    config.SetEntitySetAccessRule("People", EntitySetRights.All);
  }
}

Com Knockout.js, um script do lado do cliente pode responder a alterações nos valores da propriedade de objetos vinculados a dados. Por exemplo, se o método applyBindings do Knockout for chamado em seu script, passando um objeto, o Knockout alertará qualquer elemento vinculado a dados das atualizações nas propriedades do objeto. Um comportamento similar pode ser obtido com coleções que o Knockout está observando à medida que elas adquirem ou ejetam itens. Tudo isso acontece no cliente sem a necessidade de escrever qualquer script de manipulação de eventos ou retornar ao servidor para ajuda.

Há uma série de tarefas que tive de executar para alcançar meu objetivo:

  • Criar modelos de exibição habilitados para o Knockout.
  • Obter OData no formato JSON.
  • Mover os resultados OData para meu objeto do modelo de exibição.
  • Vincular os dados a Knockout.js.
  • Para atualizações, mover os valores do modelo de exibição de volta para o objeto de resultado OData.

Criando modelos de exibição habilitados para o Knockout

Para que isso funcione, o Knockout precisa ser capaz de “observar” as propriedades desse objeto. Isso pode ser habilitado usando o Knockout ao definir as propriedades em seu objeto. Mas espere! Não estou sugerindo que você “polua” seus objetos de domínio com lógica específica de interface do usuário para que o Knockout possa observar alterações de valores. É aqui que o padrão MVVM entra. O MVVM permite criar uma versão específica da interface do usuário (ou exibição) de seu modelo. É o VM (ViewModel) do MVVM. Isso significa que você pode extrair seus dados para seu aplicativo da maneira que quiser (consultando o WCF Data Services, acessando um serviço ou mesmo via a nova API da Web) e então reformatar os resultados para que fiquem alinhados à sua exibição. Por exemplo, meu serviço de dados retorna tipos Person com um FirstName, LastName e IdentityCard. Mas em minha exibição, estou interessando apenas em FirstName e LastName. É possível ainda aplicar lógica específica de exibição à versão do modelo de exibição de seu objeto. Isso lhe dá o melhor dos dois mundos: você obtém um objeto especificamente destinado à sua exibição, independentemente do que a fonte de dados fornece.

Aqui está o PersonViewModel do lado do cliente que defini em um objeto JavaScript:

function PersonViewModel(model) {
  model = model || {};
  var self = this;
  self.FirstName = ko.observable(model.FirstName || ' ');
  self.LastName = ko.observable(model.LastName || ' ');
}

Não importa o que retornou do serviço, quero apenas usar o nome e sobrenome em minha exibição, portanto, são as únicas propriedades que ele contém. Observe que os nomes não são definidos como cadeia de caracteres, mas como objetos observable do Knockout. Isso é importante lembrar ao definir valores, como será visto adiante.

Obtendo OData no formato JSON

A consulta OData que usarei retornará apenas a primeira Person do serviço de dados. No momento, está vindo do meu servidor de desenvolvimento:

http://localhost:43447/DataService.svc/People?$top=1

Por padrão, os resultados OData são retornados como ATOM (expressos com o uso de XML). No entanto, Knockout.js funciona com dados JSON, que o OData também pode fornecer. Portanto, como estou trabalhando diretamente em JavaScript, é muito mais simples lidar com resultados JSON do que com XML. Em uma solicitação JavaScript é possível acrescentar um parâmetro à consulta OData para especificar que os resultados sejam retornados como JSON: “$format=json.” Mas isso requer que seu serviço de dados específico saiba como processar a opção de consulta do formato. O meu não sabe. Se eu quiser seguir nesse caminho, por exemplo, se estiver usando AJAX para fazer minhas chamadas OData, terei de usar uma extensão em meu serviço para dar suporte à saída JSON (consulte bit.ly/mtzpN4 para obter mais informações).

No entanto, como estou usando o kit de ferramentas datajs para OData (datajs.codeplex.com), não preciso me preocupar com isso. O comportamento padrão do kit de ferramentas é adicionar automaticamente informações de cabeçalho às solicitações para que retornem resultados JSON. Portanto, não preciso adicionar a extensão JSONP ao meu serviço de dados. O objeto OData do kit de ferramentas datajs tem um método Read que permite executar uma consulta cujos resultados estarão em formato JSON:

OData.read({
  requestUri: http://localhost:43447/DataService.svc/People?$top=1"
  })

Enviando OData para PersonViewModel

Uma vez que os resultados foram retornados, em meu caso um tipo Person único como definido pelo meu modelo de domínio, quero então criar uma instância PersonViewModel a partir do resultado. Meu método JavaScript, personToViewModel, pega um objeto Person, cria um novo PersonViewModel de seus valores e, em seguida, retorna o PersonViewModel:

function personToViewModel(person) {
  var vm=new PersonViewModel;
  vm.FirstName(person.FirstName);
  vm.LastName(person.LastName);
  return vm;
}

Observe que estou definindo os valores passando os novos valores como se as propriedades fossem métodos. Originalmente, defino os valores usando vm.FirstName=person.FirstName. Mas isso transformou FirstName em uma cadeia de caracteres, em vez de um observable. Lutei por instantes, tentando ver por que o Knockout não estava notando as alterações subsequentes no valor e, finalmente, tive de ceder e pedir ajuda. As propriedades são funções, não cadeias de caracteres, portanto, é preciso defini-las usando sintaxe de método.

Quero executar personToViewModel em resposta à consulta. Isso é possível, pois OData.read permite informar o método de retorno de chamada a ser usado quando a consulta foi executada com êxito. Nesse caso, passarei os resultados para um método chamado mapResultsToViewModel, o qual, por sua vez, chama personToViewModel (consulte a Figura 1). Em outro local da solução, predefini a variável peopleFeed como “http://localhost:43447/DataService.svc/People”.

Figura 1 Executando uma consulta e manipulando a resposta

OData.read({
  requestUri: peopleFeed + "?$top=1"
  },
  function (data, response) {
    mapResultsToViewModel(data.results);
  },
  function (err) {
    alert("Error occurred " + err.message);
  });
  function mapResultsToViewModel(results) {
    person = results[0];
    vm = personToViewModel(person)
    ko.applyBindings(vm);
}

Vinculando aos controles HTML

Observe o código no método mapResultsToViewModel: ko.applyBindings(vm). Esse é outro aspecto importante de como o Knockout funciona. Mas a que estou aplicando vínculos? Isso é o que definirei em minha marcação. Em meu código de marcação, uso o atributo data-bind do Knockout para vincular os valores do meu PersonViewModel a alguns elementos input:

<body>
  <input data-bind="value: FirstName"></input>
  <input data-bind="value: LastName"></input>
  <input id="save" type="button" value="Save" onclick="save();"></input>
</body>

Se eu quisesse apenas exibir os dados, poderia usar elementos label e, em vez de vincular dados ao valor, poderia vincular ao texto. Por exemplo:

<label data-bind="text: FirstName"></label>

Mas eu quero editar, portanto, não estou apenas usando um elemento input. Minha vinculação de dados do Knockout também especifica que estou vinculando ao valor dessas propriedades.

Os principais ingredientes que o Knockout está fornecendo à minha solução são as propriedades observable em meu modelo de exibição, o atributo data-bind para meus elementos de marcação e o método applyBindings que adiciona a lógica em tempo de execução necessária para que o Knockout notifique esses elementos quando um valor de propriedade for alterado.

Se eu executar o que tenho até o momento no aplicativo, poderei ver a pessoa retornada pela consulta em modo de depuração, conforme mostrado na Figura 2.

Person Data from the OData Service
Figura 2 Dados da pessoa do serviço OData

A Figura 3 mostra os valores da propriedade PersonViewModel exibidos na página.

The PersonViewModel Object Bound to Input Controls
Figura 3 O objeto PersonViewModel vinculado aos controles de entrada

Salvando no banco de dados

Graças ao Knockout, quando for a hora de salvar não terei de extrair os valores dos elementos input. O Knockout já atualizou o objeto PersonViewModel que foi vinculado ao formulário. No meu método save enviarei os valores de PersonViewModel para o objeto Person (que veio do serviço) e, em seguida, salvarei essas alterações no banco de dados por meio do meu serviço. Você verá no download do código que mantive a instância person que foi retornada originalmente da consulta OData e que estou usando o mesmo objeto aqui. Tendo atualizado Person com o método viewModeltoPerson, posso então passá-la para Odata.request como parte de um objeto de solicitação, conforme mostrado na Figura 4. O objeto de solicitação é o primeiro parâmetro e consiste no URI, no método e nos dados. Dê uma olhada na documentação do datajs em bit.ly/FPTkZ5 para saber mais sobre o método de solicitação. Observe que estou aproveitando o fato de que a instância person armazenou o URI ao qual está vinculada na propriedade __metadata.uri. Com essa propriedade, não terei de codificar o URI, que é “http://localhost:43447/DataService.svc/People(1)”.

Figura 4 Salvando alterações no banco de dados

function save() {
  viewModeltoPerson(vm, person);
  OData.request(
    {
      requestUri: person.__metadata.uri,
      method: "PUT",
      data: person
    },
    success,
    saveError
    );
  }
  function success(data, response) {
    alert("Saved");
  }
  function saveError(error) {
    alert("Error occurred " + error.message);
  }
}
function viewModeltoPerson(vm,person) {
  person.FirstName = vm.FirstName();
  person.LastName = vm.LastName();
}

Agora, quando modifico os dados, alterando Julia para Julie, por exemplo, e pressiono o botão Salvar, não apenas obtenho um alerta de “Salvo” para indicar que nenhum erro ocorreu, mas posso ver a atualização do banco de dados em meu criador de perfil.

    exec sp_executesql N'update [dbo].[People]
    set [FirstName] = @0
    where ([PersonId] = @1)
    ',N'@0 nvarchar(max) ,@1 int',@0=N'Julie',@1=1

Knockout.js e WCF Data Services para as massas

Explorar Knockout.js me forçou a aprender algumas novas ferramentas que podem ser usadas pelos desenvolvedores de qualquer faixa, não apenas na plataforma .NET. E embora tenha me forçado a exercitar algumas habilidades em JavaScript que estavam enferrujadas, o código que escrevi se concentrou na tarefa familiar da manipulação de objeto e não no trabalho enfadonho de interagir com os controles. Também me levou por um caminho de excelência em arquitetura, usando a abordagem MVVM para diferenciar meus objetos de modelo dos que eu quero apresentar em minha interface do usuário. Com certeza, há muito mais no que você pode fazer com Knockout.js, especialmente na criação de interfaces do usuário da Web responsivas. Também é possível usar excelentes ferramentas como a WCF Web API (bit.ly/kthOgY) para criar sua fonte de dados. Espero aprender mais com os profissionais e encontrar outras desculpas para trabalhar no lado do cliente.

Julie Lerman é uma Microsoft MVP, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrá-la fazendo apresentações sobre acesso a dados e outros tópicos do Microsoft .NET em grupos de usuários e conferências em todo o mundo. Seu blog está em thedatafarm.com/blog e ela é autora dos livros “Programming Entity Framework” (O’Reilly Media, 2010) e “Programming Entity Framework: Code First” (O’Reilly Media, 2011). Siga-a no Twitter, em twitter.com/julielerman.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: John Papa e Alejandro Trigo