[Kendo UI] Funcionamiento del Grid – Parte 2 – Edicion

Vamos a utilizar el control Kendo-UI Grid de Telerik para enlazarlo a un servicio OData, de forma que vamos a ver cómo recuperar datos, editarlos, borrarlos…

En un artículo anterior vimos cómo trabajar con el Kendo-UI Grid enlazado a un array de datos locales. Para ello usamos la plantilla Kendo UI ASP.NET MVC 5 Application junto con el framework AngularJS 1.

Antes de comenzar vamos a preparar nuestro proyecto siguiendo los siguientes pasos:

– Creamos un nuevo proyecto usando la plantilla de KendoUI y AngularJS.

NOTA: Anteriormente trabajamos con la versión 2016.1.226 de Kendo-UI. En esta ocasión vamos a usar la versión 2016.2.504 (última versión disponible durante la elaboración de este articulo)

– 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.504/ 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.504.

Nota: en nuestro caso no lo teníamos, por lo que hemos tenido que copiarlo a mano.

– 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.

– Modificamos el fichero “/App_Start/BundleConfig.cs”, para añadir los bundles de angular y kendo

using System.Web;
using System.Web.Optimization;

namespace GridKendoRemoto
{
    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.504/angular.min.js"));

            //kendo
            bundles.Add(new ScriptBundle("~/bundles/kendo").Include(
                      "~/Scripts/kendo/2016.2.504/kendo.all.min.js",
                      "~/Scripts/kendo/2016.2.504/kendo.culture.es-ES.min.js"));

            bundles.Add(new StyleBundle("~/Content/kendocss").Include(
                      "~/Content/kendo/2016.2.504/kendo.common.min.css",
                      "~/Content/kendo/2016.2.504/kendo.rtl.min.css",
                      "~/Content/kendo/2016.2.504/kendo.silver.min.css",
                      "~/Content/kendo/2016.2.504/kendo.mobile.all.min.css"));
        }
    }
}

– 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")
</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 - Kendo UI Grid Datos Remotos</p>
        </footer>
    </div>

    @RenderSection("scripts", required: false)
</body>
</html>

– En este artículo, vamos a trabajar con el Grid de Kendo enlazado a un servicio OData, por lo que vamos a descargar el proyecto de ejemplo usando en el artículo [ODATA V4] Funciones y Acciones, y añadimos el siguiente fragmento de código al archivo Global.asax

        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
            if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
            {
                HttpContext.Current.Response.AddHeader("Cache-Control", "no-cache");
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, MERGE, PUT, DELETE");
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
                HttpContext.Current.Response.AddHeader("Access-Control-Max-Age", "1728000");
                HttpContext.Current.Response.End();
            }
        }

El código anterior realiza 2 operaciones al llegar una petición
1. Añadir la cabecera “Access-Control-Allow-Origin” (CORS) a la respuesta HTTP, para permitir peticiones desde cualquier dominio de origen
2. Si nos llega una petición HTTP de tipo “OPTIONS“, generamos una serie de cabeceras HTTP, donde vamos a destacar “Access-Control-Allow-Methods”, la cual indica las operaciones permitidas por nuestro servicio.

Nota: Esta modificación es necesaria, ya que kendo realiza peticiones HTTP de tipo OPTIONS antes de enviar modificaciones al servicio de datos remoto.

Aquí os dejo el enlace con el servicio OData modificado.

Una vez que tenemos listo el servicio Odata, vamos iniciarlo para tomar nota de la url. En nuestro caso es http://localhost:49952/

Por último, vamos a preparar nuestra vista “Index” para trabajar con Angular. Para ello creamos un módulo (appGridRemoto) y controlador (ctrlGridRemoto) Angular

@{
    ViewBag.Title = "Index";
}
<div ng-app="appGridRemoto" ng-controller="ctrlGridRemoto">

</div>

<script>
    angular.module("appGridRemoto", ["kendo.directives"])
            .controller("ctrlGridRemoto", function ($scope, $http) {

            }).run(function () {
                kendo.culture("es-ES");
            })
</script>

En este punto, ya tenemos nuestro proyecto preparado pasa usar Kendo, Angular y el servicio web OData.

Vamos a comenzar añadiendo un grid de kendo que solo muestre los datos de clientes de un servicio OData

@{
    ViewBag.Title = "Index";
}
<div ng-app="appGridRemoto" ng-controller="ctrlGridRemoto">
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Grid Remoto OData - Recuperar Clientes</h3>
        </div>
        <div class="panel-body">
            <div kendo-grid k-options="opcionesGridRemotoClientes"></div>
        </div>
    </div>
</div>

<script>
    angular.module("appGridRemoto", ["kendo.directives"])
            .controller("ctrlGridRemoto", function ($scope, $http) {
                $scope.origenDatos = new kendo.data.DataSource({
                    type: "odata-v4",
                    transport: {
                        read: {
                            url: "http://localhost:49952/Clientes",
                            dataType: "json"
                        },
                    },
                    schema: {
                        data: function (data) {
                            return data.value;
                        },
                        total: function (data) {
                            return data['@@odata.count'];
                        },
                        model: {
                            id: "Id",
                            fields: {
                                Id: { editable: false, type: "number" },
                                Nombre: { type: "string" },
                                Mail: { type: "string" },
                                Telefono: { type: "string" },
                                Direccion: { type: "string" },
                                Tipo: { type: "string" }
                            }
                        }
                    },
                    pageSize: 5,
                    error: function (e) {
                        alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                    }
                });
 
                $scope.opcionesGridRemotoClientes = {
                    groupable: true,
                    filterable: true,
                    pageable:true,
                    sortable: {
                        allowUnsort: true,
                        mode: "multiple"
                    },
                    columnMenu: {
                        columns: true,
                        filterable: true,
                        sortable: true
                    },
                    columns: [
                        { field: "Nombre", title: "Nombre Completo" },
                        { field: "Telefono", title: "Telefono" },
                        { command: ["edit", "destroy"], title: "&nbsp;", width: "250px" }
                    ],

                    dataSource: $scope.origenDatos,
                };
            }).run(function () {
                kendo.culture("es-ES");
            })
</script>

En este fragmento de código hemos realizado las siguientes operaciones:
– Creamos un DataSource que tipo “odata-v4” que realice lecturas a la url http://localhost:49952/Clientes usando la propiedad “read” del objeto “Trasport”

– En el DataSource, hemos configurado el schema con la entidad cliente recuperada por el servicio y, el tamaño de la página.

Nota: Observa como hemos configurado el campo asociado al identificador de la entidad (id: “Id”). Este punto es muy importante para que Kendo pueda gestionar correctamente las modificaciones y borrados.

De forma opcional, hemos configurado el campo “total” para guardar el total de registros (es un total general independiente de los datos retornados por página) y “data” para guardar los datos recibidos.

– Creamos el grid de kendo (añadiendo el atributo “kendo-grid” a un elemento HTML de tipo DIV) asociando su configuración a la variable “opcionesGridRemotoClientes” del $scope de AngularJS. En un artículo anterior, ya vimos algunas de las opciones de configuración.

Si ahora ejecutamos nuestro proyecto, vemos que aparentemente funciona bien

RecuperarClientes1

Pero si analizamos las peticiones HTTP y sus respuestas, vemos que solo se hace una petición al servidor, la cual incluye todos los clientes (Esto indica que Kendo, de forma predeterminada, realiza la paginación en la parte cliente). En la mayoría de los casos, necesitaremos que la paginación la realice el servidor. Para conseguirlo, basta con añadir la propiedad “serverPaging” al DataSource

      $scope.origenDatos = new kendo.data.DataSource({
          type: "odata-v4",
          transport: {
              read: {
                  url: "http://localhost:49952/Clientes",
                  dataType: "json"
              }
          },
          schema: {
              data: function (data) {
                  return data.value;
              },
              total: function (data) {
                  return data['@@odata.count'];
              },
              model: {
                  id: "Id",
                  fields: {
                      Id: { editable: false, type: "number" },
                      Nombre: { type: "string" },
                      Mail: { type: "string" },
                      Telefono: { type: "string" },
                      Direccion: { type: "string" },
                      Tipo: { type: "string" }
                  }
              }
          },
          pageSize: 5,
          serverPaging: true,
          error: function (e) {
              alert("Status: " + e.status + "; Error message: " + e.errorThrown);
          }
      }); 

Si ahora volvemos a probar el ejemplo, vemos que se realiza una petición de datos cada vez que se cambia de página.

Si ahora usamos los filtros a nivel de columna, podemos comprobar que por defecto, el filtrado se realiza en el lado cliente, por lo que debemos establecer a true la propiedad “serverFiltering” del DataSource.

Con la ordenación pasa exactamente lo mismo, por lo que para delegar esta tarea en el servidor debemos establecer a true la propiedad “serverSorting”.

En el siguiente fragmento de código podemos ver cómo queda nuestro DataSource

                $scope.origenDatos = new kendo.data.DataSource({
                    type: "odata-v4",
                    transport: {
                        read: {
                            url: "http://localhost:49952/Clientes",
                            dataType: "json"
                        }
                    },
                    schema: {
                        data: function (data) {
                            return data.value;
                        },
                        total: function (data) {
                            return data['@@odata.count'];
                        },
                        model: {
                            id: "Id",
                            fields: {
                                Id: { editable: false, type: "number" },
                                Nombre: { type: "string" },
                                Mail: { type: "string" },
                                Telefono: { type: "string" },
                                Direccion: { type: "string" },
                                Tipo: { type: "string" }
                            }
                        }
                    },
                    pageSize: 5,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true,
                    error: function (e) {
                        alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                    }
                }); 

En el siguiente paso vamos a activar la edición. El componente grid de kendo incluye varias opciones: inline, popup, batch

INLINE

Para activar la edición “inline”, hay que establecer la propiedad “editable/mode” como “inline”. En el siguiente fragmento de código vemos un ejemplo de edición que incluye las opciones para creación, modificación y borrado.

    angular.module("appGridRemoto", ["kendo.directives"])
            .controller("ctrlGridRemoto", function ($scope, $http) {
                $scope.origenDatos = new kendo.data.DataSource({
                    type: "odata-v4",
                    transport: {
                        read: {
                            url: "http://localhost:49952/Clientes",
                            dataType: "json"
                        },
                        create: {
                            type:"POST",
                            url: "http://localhost:49952/Clientes",
                            contentType: "application/json",
                            dataType: "json",
                            complete: function (e) {
                                $scope.recuperarClientes();
                            }
                        },
                        update: {
                            url: function (data) {
                                return "http://localhost:49952/Clientes" + "(" + data.Id + ")";
                            },
                            type: "PUT",
                            contentType: "application/json",
                            dataType: "json"
                        },
                        destroy: {
                            url: function (data) {
                                return "http://localhost:49952/Clientes" + "(" + data.Id + ")";
                            },
                            dataType: "json",
                            type: "DELETE"
                        }
                    },
                    schema: {
                        data: function (data) {
                            return data.value;
                        },
                        total: function (data) {
                            return data['@@odata.count'];
                        },
                        model: {
                            id: "Id",
                            fields: {
                                Id: { editable: false, type: "number" },
                                Nombre: { type: "string" },
                                Mail: { type: "string" },
                                Telefono: { type: "string" },
                                Direccion: { type: "string" },
                                Tipo: { type: "string" }
                            }
                        }
                    },
                    pageSize: 5,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true,
                    error: function (e) {
                        alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                    }
                });
 
                $scope.opcionesGridRemotoClientes = {
                    groupable: true,
                    filterable: true,
                    pageable:true,
                    sortable: {
                        allowUnsort: true,
                        mode: "multiple"
                    },
                    columnMenu: {
                        columns: true,
                        filterable: true,
                        sortable: true
                    },
                    columns: [
                        { field: "Nombre", title: "Nombre Completo" },
                        { field: "Telefono", title: "Telefono" },
                        { command: ["edit", "destroy"], title: "&nbsp;", width: "250px" }
                    ],

                    dataSource: $scope.origenDatos,
                    editable: {
                        createAt: "bottom",
                        mode: "inline"
                    },
                    toolbar: ["create"]
                };

                $scope.recuperarClientes = function () {
                    $scope.opcionesGridRemotoClientes.dataSource.read();
                };
            }).run(function () {
                kendo.culture("es-ES");
            })

El el fragmento de código anterior, as operaciones realizadas son:

  • Modificamos el objeto “Transport” del “DataSource”, para incluir la configuración de las operaciones:
    • “create”: configuramos la petición como POST. Cuando creamos un registro, el servicio OData añade un nuevo registro a la base de datos, generado está el ID. Para recuperar el Id asignado, hemos creado el método “recuperarClientes” en el $scope, para que después de crear un cliente se refresque el grid.
    • “update”: las modificaciones se enviarán como peticiones HTTP PUT. En este caso la url sigue el patrón /Clientes(X).
      Nota: Este formato de URL es una convención de OData.
    • “destroy”. Los borrados se enviarán como peticiones HTTP DELETE.

    En todos los casos, los datos son enviados como json (dataType)

  • Configuramos la propiedad “editable” con las siguientes propiedades:
    • mode: Establecemos el modo de edición como inline.
    • createAt: cuando queremos crear un nuevo registro, la línea de edición se mostrará al final (bottom). La otra opción disponible es “top”.
  • Configuramos el toolbar de opciones para que muestre el botón “create” (este es un botón prediseñado por kendo)
  • En las columnas del grid, añadimos los comandos prediseñados para “edit” y “destroy”.

En la siguiente imagen se puede ver el aspecto del grid, después de pulsar sobre el botón “Add new record” de la toolbar.

EdicionInlineCreate

Cuando rellenemos los campos y pulsemos, sobre “update”, se enviará un POST al servidor y posteriormente un GET para volver a recuperar el cliente ya con el Id que la base de datos le ha asignado.

Si pulsamos sobre el botón “Edit” de una de las líneas, se activa la edición de la misma.

EdicionInlineUpdate

De forma que al pulsar sobre “update”, se enviará un PUT a la url /Clientes(X) (donde X es el Id del cliente) con todos los datos del registro modificado.

Por último, si pulsamos sobre el botón “Delete”, se enviará una petición DELETE.

A continuación, vamos a incluir los cambios necesarios para personalizar los textos de nuestro grid.

                $scope.opcionesGridRemotoClientes = {
                    groupable: {
                        messages: {
                            empty: "Para agrupar el listado, arrastre la cabecera de la columna aquí"
                        }
                    },
filterable: true,
                    pageable: {
                        pageSize: 5,
                        previousNext: true,
                        numeric: true,
                        buttonCount: 5,
                        input: true,
                        pageSizes: [5, 10, 15, 20, 30, "all"],
                        refresh: true,
                        info: true,
                        messages: {
                            display: "Mostrando {0}-{1} de {2} clientes",
                            empty: "No hay datos de clientes",
                            page: "Introduzca Página",
                            of: " de {0}",
                            itemsPerPage: "registros por página",
                            first: "Primera",
                            last: "Última",
                            next: "Siguiente",
                            previous: "Anterior",
                            refresh: "Actualizar",
                            morePages: "Más paginas"
                        }
                    },
                    sortable: {
                        allowUnsort: true,
                        mode: "multiple"
                    },
                    columnMenu: {
                        columns: true,
                        filterable: true,
                        sortable: true
                    },
                    columns: [
                        { field: "Nombre", title: "Nombre Completo" },
                        { field: "Telefono", title: "Telefono" },
                        {
                            command: [
                                {
                                    name: "edit",
                                    text: { edit: "Editar", cancel: "Cancelar Edición", update: "Grabar Cambios" }
                                },
                                {
                                    name: "destroy",
                                    text:"Borrar"
                                }
                            ],
                            title: "&nbsp;",
                            width: "250px"
                        }
                    ],

                    dataSource: $scope.origenDatos,
                    editable: {
                        createAt: "bottom",
                        mode: "inline"
                    },
                    toolbar: [
                        {
                            name: "create",
                            text:"Nuevo Cliente"
                        }
                        
                    ]
                };

En la siguiente imagen se puede ver el aspecto del grid.

EdicionInlineCompletoTextos

La personalización de los textos es bastante intuitiva, de todas formas, en un artículo anterior ya explicamos cómo se configura.

POPUP

El modo de edición “popup”, al pulsar sobre el botón “edit” de la línea se abrirá un PopUp que contiene las columnas del grid en formato formulario tabular. Para probarlo, vamos a tomar como base el ejemplo anterior y a cambiar la propiedad “editable/mode” a “popup”

    //POPUP EDIT
    $scope.origenDatosPopUp = new kendo.data.DataSource({
        type: "odata-v4",
        transport: {
            read: {
                url: "http://localhost:49952/Clientes",
                dataType: "json"
            },
            create: {
                type: "POST",
                url: "http://localhost:49952/Clientes",
                contentType: "application/json",
                dataType: "json",
                complete: function (e) {
                    $scope.recuperarClientesPopUp();
                }
            },
            update: {
                url: function (data) {
                    return "http://localhost:49952/Clientes" + "(" + data.Id + ")";
                },
                type: "PUT",
                contentType: "application/json",
                dataType: "json"
            },
            destroy: {
                url: function (data) {
                    return "http://localhost:49952/Clientes" + "(" + data.Id + ")";
                },
                dataType: "json",
                type: "DELETE"
            }
        },
        schema: {
            data: function (data) {
                return data.value;
            },
            total: function (data) {
                return data['@@odata.count'];
            },
            model: {
                id: "Id",
                fields: {
                    Id: { editable: false, type: "number" },
                    Nombre: { type: "string" },
                    Mail: { type: "string" },
                    Telefono: { type: "string" },
                    Direccion: { type: "string" },
                    Tipo: { type: "string" }
                }
            }
        },
        pageSize: 5,
        serverPaging: true,
        serverFiltering: true,
        serverSorting: true,
        error: function (e) {
            alert("Status: " + e.status + "; Error message: " + e.errorThrown);
        }
    });

    $scope.opcionesGridRemotoClientesPopup = {
        groupable: {
            messages: {
                empty: "Para agrupar el listado, arrastre la cabecera de la columna aquí"
            }
        },
        filterable: true,
        pageable: {
            pageSize: 5,
            previousNext: true,
            numeric: true,
            buttonCount: 5,
            input: true,
            pageSizes: [5, 10, 15, 20, 30, "all"],
            refresh: true,
            info: true,
            messages: {
                display: "Mostrando {0}-{1} de {2} clientes",
                empty: "No hay datos de clientes",
                page: "Introduzca Página",
                of: " de {0}",
                itemsPerPage: "registros por página",
                first: "Primera",
                last: "Última",
                next: "Siguiente",
                previous: "Anterior",
                refresh: "Actualizar",
                morePages: "Más paginas"
            }
        },
        sortable: {
            allowUnsort: true,
            mode: "multiple"
        },
        columnMenu: {
            columns: true,
            filterable: true,
            sortable: true
        },
        columns: [
            { field: "Nombre", title: "Nombre Completo" },
            { field: "Telefono", title: "Telefono" },
            {
                command: [
                    {
                        name: "edit",
                        text: { edit: "Editar", cancel: "Cancelar Edición", update: "Grabar Cambios" }
                    },
                    {
                        name: "destroy",
                        text: "Borrar"
                    }
                ],
                title: "&nbsp;",
                width: "250px"
            }
        ],

        dataSource: $scope.origenDatosPopUp,
        editable: {
            createAt: "bottom",
            mode: "popup"
        },
        toolbar: [
            {
                name: "create",
                text: "Nuevo Cliente"
            }

        ]
    };
    $scope.recuperarClientesPopUp = function () {
        $scope.opcionesGridRemotoClientesPopUp.dataSource.read();
    }; 

Si ejecutamos el proyecto, y pulsamos sobre el botón “edit”, vemos como efectivamente se muestra una ventana modal para editar el contenido de la fila

EdicionPopup

Los controles asociados a cada columna, se puede personalizar. Por ejemplo, vamos a añadir la columna “Tipo”, de forma que al editar muestre un control ComboBox con las diferentes opciones (Para este ejemplo, hemos usando un array local de valores).

                //POPUP EDIT - Edicion Personalizada
                $scope.origenDatosPopupCustomEdit = new kendo.data.DataSource({
                    type: "odata-v4",
                    transport: {
                        read: {
                            url: "http://localhost:49952/Clientes",
                            dataType: "json"
                        },
                        create: {
                            type: "POST",
                            url: "http://localhost:49952/Clientes",
                            contentType: "application/json",
                            dataType: "json",
                            complete: function (e) {
                                $scope.recuperarClientesPopupCustomEdit();
                            }
                        },
                        update: {
                            url: function (data) {
                                return "http://localhost:49952/Clientes" + "(" + data.Id + ")";
                            },
                            type: "PUT",
                            contentType: "application/json",
                            dataType: "json"
                        },
                        destroy: {
                            url: function (data) {
                                return "http://localhost:49952/Clientes" + "(" + data.Id + ")";
                            },
                            dataType: "json",
                            type: "DELETE"
                        }
                    },
                    schema: {
                        data: function (data) {
                            return data.value;
                        },
                        total: function (data) {
                            return data['@@odata.count'];
                        },
                        model: {
                            id: "Id",
                            fields: {
                                Id: { editable: false, type: "number" },
                                Nombre: { type: "string" },
                                Mail: { type: "string" },
                                Telefono: { type: "string" },
                                Direccion: { type: "string" },
                                Tipo: { type: "string" }
                            }
                        }
                    },
                    pageSize: 5,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true,
                    error: function (e) {
                        alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                    }
                });

                $scope.tipoDropdown = function (container, options) {
                    var editor = $('<select kendo-combo-box k-data-source="[\'General\',\'VIP\']" data-bind="value:' + options.field + '"/>')
                    .appendTo(container);
                }

                $scope.opcionesGridRemotoClientesPopupCustomEdit = {
                    groupable: {
                        messages: {
                            empty: "Para agrupar el listado, arrastre la cabecera de la columna aquí"
                        }
                    },
                    filterable: true,
                    pageable: {
                        pageSize: 5,
                        previousNext: true,
                        numeric: true,
                        buttonCount: 5,
                        input: true,
                        pageSizes: [5, 10, 15, 20, 30, "all"],
                        refresh: true,
                        info: true,
                        messages: {
                            display: "Mostrando {0}-{1} de {2} clientes",
                            empty: "No hay datos de clientes",
                            page: "Introduzca Página",
                            of: " de {0}",
                            itemsPerPage: "registros por página",
                            first: "Primera",
                            last: "Última",
                            next: "Siguiente",
                            previous: "Anterior",
                            refresh: "Actualizar",
                            morePages: "Más paginas"
                        }
                    },
                    sortable: {
                        allowUnsort: true,
                        mode: "multiple"
                    },
                    columnMenu: {
                        columns: true,
                        filterable: true,
                        sortable: true
                    },
                    columns: [
                        { field: "Nombre", title: "Nombre Completo" },
                        { field: "Telefono", title: "Telefono" },
                        { field: "Tipo", title: "Tipo", editor: $scope.tipoDropdown, template: "#=Tipo#" },
                        {
                            command: [
                                {
                                    name: "edit",
                                    text: { edit: "Editar", cancel: "Cancelar Edición", update: "Grabar Cambios" }
                                },
                                {
                                    name: "destroy",
                                    text: "Borrar"
                                }
                            ],
                            title: "&nbsp;",
                            width: "250px"
                        }
                    ],

                    dataSource: $scope.origenDatosPopupCustomEdit,
                    editable: {
                        createAt: "bottom",
                        mode: "popup"
                    },
                    toolbar: [
                        {
                            name: "create",
                            text: "Nuevo Cliente"
                        }

                    ]
                };
                $scope.recuperarClientesPopupCustomEdit = function () {
                    $scope.opcionesGridRemotoClientesPopUp.dataSource.read();
                };

Como podemos observar en el ejemplo anterior, el único cambio ha consistido en añadir propiedad “editor” a la columa “Tipo” y, asignarle una función del $scope, la cual crea un control ComboBox de Kendo-UI.

{ field: "Tipo", 
  title: "Tipo", 
  editor: $scope.tipoDropdown, template: "#=Tipo#" 
}
….

$scope.tipoDropdown = function (container, options) {
        var editor = $('<select kendo-combo-box k-data-source="[\'General\',\'VIP\']" data-bind="value:' + options.field + '"/>')
        .appendTo(container);
}

La personalización la podríamos mejorar para que los valores los recuperará de un servicio web.

Nota: esta personalización sobre el control mostrado al editar, no solo se limita al modo “popup”, sino que también es compatible con el modo “inline” e “incell”.

INCELL

En los ejemplos anteriores, para poder editar una fila había que hacer clic sobre el botón “Edit”. En el siguiente ejemplo, vamos a usar el modo de edición “incell”, de forma que todas las celdas son editables con un simple clic encima de la misma.

    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Grid Remoto OData - Edicion InCell</h3>
        </div>
        <div class="panel-body">
            <div kendo-grid k-options="opcionesGridRemotoClientesIncell"></div>
        </div>
    </div>
    //INCELL EDIT
    $scope.origenDatosBatch = new kendo.data.DataSource({
                    type: "odata-v4",
                    transport: {
                        read: {
                            url: "http://localhost:49952/Clientes",
                            dataType: "json"
                        },
                        create: {
                            type: "POST",
                            url: "http://localhost:49952/Clientes",
                            contentType: "application/json",
                            dataType: "json",
                            complete: function (e) {
                                $scope.recuperarClientes();
                            }
                        },
                        update: {
                            url: function (data) {
                                return "http://localhost:49952/Clientes" + "(" + data.Id + ")";
                            },
                            type: "PUT",
                            contentType: "application/json",
                            dataType: "json"
                        },
                        destroy: {
                            url: function (data) {
                                return "http://localhost:49952/Clientes" + "(" + data.Id + ")";
                            },
                            dataType: "json",
                            type: "DELETE"
                        }
                    },
                    schema: {
                        data: function (data) {
                            return data.value;
                        },
                        total: function (data) {
                            return data['@@odata.count'];
                        },
                        model: {
                            id: "Id",
                            fields: {
                                Id: { editable: false, type: "number" },
                                Nombre: { type: "string" },
                                Mail: { type: "string" },
                                Telefono: { type: "string" },
                                Direccion: { type: "string" },
                                Tipo: { type: "string" }
                            }
                        }
                    },
                    pageSize: 5,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true,
                    error: function (e) {
                        alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                    }
                });
                $scope.opcionesGridRemotoClientesBatch = {
                    groupable: {
                        messages: {
                            empty: "Para agrupar el listado, arrastre la cabecera de la columna aquí"
                        }
                    },
                    filterable: true,
                    pageable: {
                        pageSize: 5,
                        previousNext: true,
                        numeric: true,
                        buttonCount: 5,
                        input: true,
                        pageSizes: [5, 10, 15, 20, 30, "all"],
                        refresh: true,
                        info: true,
                        messages: {
                            display: "Mostrando {0}-{1} de {2} clientes",
                            empty: "No hay datos de clientes",
                            page: "Introduzca Página",
                            of: " de {0}",
                            itemsPerPage: "registros por página",
                            first: "Primera",
                            last: "Última",
                            next: "Siguiente",
                            previous: "Anterior",
                            refresh: "Actualizar",
                            morePages: "Más paginas"
                        }
                    },
                    sortable: {
                        allowUnsort: true,
                        mode: "multiple"
                    },
                    columnMenu: {
                        columns: true,
                        filterable: true,
                        sortable: true
                    },
                    columns: [
                        { field: "Nombre", title: "Nombre Completo" },
                        { field: "Telefono", title: "Telefono" },
                        {
                            command: [
                                {
                                    name: "destroy",
                                    text: "Borrar"
                                }
                            ],
                            title: "&nbsp;",
                            width: "250px"
                        }
                    ],

                    dataSource: $scope.origenDatosBatch,
                    editable: {
                        createAt: "bottom",
                        mode: "incell"
                    },
                    toolbar: [
                        {
                            name: "create",
                            text: "Nuevo Cliente"
                        },
                        {
                            name: "save",
                            text: "Guardar Cambios"
                        },
                        {
                            name: "cancel",
                            text: "Cancelar cambios"
                        }
                    ]
                };

                $scope.recuperarClientesBatch = function () {
                    $scope.opcionesGridRemotoClientesBatch.dataSource.read();
    };

Con respecto a la edición inline, las diferencias son:

  • Hemos quitado el comando “edit” de la fila, ya que no es necesario.
  • Hemos añadido a la toolbar, 2 comandos: “Save” y “Cancel”. Ambos comandos están incluidos en el grid y tan solo hay que especificar el literal a mostrar en pantalla.
  • Hemos cambiado la propiedad “editable/mode” a “incell”.

En ese ejemplo, podemos realizar cambios en las filas (las celdas modificas aparecerán con una marca roja en la esquina izquierda), añadir registros, borrarlos… sin que se produzca ninguna comunicación con el servidor, pero claro, los cambios solo tienen efecto en la parte cliente. Para replicar los cambios al servidor hay que hacer clic sobre el botón “Guardar Cambios” (Save) de la toolbar, de forma que cada fila modificada/borrada/creada es procesada como una petición independiente al servidor (PUT/DELETE/POST).

EdicionInCell

BATCH

Este funcionamiento es debido a que, por defecto, la propiedad “Batch” del Datasource es false. Si modificamos el Datasource, para establecer “Batch:true”, esto provoca que al pulsar sobre “Guardar Cambios”, se realice un envió con todos los nuevos registros, otro con las modificaciones y otro con los borrados.

En el siguiente fragmento de código muestra un ejemplo de edición en modo Batch

    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Grid Remoto OData - Edicion InCell BATCH</h3>
        </div>
        <div class="panel-body">
            <div kendo-grid k-options="opcionesGridRemotoClientesIncellBatch"></div>
        </div>
    </div>
    //INCELL EDIT BTACH
    $scope.origenDatosIncellBatch = new kendo.data.DataSource({
                    type: "odata-v4",
                    transport: {
                        read: {
                            url: "http://localhost:49952/Clientes",
                            dataType: "json"
                        },
                        create: {
                            type: "POST",
                            url: "http://localhost:49952/Clientes/ServicioODataFacturacion.ActualizarClientes",
                            contentType: "application/json",
                            dataType: "json"
                        },
                        update: {
                            url: "http://localhost:49952/Clientes/ServicioODataFacturacion.ActualizarClientes",
                            type: "POST",
                            contentType: "application/json",
                            dataType: "json"
                        },
                        destroy: {
                            url: "http://localhost:49952/Clientes/ServicioODataFacturacion.BorrarClientes",
                            type: "POST",
                            contentType: "application/json",
                            dataType: "json"
                        }
                    },
                    schema: {
                        data: function (data) {
                            return data.value;
                        },
                        total: function (data) {
                            return data['@@odata.count'];
                        },
                        model: {
                            id: "Id",
                            fields: {
                                Id: { editable: false, type: "number" },
                                Nombre: { type: "string" },
                                Mail: { type: "string" },
                                Telefono: { type: "string" },
                                Direccion: { type: "string" },
                                Tipo: { type: "string" }
                            }
                        }
                    },
                    batch:true,
                    pageSize: 5,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true,
                    error: function (e) {
                        alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                    }
                });
                $scope.opcionesGridRemotoClientesIncellBatch = {
                    groupable: {
                        messages: {
                            empty: "Para agrupar el listado, arrastre la cabecera de la columna aquí"
                        }
                    },
                    filterable: true,
                    pageable: {
                        pageSize: 5,
                        previousNext: true,
                        numeric: true,
                        buttonCount: 5,
                        input: true,
                        pageSizes: [5, 10, 15, 20, 30, "all"],
                        refresh: true,
                        info: true,
                        messages: {
                            display: "Mostrando {0}-{1} de {2} clientes",
                            empty: "No hay datos de clientes",
                            page: "Introduzca Página",
                            of: " de {0}",
                            itemsPerPage: "registros por página",
                            first: "Primera",
                            last: "Última",
                            next: "Siguiente",
                            previous: "Anterior",
                            refresh: "Actualizar",
                            morePages: "Más paginas"
                        }
                    },
                    sortable: {
                        allowUnsort: true,
                        mode: "multiple"
                    },
                    columnMenu: {
                        columns: true,
                        filterable: true,
                        sortable: true
                    },
                    columns: [
                        { field: "Nombre", title: "Nombre Completo" },
                        { field: "Telefono", title: "Telefono" },
                        {
                            command: [
                                {
                                    name: "destroy",
                                    text: "Borrar"
                                }
                            ],
                            title: "&nbsp;",
                            width: "250px"
                        }
                    ],

                    dataSource: $scope.origenDatosIncellBatch,
                    editable: {
                        createAt: "bottom",
                        mode: "incell"
                    },
                    toolbar: [
                        {
                            name: "create",
                            text: "Nuevo Cliente"
                        },
                        {
                            name: "save",
                            text: "Guardar Cambios"
                        },
                        {
                            name: "cancel",
                            text: "Cancelar cambios"
                        }
                    ]
    };

En el fragmento de código anterior hemos introducido los siguientes cambios:

  • Establecemos la propiedad Batch a true.
  • En el objeto Transport del DataSource, las propiedades “create” y “update” apuntan al action de OData-V4 llamado ServicioODataFacturacion.ActualizarClientes.
  • En el objeto Transport del DataSource, la propiedad “destroy” apunta al action de OData-V4 llamado ServicioODataFacturacion.BorrarClientes.

Las acciones en OData son necesarias para este caso, ya que son operaciones que trabajan sobre un objeto completo (en nuestro caso, un array de entidades de tipo Cliente), permiten trabajar con varias entidades en la misma petición, usar transacciones….

Nota: En un artículo anterior ya vimos cómo crear acciones y funciones de OData,

Antes de testear el ejemplo es necesario, modificar el servicio OData para añadir el código de las 2 actions de OData.

1. Modificar WebApiConfig.cs, para añadir

            builder.EntityType<Cliente>().Collection
                .Action("ActualizarClientes")
                .Returns<bool>()
                .CollectionParameter<Cliente>("models");

            builder.EntityType<Cliente>().Collection
                .Action("BorrarClientes")
                .Returns<bool>()
                .CollectionParameter<Cliente>("models");

Antes de la llamada a “MapODataServiceRoute”.
2. Modificar “ClientesController.cs”, para añadir los 2 metodos

        [HttpPost]
        public async Task<IHttpActionResult> ActualizarClientes(ODataActionParameters parameters)
        {
            Cliente clienteOriginal = null;

            if (parameters != null)
            {
                var clientes = parameters["models"] as IEnumerable<Cliente>;
                if ((clientes != null) && (clientes.Count() > 0))
                {
                    foreach (Cliente cliente in clientes)
                    {
                        if (cliente.Id == 0)
                        {
                            db.Clientes.Add(cliente);
                        }
                        else
                        {
                            clienteOriginal = await db.Clientes.FindAsync(cliente.Id);

                            if (clienteOriginal != null)
                            {
                                db.Entry(clienteOriginal).CurrentValues.SetValues(cliente);
                                db.Entry(clienteOriginal).State = EntityState.Modified;
                            }
                        }

                    }
                    await db.SaveChangesAsync();

                    return Ok(true);
                }
                else
                {
                    return Ok(false);
                }
            }
            else
            {
                return Ok(false);
            }

        }

        [HttpPost]
        public async Task<IHttpActionResult> BorrarClientes(ODataActionParameters parameters)
        {
            if (parameters != null)
            {
                var clientes = parameters["models"] as IEnumerable<Cliente>;
                foreach (Cliente cliente in clientes)
                {
                    db.Clientes.Remove(cliente);
                }
                await db.SaveChangesAsync();
                return Ok(true);
            }
            else
            {
                return Ok(false);
            }            
        }

Por simplicidad, el método ActualizarClientes es usado tanto para los nuevos registros (aquellos cuyo campo Id=0), como para los modificados.

Si ahora probamos el código anterior

EdicionInCellBatch

Comprobamos que, al guardar los cambios se realizan un máximo de 3 peticiones:

  • Si hay nuevos clientes, se envía un array con los nuevos clientes
  • Si hay clientes modificados, se envía un array con los clientes modificados
  • Si hay clientes borrados, se envía un array con los clientes borrados

Nota: con la versión actual de Telerik (2016.2.504), hemos detectado que el proceso de borrado no funciona correctamente, ya que, aunque la petición se realiza contra el servicio OData, está viene vacía y sin datos. Hemos contacto con el equipo de soporte para que revisen la incidencia, y en cuanto quede resuelta actualizaremos este post e indicaremos la versión que corrigen el problema.

A continuación, os dejamos los enlaces del proyecto kendo y del servicio odata.

1. Proyecto Kendo grid.
2. Servicio web OData V4 WebApi 2