[Kendo UI] Funcionamiento del Grid – Parte 6 – SignalR

En esta ocasión, vamos a utilizar el control Kendo-UI Grid de Telerik junto con los Hubs de SignalR para mostrar como funcionan las actualizaciones en tiempo real. Es decir, podemos mostrar abrir una misma página desde diferentes instancias del navegador, de forma que las actualizaciones se propaguen a todas las instancias en tiempo real.

En artículos anteriores, ya vimos cómo trabajar con actualizaciones en tiempo real usando los hubs de signalR y como configurar la edición del Grid Kendo UI.

Vamos a crear un nuevo proyecto usando Visual Studio 2015 con las siguientes características:

  • Kendo UI Professional versión 2016.2.714.
  • Framework AngularJS 1.
  • La versión 2 de signalR. Mientras se escribía este artículo, la versión usada es la 2.2.1.
  • EntityFramework como capa de persistencia de datos. Mientras se escribía este artículo, la versión usada es la 6.3.1.

Para preparar nuestro proyecto vamos a seguir los siguientes pasos:

  1. Creamos un nuevo proyecto usando la plantilla Kendo UI ASP.NET MVC 5 Application
  2. Comprobamos si tenemos en .js asociado a nuestra cultura. Para ello buscamos el archivo “kendo.culture.es-ES.min.js” en la carpeta /Scripts/kendo/2016.2.714/ o dentro del subcarpeta “cultures” . Si no lo encontramos, vamos a añadirlo copiándolo de la carpeta de instalación de Kendo (por defecto, C:\Program Files (x86)\Telerik\Kendo UI Professional Q2 2016\js\cultures) a la carpeta de nuestro proyecto /Scripts/kendo/2016.2.714.
  3. Agregamos el paquete Nuget para EntityFramework
    install-package EntityFramework
    
  4. Activamos las migrations de EntityFramwork
    enable-migrations
    

    Este proceso crea la carpeta “Migrations” y dentro de ella, se creará la clase “Configuration”.

    namespace KendoGridSignalR.Migrations
    {
        using System;
        using System.Data.Entity;
        using System.Data.Entity.Migrations;
        using System.Linq;
    
        internal sealed class Configuration : DbMigrationsConfiguration<KendoGridSignalR.Models.ContextoClientes>
        {
            public Configuration()
            {
                AutomaticMigrationsEnabled = true;
            }
    
            protected override void Seed(KendoGridSignalR.Models.ContextoClientes context)
            {
                //  This method will be called after migrating to the latest version.
    
                //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
                //  to avoid creating duplicate seed data. E.g.
                //
                //    context.People.AddOrUpdate(
                //      p => p.FullName,
                //      new Person { FullName = "Andrew Peters" },
                //      new Person { FullName = "Brice Lambson" },
                //      new Person { FullName = "Rowan Miller" }
                //    );
                //
            }
        }
    }
    
  5. Creamos la carpeta “models” y agregamos las clases para las siguientes entidades
    • Clientes
    • Facturas
    • LineasFactura
    • FormasPago
    • Articulos
    • Incidencias
    • TiposCliente

    Nota: El código completo se incluirá en un zip al final del artículo.

  6. Creamos la clase para el contexto de EntityFramework
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Data.Entity;
    using KendoGridSignalR.Models.Mapping;
    using KendoGridSignalR.Migrations;
    
    namespace KendoGridSignalR.Models
    {
        public class ContextoClientes:DbContext
        {
            static ContextoClientes()
            {
                Database.SetInitializer<ContextoClientes>(new MigrateDatabaseToLatestVersion<ContextoClientes, Configuration>());
            }
    
            public ContextoClientes() : base("ClientesContext")
            {
    
            }
            public DbSet<Articulo> Articulos { get; set; }
            public DbSet<Cliente> Clientes { get; set; }
            public DbSet<Factura> Facturas { get; set; }
            public DbSet<FormaPago> FormasPago { get; set; }
            public DbSet<LineaFactura> LineasFactura { get; set; }
            public DbSet<TipoCliente> TiposCliente { get; set; }
            public DbSet<Incidencia> Incidencias { get; set; }
    
            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                modelBuilder.Configurations.Add(new ClienteMap());
                modelBuilder.Configurations.Add(new FacturaMap());
                modelBuilder.Configurations.Add(new ArticuloMap());
                modelBuilder.Configurations.Add(new FormaPagoMap());
                modelBuilder.Configurations.Add(new LineaFacturaMap());
                modelBuilder.Configurations.Add(new TipoClienteMap());
                modelBuilder.Configurations.Add(new IncidenciaMap());
            }
        }
    }
    
  7. Añadimos una “migration” para la creación inicial de las tablas.
    add-migration inicial
    
  8. Modificamos el fichero web.config para añadir la cadena de conexión que usará EntityFramework
      <connectionStrings>
        <add name="ClientesContext" connectionString="Data Source=(local);Initial Catalog=Clientes;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
      </connectionStrings>
    
  9. Agregamos el paquete Nuget para SignalR
    install-package Microsoft.AspNet.SignalR
    

    Nota: Al final del proceso de instalación, se crea un fichero “readme.txt” con instrucciones sobre cómo incluir signalr en nuestro proyecto. Esto es debido a que signalr se basa en OWIN.

  10. Creamos la clase de inicio para OWIN (Por defecto, startup.cs)
    using Microsoft.AspNet.SignalR;
    using Owin;
    
    namespace HubChatBasico
    {
        public static class Startup
        {
            public static void Configuration(IAppBuilder app)
            {
                app.MapSignalR();
            }
        }
    }
    

    En los Hubs no es necesario especificar la url, ya que por defecto la url para todos los Hubs es “/signalr”.

  11. Modificamos el fichero “/App_Start/BundleConfig.cs” para incluir bundles para Angular, SignalR y Kendo
    using System.Web;
    using System.Web.Optimization;
    
    namespace KendoGridSignalR
    {
        public class BundleConfig
        {
            // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
            public static void RegisterBundles(BundleCollection bundles)
            {
                bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                            "~/Scripts/jquery-{version}.js"));
    
                // Use the development version of Modernizr to develop with and learn from. Then, when you're
                // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
                bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                            "~/Scripts/modernizr-*"));
    
                bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                          "~/Scripts/bootstrap.js",
                          "~/Scripts/respond.js"));
    
                bundles.Add(new StyleBundle("~/Content/css").Include(
                          "~/Content/bootstrap.css",
                          "~/Content/site.css"));
    
                //angular
                bundles.Add(new ScriptBundle("~/bundles/angular").Include(
                          "~/Scripts/kendo/2016.2.714/angular.min.js"));
    
                //kendo
                bundles.Add(new ScriptBundle("~/bundles/kendo").Include(
                          "~/Scripts/kendo/2016.2.714/kendo.all.min.js",
                          "~/Scripts/kendo/2016.2.714/kendo.culture.es-ES.min.js"));
    
                bundles.Add(new StyleBundle("~/Content/kendocss").Include(
                          "~/Content/kendo/2016.2.714/kendo.common.min.css",
                          "~/Content/kendo/2016.2.714/kendo.rtl.min.css",
                          "~/Content/kendo/2016.2.714/kendo.silver.min.css",
                          "~/Content/kendo/2016.2.714/kendo.mobile.all.min.css"));
    
                //signalr
                bundles.Add(new ScriptBundle("~/bundles/signalr").Include(
                          "~/Scripts/jquery.signalR-2.2.1.min.js"));
            }
        }
    }
    

    Observa como la librería js para SignalR es en realidad un plugin de jQuery, por lo que se necesita la librería jQuery 1.6.4 o posterior.

  12. Creamos el controlador “HomeController”, añadimos un método de acción llamado “index” y, por último, creamos su vista.
    Nota: a la hora de crear la vista, hay que marcar la opción “Usar página de diseño”, para que se genere el _Layout.cshtml predeterminado.
  13. Modificamos el fichero “/Views/Shared/_Layout.cshtml”
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>@ViewBag.Title - My ASP.NET Application</title>
        @Styles.Render("~/Content/css")
        @Styles.Render("~/Content/kendocss")
        @Scripts.Render("~/bundles/modernizr")
        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/bootstrap")
        @Scripts.Render("~/bundles/angular")
        @Scripts.Render("~/bundles/kendo")
        @Scripts.Render("~/bundles/signalr")
    </head>
    <body>
        <div class="navbar navbar-inverse navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
                </div>
                <div class="navbar-collapse collapse">
                    <ul class="nav navbar-nav">
                    </ul>
                </div>
            </div>
        </div>
    
        <div class="container body-content">
            @RenderBody()
            <hr />
            <footer>
                <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
            </footer>
        </div>
    
        @RenderSection("scripts", required: false)
    </body>
    </html>
    
  14. Por último, vamos a preparar nuestra vista “Index” para trabajar con Angular y SignalR. Para ello creamos un módulo (appGridHub) y controlador (ctrlGridHub) Angular
    @{
        ViewBag.Title = "Index";
    }
    <div ng-app="appGridHub" ng-controller="ctrlGridHub">
    </div>
    <script src="/signalr/js"></script>
    <script>
        var app = angular.module("appGridHub", ["kendo.directives"]);
    
        app.value('$', $);
    
        app.controller("ctrlGridHub", function ($scope, $http, $) {
    
            
        }).run(function () {
            kendo.culture("es-ES");
        });
    </script> 
    

En este punto, ya tenemos nuestro proyecto preparado para implementar nuestro ejemplo usando Kendo UI Grid y SignalR. En primer lugar, vamos implementar la parte del servidor añadiendo un elemento de tipo Clase de concentrador SignalR v2

PlantillaHUb

con los métodos para recuperar, crear, actualizar y borrar clientes.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using KendoGridSignalR.Models;
using Microsoft.AspNet.SignalR;

namespace KendoGridSignalR.Hubs
{
    public class ClientesHub : Hub
    {
        ContextoClientes db = new ContextoClientes();

        public IEnumerable<Cliente> ObtenerClientes()
        {
            return db.Clientes;
        }

        public Cliente ObtenerClientePorId(int id)
        {
            Cliente result = db.Clientes.Find(id);
            return result;
        }

        public bool ActualizarCliente(Cliente cliente)
        {
            int nErrores = 0;

            if (cliente != null)
            {
                var articuloOriginal = db.Clientes.Find(cliente.Id);
                if (articuloOriginal == null)
                {
                    nErrores++;
                }
                else
                {
                    db.Entry(articuloOriginal).CurrentValues.SetValues(cliente);
                    db.Entry(articuloOriginal).State = EntityState.Modified;
                    try
                    {
                        db.SaveChanges();
                    }
                    catch (Exception)
                    {
                        nErrores++;
                    }
                }
            }
            else
            {
                nErrores++;
            }
            if (nErrores == 0)
            {
                Clients.Others.update(cliente);
                return true;
            }
            else
            {
                return false;
            }
        }

        public bool BorrarCliente(Cliente cliente)
        {
            int nErrores = 0;

            var clientedb = db.Clientes.Find(cliente.Id);
            if (clientedb == null)
            {
                nErrores++;
            }
            else
            {
                db.Clientes.Remove(clientedb);
                try
                {
                    db.SaveChanges();
                }
                catch (Exception)
                {
                    nErrores++;
                }
            }
            if (nErrores == 0)
            {
                Clients.Others.destroy(clientedb);
                return true;
            }
            else
            {
                return false;
            }
        }

        public bool CrearCliente(Cliente cliente)
        {
            int nErrores = 0;

            if (cliente == null)
            {
                nErrores++;
            }
            else
            {
                db.Clientes.Add(cliente);
                try
                {
                    db.SaveChanges();
                }
                catch (Exception)
                {
                    nErrores++;
                }
            }
            if (nErrores == 0)
            {
                Clients.Others.create(cliente);
                return true;
            }
            else
            {
                return false;
            }
        }

    }
}

La clase del Hub anterior incluye los siguientes métodos:

  • ObtenerClientes: retorna los clientes de la base de datos.
  • ActualizarCliente: actualiza el cliente en la base de datos e invoca al método “update” de todos los clientes conectados al hub exceptuando a la conexión actual (Clients.Others)
  • BorrarCliente: borra el cliente en la base de datos e invoca al método “destroy” de todos los clientes conectados al hub exceptuando a la conexión actual (Clients.Others)
  • CrearCliente: añade el cliente a la base de datos e invoca al método “create” de todos los clientes conectados al hub exceptuando a la conexión actual (Clients.Others)

A continuación, vamos a implementar la parte cliente en la vista “index.cshtml”

@{
    ViewBag.Title = "Index";
}
<div ng-app="appGridHub" ng-controller="ctrlGridHub">
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Grid Kendo - SignalR</h3>
        </div>
        <div class="panel-body">
            <span kendo-notification="Notificacion" k-append-to="'#ContenedorNotificaciones'"></span>
            <div id="ContenedorNotificaciones" class="k-content"></div>

            <div kendo-grid k-options="opcionesGridSignalR"></div>
        </div>

    </div>
</div>
<script src="/signalr/js"></script>
<script>
    var app = angular.module("appGridHub", ["kendo.directives"]);

    app.value('$', $);

    app.controller("ctrlGridHub", function ($scope, $http, $) {
        $scope.hubStart = null;
        $scope.hub = null;
        $scope.conexion = null;

        $scope.iniciar = function () {
            $scope.hub = $.connection.clientesHub;
            $scope.conexion = $.connection;
            $scope.hubStart = $.connection.hub.start();

            $.connection.logging = true;
        };

        $scope.iniciar();

        $scope.mostrarNotificacion = function (tipo) {
            var date = new Date();
            var texto = kendo.toString(date, "HH:MM:ss.") + tipo;
            $scope.Notificacion.show(texto, "info");
        };

        $scope.origenDatos = new kendo.data.DataSource({
            type: "signalr",
            autoSync: true,
            schema: {
                model: {
                    id: "Id",
                    fields: {
                        Id: { editable: false, type: "number" },
                        Nombre: { type: "string" },
                        Mail: { type: "string" },
                        Telefono: { type: "string" },
                        Direccion: { type: "string" }
                    }
                }
            },
            transport: {
                signalr: {
                    promise: $scope.hubStart,
                    hub: $scope.hub,
                    server: {
                        read: "obtenerClientes",
                        update: "ActualizarCliente",
                        destroy: "BorrarCliente",
                        create: "CrearCliente"
                    },
                    client: {
                        read: "read",
                        update: "update",
                        destroy: "destroy",
                        create: "create"
                    }
                }
            },
            push: function (e) {
                $scope.mostrarNotificacion(e.type);
            },
            error: function (e) {
                alert("Status: " + e.status + "; Error message: " + e.errorThrown);
            }
        });

        $scope.opcionesGridSignalR = {
            columns: [
                { field: "Id", title: "Identificador" },
                { field: "Nombre", title: "Nombre Completo" },
                { field: "Mail", title: "e-Mail" },
                { field: "Telefono", title: "Telefono" },
                { field: "Direccion", title: "Dirección" },
                {
                    command: [

                                {
                                    name: "destroy",
                                    text: "Borrar"
                                }
                    ],
                    title: "&nbsp;",
                    width: "250px"
                }
            ],
            dataSource: $scope.origenDatos,
            editable: true,
            toolbar: [
                {
                    name: "create",
                    text: "Nuevo Cliente"
                }

            ]
        };


    }).run(function () {
        kendo.culture("es-ES");
    });

</script>

la cual realiza las siguientes operaciones:

  1. Añadimos el grid de kendo usando la directiva “kendo-grid”
    <div kendo-grid k-options="opcionesGridSignalR"></div>
    
  2. Añadimos una referencia a la url del hub “/signalr/js” (ruta predeterminada)
    <script src="/signalr/js"></script>
    

    para que SignalR genere dinámicamente el proxy javascript.

    Nota: este proxy, permite que el cliente javascript invoque fácilmente métodos del hub en el servidor
    Nota: si implementamos varios hubs en el servidor, al referenciar “/signalr/js” se creará un proxy por cada hub cuyo nombre sigue la nomenclatura “camelCase”.

  3. En el $scope del controlador, hemos creado el método “iniciar” el cual guarda la conexión de signalR, el hub y el promise retornado por el método start()
  4. Creamos el objeto “kendo.data.DataSource” con las siguientes propiedades
    • type: signalr
    • autoSync: Si se establece a true, cada vez que se cambie el valor de una columna, dicha modificación es enviada automáticamente al servidor. En caso contrario, al confirmar la edición de la fila, todos los cambios en la fila son enviados juntos al servidor.
    • autoSync: true
    • schema: especificamos los campos de la entidad con la que vamos a trabajar
    • transport: esta propiedad es la base de la configuración. Debemos asignar la referencia del hub y del promise. Ademas, debemos configurar los nombres de los métodos para cada una de las operaciones en la parte cliente y servidor.
    • push: hemos definido una función que se invocará cuando el cliente recibe un push desde el servidor. Esta función se ejecuta cuando desde el servidor se invoca a un método en la parte cliente (Clientes.Others, Clients.Caller,…)
    • error: hemos definido una función que se invocará cuando se produzca un error.
  5. Usamos la directiva k-options para asociar una variable del $scope, en la cual establecemos la configuración del grid: columnas, origen de datos, opciones de edición …

Si ahora ejecutamos el proyecto y abrimos 2 o mas instancias del navegador, podemos comprobar que las actualizaciones y borrados funcionan correctamente.

KendoGridSignalRActualizacion

Pero si intentamos añadir un nuevo registro, ocurre lo siguiente:

  1. Al pulsar sobre “Nuevo Cliente” se invoca al método “create” del hub, el cual crea un registro vació con un id generado por la base de datos.
  2. Este nuevo registro es comunicado al resto de instancias
  3. El navegador que creo el cliente desconoce el id asignado por la base de datos, por lo que su id=0

KendoGridSignalRCreate_1

Si ahora intentamos modificar el registro, cada columna modificada creará un nuevo cliente en la base de datos. En la siguiente imagen, se puede ver como al cambiar el valor de 4 columnas, se invoca 4 veces al método “create” del hub.

KendoGridSignalRCreate_2

Hemos intentado buscar una solución usando la configuración del grid, pero al no encontrar nada, hemos realizado una pequeña modificación añadiendo un método en la parte cliente, el cual será invocado por el método “create” del hub.

En el siguiente fragmento de código, mostramos el método create del hub

        public bool CrearCliente(Cliente cliente)
        {
            int nErrores = 0;

            if (cliente == null)
            {
                nErrores++;
            }
            else
            {
                db.Clientes.Add(cliente);
                try
                {
                    db.SaveChanges();
                }
                catch (Exception)
                {
                    nErrores++;
                }
            }
            if (nErrores == 0)
            {
                Clients.Caller.actualizar();
                Clients.Others.create(cliente);
                return true;
            }
            else
            {
                return false;
            }
        }

Como podemos ver hemos añadido la sentencia “Clients.Caller.actualizar()” para invocar al método actualizar de la instancia que creo el cliente.

En la parte cliente simplemente añadimos el método “actualizar”, el cual refresca los datos del grid.

        $scope.iniciar = function () {
            $scope.hub = $.connection.clientesHub;
            $scope.conexion = $.connection;
            $scope.hubStart = $.connection.hub.start();

            $scope.hub.client.actualizar = function () {
                $scope.opcionesGridSignalR.dataSource.read();
            };

            $.connection.logging = true;
        };

Si ahora volvemos a probar el proyecto, vemos que las operaciones “create”, “update” y “destroy” funcionan correctamente.

KendoGridSignalRCreate_3

En el siguiente enlace que puede descargar el proyecto de ejemplo