Hola a todos! El día de hoy quiero mostrarles como pueden mejorar la manera en que desarrollan sus proyectos (cuando solo usan Javascript y jQuery como lo era yo) empleando una base limpia que es Knockout.js que nos permite generar interfaces de usuario robustas sin perdernos en una telaraña de controladores de eventos y actualizaciones manuales del DOM (Esto no quiere decir que vamos a dejar de utilizar nuestro querido jQuery ya que estas librerías tienen propósitos totalmente distintos).
Diapositiva de la Charla
| Ejemplos de código empleados en la Charla
|
Mediante Knockout nosotros obtenemos una serie de ventajas importantes tales como:
Como podemos observar en la documentación, Knockout emplea observadores para mantener sincronizada la interfaz de usuario con el modelo de datos que nosotros configuremos, junto con un conjunto de enlaces declarativos para permitir un desarrollo mucho más productivo los cuales vamos a entender más adelante en este artículo.
Configurando nuestro Proyecto ASP.NET MVC
Inicialmente vamos a dejar todo listo en nuestro proyecto referente al lado del Servidor y ver como ASP.NET MVC va a comunicarse (transferir los datos) con Knockout.
Procedemos a instalar Knockout desde el Manage NuGet Packages.
Y registramos nuestro archivo Javascript en nuestra Clase BundleConfig (ubicada dentro de la carpeta App_Start)
BundleConfig.cs
- // For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725
- publicstaticvoid RegisterBundles(BundleCollection bundles)
- {
- //...Aqui configuramos nuestras tecnicas para agrupar y minificar los archivos
- //para reducir el numero de Peticiones al Servidor desde el Navegador y reducir el codigo de los archivos
- //permitiendo mejorar el tiempo de carga de nuestras paginas Web
- //Esto no tiene que nada que ver con la seguridad porque el javascript minificado se puede visualizar tal cual fue escrito http://jsbeautifier.org/
-
- bundles.Add(newScriptBundle("~/bundles/knockout").Include(
- "~/Scripts/knockout-{version}.js"));
-
- //...Otros archivos JS y CSS
- }
Como se explica en el siguiente artículo http://sebys.com.ar/2013/08/21/asp-net-mvc-y-knockout-viewmodel/, existen 2 opciones de pasar los datos a la Vista al momento de inicializar el modelo de Knockout, mediante peticiones AJAX (Generalmente un GET) y cuando se renderiza la Vista por el Controlador (Se carga la página Web en nuestro navegador) , la primera principalmente nos es útil cuando los datos cambian totalmente dependiendo de una acción del usuario (Por ejemplo cuando se selecciona un departamento y se deben de cargar las ciudades respectivas), de lo contrario la segunda opción siempre será la más optima porque los datos están siendo obtenidos en la misma petición de la Página Web, reduciendo por tanto el número de peticiones del navegador hacia nuestro Servidor (Influye directamente en el tiempo de carga de la página web) lo cual es un factor totalmente importante con respecto a la Experiencia de Usuario.
Para pasar los datos de nuestros Modelos a nuestras Vistas vamos a emplear el Método de Extensión propuesto en el artículo anterior, para esto procedemos a dar Click derecho en nuestro Proyecto y añadimos la Carpeta App_Code.
Esta Carpeta nos permite la creación de Clases personalizadas, en donde el código se compilará automáticamente en tiempo de ejecución. Por lo tanto nuestra Clase se verá de la siguiente manera:
JsonExtension.cs
- using System.IO;
- using Newtonsoft.Json;
-
- namespace Knockout.Mvc.Extensions
- {
- public static class JsonExtension
- {
- public static string ToJson(this object obj)
- {
- JsonSerializer js = JsonSerializer.Create(new JsonSerializerSettings());
- var jw = new StringWriter();
- js.Serialize(jw, obj);
- return jw.ToString();
- }
- }
- }
Este Método de Extensión nos permitirá convertir nuestras Entidades a un objeto JSON el cual será manipulado posteriormente por Knockout.
- Configurando el Modelo para ser Serializado
Al Serializar una Entidad a un objeto JSON con el Método anterior debemos de tener cuidado de no incluir las relaciones que tenga esta con Colecciones de otras Entidades aplicando el atributo JsonIgnore, como por ejemplo:
UserList.cs
- using Newtonsoft.Json;
- using System;
- using System.Collections.Generic;
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- namespace WebApp.Models
- {
- public class UserList
- {
- public UserList()
- {
- //La clase HashSet proporciona operaciones de conjuntos de alto rendimiento.
- //Un conjunto es una coleccion que no contiene elementos duplicados, y cuyos elementos no tienen un orden en particular.
- Products = new HashSet<Product>();
- UserListFriends = new HashSet<UserListFriend>();
- }
-
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int IdUserList { get; set; }
-
- public DateTime CreationDate { get; set; }
-
- public int IdUser { get; set; }
-
- [ForeignKey("IdUser")]
- public User User { get; set; }
-
- [JsonIgnore]
- public virtual ICollection<Product> Products { get; set; }
-
- [JsonIgnore]
- public virtual ICollection<UserListFriend> UserListFriends { get; set; }
- }
- }
Para entender mejor acerca de la serialización de objetos en JSON podemos observar el siguiente artículo http://julitogtu.com/2013/11/12/asp-net-web-api-personalizando-la-serializacion-de-los-objetos-en-json/
Un dato curioso con Entity Framework para los que siempre nos encargamos de realizar la Normalización de nuestra Base de Datos, es que si por ejemplo tenemos 2 Tablas las cuales tienen una relación de muchos a muchos es obvio que tenemos que crear un Tabla intermedia la cual nos sirva de puente entre las dos, registrando la Clave Primaria de ambas Tablas. Si en nuestro caso no vamos a incluir un campo adicional a esta Tabla intermedia podemos dejarle la labor al Entity Framework de crearla automáticamente. Para que tengamos un ejemplo claro la Tabla anterior UserLists tiene una relación de muchos a muchos con la Tabla Products (una Lista de Usuario puede tener muchos Productos asignados y un mismo Producto puede estar en muchas Listas de Usuario), por lo cual en nuestro modelo un Producto sería por ejemplo:
Product.cs
- using Newtonsoft.Json;
- using System.Collections.Generic;
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- namespace WebApp.Models
- {
- public class Product
- {
- public Product()
- {
- UserLists = new HashSet<UserList>();
- }
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int IdProduct { get; set; }
-
- public string Name { get; set; }
-
- public string Image { get; set; }
-
- [JsonIgnore]
- public virtual ICollection<UserList> UserLists { get; set; }
- }
- }
Y ya Entity Framework se encargaría en este caso de crear la Tabla Intermedia UserListProducts.
- Detalle Importante para tener en Cuenta de Entity Framework en Nuestros Controladores de WebAPI
Supongamos que en una acción del Usuario vamos a registrar su Lista, la cual definimos en nuestro Modelo (UserList). Esto lo vamos a realizar mediante un Controlador de WebAPI al cual le vamos a enviar los datos que requiere una entidad UserList para crearse, en un POST con AJAX. El usuario ya ha seleccionado con Knockout su lista de productos y su lista de amigos, los productos fueron obtenidos de la Base de Datos con nuestro Método de Extensión (definido previamente) al momento de cargarse la página Web, en cambio los amigos fueron obtenidos mediante una petición AJAX a una URL externa como por ejemplo con Facebook.
Como la lista de amigos son una Colección de registros totalmente nuevos a la Tabla UserListFriends con Entity Framework no existe ningún problema, al momento de insertar la nueva Lista (UserList) y guardar los cambios ( context.SaveChanges(); ) se insertará también la lista de amigos en la Base de Datos, pero el caso de la Colección de Productos es totalmente distinto y ya les voy a explicar el porque.
Entity Framework al realizar un Select a nuestra Base de Datos se encarga de asociar la respuesta con las Entidades definidas en nuestro Modelo y el Contexto por lo tanto le hace un seguimiento a estas Entidades. Como la lista de Productos que le mandaremos a Knockout inicialmente pertenecen a un Contexto diferente al que vamos a emplear en el Controlador de WebAPI, a pesar que de los Productos ya existen en la Base de Datos y estén con la misma Clave Primaria, Entity Framework los tomará como nuevas Entidades y si guardamos serán registros totalmente nuevos, para evitar esto necesitamos primero realizar un Select de los Productos antes de asociar estos con nuestra Lista de Usuario, para que el Contexto entienda que ya existen:
UserListsController.cs
- namespace WebApp.Controllers
- {
- public class UserListsController : ApiController
- {
- // POST api/userlists
- public HttpResponseMessage Post(UserList userlist)
- {
- if (ModelState.IsValid)
- {
- var Products = userlist.Products;
- userlist.Products = new List<Product>();
- using (DataContext context = new DataContext())
- {
- foreach (var product in Products)
- {
- userlist.Products.Add(context.Products.Single(x => x.IdProduct == product.IdProduct));
- }
- userlist.User = context.Users.Single(x => x.IdUser == userlist.IdUser);
- userlist.CreationDate = DateTime.UtcNow.AddHours(-5);
- context.UserLists.Add(userlist);
- context.SaveChanges();
- }
- return Request.CreateResponse(HttpStatusCode.Created, userlist);
- }
- else
- {
- return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
- }
- }
- }
- }
Se aceptan sugerencias de maneras más optimas para realizar este procedimiento y por lo cual actualizar este artículo, hasta ahora no se me ocurre otra manera excepto que se llamen Procedimientos Almacenados con el Entity Framework como podemos observar en el siguiente artículo http://jhonnyslopez.wordpress.com/2012/04/21/procedimientos-almacenados-transacciones-en-entity-framework-4-3-1-con-c-y-visual-studio-11/
Iniciando con Knockout.js y Linq.js
Fuente: http://it.toolbox.com/blogs/rymoore/aspnet-mvc-and-knockout-mvc-vs-mvvm-54423
Como comenté anteriormente Knockout al aplicar el Modelo de Presentación (ViewModel) emplea observadores para enlazar los datos de nuestro Modelo de manera bidireccional a nuestra Vista, esto en sí nos permite realizar un seguimiento de los cambios que se realicen en la Vista y tanto el Modelo de Presentación como la misma Vista se mantendrán actualizados sin que nosotros tengamos que realizar alguna manipulación del DOM manualmente. Por cierto, No todo en nuestro Modelo de Presentación de Knockout tiene que ser observable, solo aquello que se requiera.
Ahora veamos un ejemplo para entender todo esto:
Como pueden observar en este sencillo ejemplo ya estamos haciendo uso de nuestro Método de Extensión con una Lista de Productos que será pasada por el Controlador a nuestra Vista, por lo cual debemos importar tanto el Namespace de nuestro Modelo como el de la Clase de nuestro método.
Aunque el código HTML con jQuery es más corto que el de Knockout en este ejemplo, este último presenta una serie de ventajas:
- La mayoría de veces no trabajamos solos y por buenas practicas nuestro Javascript quedará en un archivo externo (Especialmente para depurarlo en navegadores como Chrome en la fase de desarrollo) por lo cual si el diseñador decide cambiar el html lo puede hacer perfectamente sin tener que modificar nuestro javascript y mucho menos tener que buscar en todo el archivo la parte donde se inserta el código.
- Como comenté anteriormente no todo en Knockout tiene que ser observable, solo aquello que nos importe tener actualizado en nuestro Modelo de Presentación y sincronizado con la Vista será observable. En este caso se esta obteniendo una Lista de Productos los cuales no van a ser alterados, pero si esta lista fuera a ser modificada más adelante y quisiéramos que nuestro html se mantuviera actualizado solo basta con hacer observable nuestro Array:
self.Products = ko.observableArray(@Html.Raw((ViewBag.Products as List<Product>).ToJson()));
- Y el punto más relevante es que logramos identificar en el html mediante el atributo data-bind (el cual es el que emplea Knockout para enlazar los datos a la Vista de manera declarativa), cual es el evento Click al que apunta en nuestro ViewModel,como el hecho de agregarle la Clase “selected” al Producto que hayamos escogido y asignarle el source a la imagen con el nombre de una propiedad de nuestro Producto definido anteriormente en el Modelo de ASP.NET MVC. Una de las cosas más interesantes es la posibilidad que nos da Javascript para saber si un elemento existe (diferente de null, undefined y false al mismo tiempo) con la doble negación (!!) además del manejo del operador ternario para retornar un valor dependiendo de la condición y Gracias a Knockout mediante el Patrón Observador, que implementa con funciones como ko.observable(),nuestra Vista estará constantemente actualizada.
Ahora veamos un ejemplo de como sería con Knockout, jQuery y Linq.js implementar un buscador y seleccionador de amigos:
- @*El buscador esta asociado a una propiedad observable el cual se actualiza a medida que se escribe*@
- <input placeholder="Buscar" data-bind="value: searchFacebook, valueUpdate: 'afterkeydown'" />
- @*El foreach retorna los amigos que coincidan con el buscador mediante la funcion ko.computed la cual se actualiza cuando una de sus dependencias observables cambia*@
- <div data-bind="foreach: SearchFacebookFriends">
- @*Con jQuery sabemos si el amigo ha sido seleccionado (si se encuentra en el array observable de amigos elegidos)*@
- <a href="javascript:void(0)" data-bind="css: { selected: $.inArray($data, $root.chosenFriends()) > -1 }, click: $root.choseFriend">
- <img width="54" height="54" data-bind="attr:{ src: '//graph.facebook.com/'+ FacebookId +'/picture?type=square' }" />
- <span data-bind="text: Name"></span>
- </a>
- </div>
- <script>
- function WebAppViewModel() {
- var self = this;
- self.searchFacebook = ko.observable();
- self.facebookFriends = ko.observableArray();
- self.chosenFriends = ko.observableArray();
- //Obtenemos el listado de amigos de una fuente como Facebook
- FB.api('/me/friends?fields=id,name', function (response) {
- if (response.data) {
- for (var i = 0; i < response.data.length; i++) {
- var friend = response.data[i];
- self.facebookFriends.push({ Name: friend.name, FacebookId: friend.id });
- }
- }
- });
- //Cuando el buscador cambia mediante Linq.js retornamos los usuarios que coincidan con el termino de busqueda, si no hay nada se muestran todos y ordenados por el nombre
- self.SearchFacebookFriends = ko.computed(function () {
- var searchterm = self.searchFacebook();
- if (searchterm) {
- return Enumerable.From(self.facebookFriends())
- .Where(function (x) { return x.Name.toLowerCase().indexOf(searchterm.toLowerCase()) !== -1 })
- .OrderBy(function (x) { return x.Name })
- .ToArray();
- }
- else {
- return Enumerable.From(self.facebookFriends())
- .OrderBy(function (x) { return x.Name })
- .ToArray();
- }
- });
- //Seleccionamos o Deseleccionamos a un amigo
- self.choseFriend = function (friend, event) {
- $target = $(event.target.parentElement);
- if (!$target.hasClass("selected")) {
- self.chosenFriends.push(friend);
- } else {
- self.chosenFriends.remove(friend);
- }
- };
- }
- ko.applyBindings(new WebAppViewModel());
- </script>
Como podemos observar Linq.js nos permite manipular arreglos muy fácilmente, muy parecido a como lo hacemos habitualmente con Linq y expresiones Lambda en C# pero esta vez desde nuestro Javascript.
Como ejemplo final veamos como sería realizar una petición AJAX a un Controlador de WebAPI definido previamente, enviando en un solo POST la Lista del Usuario (UserList) la cual incluye la lista de amigos (UserListFriends) y la lista de productos (Products) seleccionados con Knockout.
- self.PostData = function () {
- $.post("/api/userlists/", {
- IdUser: "Id del Usuario",
- Products: self.chosenProducts(),
- UserListFriends: self.chosenFriends()
- }, function (userlist) {
- //Respuesta Exitosa
- }).fail(function () {
- //Error
- });
- };
Como podemos observar es tan fácil como indicar los campos definidos en la Entidad (UserList) de nuestro Modelo de ASP.NET MVC para que el Controlador pueda identificar los campos y asociarlos a las propiedades de nuestra Entidad.
Les recomiendo que realicen los tutoriales que tienen en el Sitio Web de Knockout http://learn.knockoutjs.com/.
Y como siempre que lean la documentación a medida que practican por ustedes mismos, ya que esta ha sido una breve introducción a esta poderosa herramienta http://knockoutjs.com/documentation/introduction.html
Espero que con este artículo ustedes hayan podido ver un poquito de lo que es el mundo de Knockout.js y les haya dado esa curiosidad de aprender y conocer más a fondo esta excelente librería para que la puedan poner en practica en sus proyectos, todo esto con el fin de que seamos mucho más productivos.
Como ya saben estaría encantado de recibir cualquier sugerencia o comentario, tanto como para que me corrijan el algún punto o si les puedo servir en alguna duda que tengan, hasta la próxima ;)
da823678-ed1e-4943-856f-1d19215fd0ed|3|2.3|96d5b379-7e1d-4dac-a6ba-1e50db561b04