Compartir a través de


Puntos de datos

Enlaces de datos OData en aplicaciones web con Knockout.js

Julie Lerman

Descargar el ejemplo de código

Julie LermanComo “geek” de datos, paso bastante tiempo escribiendo código back-end y por lo tanto me pierdo de gran parte de la diversión que ocurre del lado de cliente. John Papa, quien solía escribir esta columna, ahora escribe una sobre tecnologías para clientes en esta revista y ha trabajado bastante en una nueva y excelente tecnología del lado del cliente llamada Knockout.js. Gracias a la pasión que Papa y otros han expresado sobre Knockout.js, aproveché la oportunidad para asistir una presentación sobre Knockout en mi grupo de usuario local, VTdotNET, que realizó Jon Hoguet de MyWebGrocer.com. La reunión convocó a más personas que lo habitual e incluyó desarrolladores externos a la comunidad de .NET. A medida que Hoguet avanzaba en la presentación, comprendí claramente por qué los desarrolladores web se sienten atraídos por Knockout: Simplifica el enlace de datos del lado del cliente en las aplicaciones web al aprovechar el patrón Model-View-ViewModel (MVVM). Enlace de datos... ¡eso sí es algo que sé hacer con los datos! Al día siguiente ya estaba aprendiendo cómo usar Knockout.js con acceso a datos y ahora puedo compartir mis descubrimientos.

Debo advertir que mis habilidades de JavaScript son bastante limitadas, por lo que esto me tomó un poco más de tiempo de lo que debería. Sin embargo, supongo que varios de los lectores columna se encuentran en la misma situación, por lo que incluso pueden llegar a agradecer mis instrucciones paso a paso. Puede acceder a un conocimiento mucho más profundo de Knockout en las columnas de Papa, además de su excelente curso en Pluralsight.com.

Mi objetivo era averiguar cómo podía usar Knockout.js para enlazar y luego actualizar datos recopilados de un servicio de datos WCF. Lo que haré en este artículo es mostrar las piezas críticas del engranaje. Luego verá cómo funcionan en conjunto en la muestra de descarga adjunta.

Comencé con un servicio de datos WCF existente. De hecho, es el mismo servicio que usé en el artículo de Puntos de datos de diciembre de 2011, “Administrar validaciones de marco de entidad en servicios de datos WCF” (msdn.microsoft.com/magazine/hh580732), la que actualicé con la reciente aparición de servicios de datos WCF 5.

Como recordatorio, mi modelo de software de demostración está compuesto de una sola clase:

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

La clase DbContext exponía la clase Person en un DbSet:

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

Luego el servicio de datos exponía dicho DbSet para lectura y escritura:

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

Con Knockout.js, los script del lado del cliente pueden responder a los cambios en los valores de las propiedades de los objetos enlazados a datos. Por ejemplo, si en el script llamamos el método Knockout applyBindings, al pasar un objeto, Knockout alertará todos los elementos enlazados a datos sobre las actualizaciones en las propiedades del objeto. Podemos lograr un comportamiento semejante para las colecciones que supervisa Knockout a medida que estas adquieren o rechazan elementos. Todo esto sucede en el cliente sin la necesidad de escribir ningún script de control de eventos y sin tener que buscar ayuda en el servidor.

Tuve que realizar varias tareas para lograr mi objetivo:

  • Crear modelos de visualización que tengan en cuenta Knockout.
  • Recibir los OData en formato JSON.
  • Mover los resultados de OData a mi objeto de modelo de vista.
  • Enlazar los datos con Knockout.js.
  • Para las actualizaciones, mover los valores del modelo de vista nuevamente al objeto de resultados de OData.

Creación de modelos de vista que tengan en cuenta Knockout

Para hacer este trabajo, Knockout debe ser capaz de “observar” las propiedades del objeto. Podemos habilitar con Knockout cuando definimos las propiedades del objeto. ¡Pero espere! No digo que “ensuciemos” los objetos de dominio con la de la interfaz de usuario para que Knockout pueda observar los cambios de los valores. Aquí es donde entra en juego el patrón MVVM. MVVM nos permite crear para una interfaz de usuario puntual (o vista) del modelo, es decir, la parte VM (ViewModel) de MVVM. Esto significa que podemos extraer los datos hacia la aplicación en la forma que queramos (con consultas a los servicios de datos WCF, a través de un servicio o incluso por medio de la nueva API de web) y luego reconfigurar los resultados para que se acomoden a la vista. Por ejemplo, mi servicio de datos devuelve tipos Person con las propiedades FirstName, LastName e IdentityCard. Pero en la vista, solo me interesa FirstName y LastName. Incluso podemos aplicar la lógica que pertenece a la vista en la versión de modelo de vista del objeto. Esto nos brinda lo mejor de ambos mundos: obtenemos un objeto que está dedicado específicamente a esa vista, independientemente del servicio de datos del que proviene.

Este es un PersonViewModel del lado cliente que definí en un objeto JavaScript:

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

Sin importar qué es lo que regresa del servicio, solo deseo usar el nombre y el apellido en mi vista, y por lo tanto esas son las únicas propiedades que contiene. Observe que los nombres no están definidos como cadenas, sino que como objetos que Knockout puede observar. Es importante tener esto presente al configurar los valores, tal como veremos más adelante.

Recepción de OData en formato JSON

La consulta OData que usaré simplemente devolverá el primer objeto Person del servicio de datos. Actualmente, eso viene de mi servidor de desarrollo:

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

De manera predeterminada, los resultados OData se devuelven como ATOM (que se expresa con el uso de XML). Pero Knockout.js trabaja con datos en JSON, que OData también puede proporcionar. Por lo tanto (y debido a que estoy trabajando directamente en JavaScript) resulta mucho más fácil trabajar con los resultados en JSON que con XML. En una solicitud JavaScript, podemos anexar un parámetro a la consulta OData para especificar que los resultados se devuelvan como JSON: “$format=json”. Pero esto requiere que el servicio de datos específico sepa cómo procesar la opción de consulta de formato. El mío no sabe cómo. Si elijo esta opción (por ejemplo, si uso AJAX para realizar mis llamadas de OData), tengo que usar una extensión en mi servicio para permitir salidas en JSON (consulte bit.ly/mtzpN4 para obtener más información).

Sin embargo, como estoy usando el kit de herramientas datajs para OData (datajs.codeplex.com), no me tengo que preocupar por esto. En forma predeterminada este kit de herramientas agrega automáticamente información de encabezado a las solicitudes, para devolver resultados JSON. Por lo tanto, no tengo que agregar la extensión JSONP a mi servicio de datos. El objeto OData del kit de herramientas datajs cuenta con el método read que nos permite ejecutar una consulta, cuyos resultados estarán en formato JSON:

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

Insertar OData en PersonViewModel

Cuando se devuelven los resultados (en mi caso, un solo tipo Person, tal como se definió en mi modelo de dominio), deseo crear una instancia PersonViewModel a partir del resultado. Mi método JavaScript, personToViewModel, toma un objeto Person, crea un nuevo PersonViewModel a partir de los valores y luego devuelve el PersonViewModel:

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

Observe que para configurar los valores paso los valores nuevos como si las propiedades fueran métodos. Originalmente, configuraba los valores con el uso de vm.FirstName=person.FirstName. Pero eso convertía FirstName en una cadena, en vez de un elemento observable. Me costó descubrir por qué Knockout no veía los cambios subsiguientes del valor, hasta que finalmente tuve que pedir ayuda. Las propiedades son funciones, no cadenas, por lo que debemos configurarlas mediante la sintaxis de métodos.

Quiero ejecutar personToViewModel como respuesta a la consulta. Esto es posible porque OData.read permite especificar qué método de devolución de llamada se debe usar cuando la consulta se ejecuta satisfactoriamente. En este caso, pasaré los resultados a un método llamado mapResultsToViewModel, el cual llama personToViewModel (consultar la Ilustración 1). En el resto de la solución, definí previamente la variable peopleFeed como “http://localhost:43447/DataService.svc/People”.

Ilustración 1 Ejecución de una consulta y control de la respuesta

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

Enlace con controles HTML

Fíjese en el código del método mapResultsToViewModel: ko.applyBindings(vm). Este es otro aspecto clave de cómo funciona Knockout. ¿A qué le estoy aplicando los enlaces? Eso es lo que definiré en el marcado. En el código de marcado, uso el atributo data-bind de Knockout para enlazar los valores de mi PersonViewModel con algunos de los 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>

Si solo quisiera mostrar los datos, podría usar elementos label y en vez de enlazar los datos al valor, podría enlazarlos a texto. Por ejemplo:

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

Pero como deseo editar, no solo usaré un elemento de entrada. Mi enlace de datos de Knockout también especifica que estoy enlazando al valor de estas propiedades.

Los ingredientes clave que Knockout le proporciona a mi solución son las propiedades observables en mi modelo de visualización, el atributo data-bind para mis elementos de marcado y el método applyBindings que agrega la lógica en tiempo de ejecución necesaria para que Knockout notifique a esos elementos cuando cambia el valor de una propiedad.

Si ejecuto lo que tengo hasta ahora en la aplicación, puedo ver la persona devuelta por la consulta en modo de depuración, tal como podemos apreciar en la Ilustración 2.

Person Data from the OData Service
Ilustración 2 Datos de la persona del servicio OData

En la Ilustración 3 podemos observar los valores de la propiedad PersonViewModel que aparecen en la pantalla.

The PersonViewModel Object Bound to Input Controls
Ilustración 3 El objeto PersonViewModel enlazado a los controles de entrada

Guardado en la base de datos

Gracias a Knockout, a la hora de guardar no tengo que extraer los valores de los elementos de entrada. Knockout ya actualizó el objeto PersonViewModel que estaba enlazado al formulario. En mi método de guardado, envío lo valores de PersonViewModel de vuelta al objeto Person (que llegó del servicio) y luego guardo esos cambios nuevamente en la base de datos a través de mi servicio. En el código de descarga notará que conservé la instancia Person que se devolvió originalmente de la consulta OData y que uso el mismo objeto aquí. Una vez que actualizo Person con el método viewModeltoPerson, puedo pasarlo a OData.request como parte de un objeto de solicitud, tal como vemos en la Ilustración 4. El objeto de solicitud es el primer parámetro y consta del URI, el método y los datos. Consulte la documentación de datajs en bit.ly/FPTkZ5 para obtener más información sobre el método de solicitud. Observe que aquí aprovecho el hecho de que la instancia Person almacenada en el URI está enlazada a la propiedad __metadata.uri. Con esa propiedad, no tengo que codificar el URI, que es “http://localhost:43447/DataService.svc/People(1)”.

Ilustración 4 Guardar cambios en la base de datos

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

Ahora, cuando modifico los datos (al cambiar Julia a Julie, por ejemplo) y presiono el botón Guardar, no solo recibo una alerta de “Guardado” que indica que no ocurrieron errores, sino que puedo ver la actualización de la base de datos en el generador de perfiles:

    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 y el servicio de datos WCF para las masas

Explorar Knockout.js me llevó a aprender algunas herramientas nuevas que pueden usar los desarrolladores de cualquier tipo, no solo en la plataforma .NET. Y si bien me obligó a practicar algunas habilidades JavaScript en desuso, el código que escribí se enfocó en la familiar tarea de la manipulación de objetos y no en la interacción pesada y monótona con los controles. También me llevó por el camino del bien arquitectónico, con el uso de MVVM para diferenciar mis objetos de modelo de los que deseo presentar en la interfaz de usuario. Realmente existen muchas otras cosas que se puede hacer con Knockout.js, especialmente para crear interfaces de usuario web que responden bien. También puede usar herramientas excelentes tales como la API web de WCF (bit.ly/kthOgY) para crear la fuente de datos. Espero con ansias poder aprender más de los profesionales y descubrir otras excusas para trabajar en el lado cliente.

Julie Lerman es una MVP de Microsoft, mentora y consultora de .NET que vive en las colinas de Vermont. Puede encontrarla haciendo presentaciones sobre acceso de datos y otros temas de Microsoft .NET en grupos y congresos de usuarios alrededor del mundo. Mantiene un blog en thedatafarm.com/blog y es la autora de “Programming Entity Framework” (O’Reilly Media, 2010) y “Programming Entity Framework: Code First” (O’Reilly Media, 2011). Puede seguirla a través de Twitter en twitter.com/julielerman.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: John Papa y Alejandro Trigo