[Kendo UI] Funcionamiento del Grid – Parte 3 – Maestro Detalle

Vamos a utilizar el control Kendo-UI Grid de Telerik para ver el procedimiento a seguir para mostrar múltiples Kendo-Grid de forma jerarquica. Por ejemplo, supongamos que tenemos un grid de clientes y para cada cliente queremos mostrar sus facturas.

En un artículo anterior vimos cómo trabajar con el Kendo-UI Grid enlazado a un servicio OData.

Vamos a usar la plantilla Kendo UI ASP.NET MVC 5 Application junto con el framework AngularJS 1.

Lo primero es preparar el proyecto del servicio OData. En artículos anteriores ya vimos cómo crear un nuevo servicio OData, establecer relaciones entre entidades, añadir funciones/acciones.

En el siguiente enlace podéis descargar el proyecto del servicio web OData el cual incluye las entidades:

  • Clientes
  • Facturas
  • LineasFactura
  • FormasPago
  • Articulos
  • Incidencias
  • TiposCliente

En la siguiente imagen se puede ver el diagrama de relaciones entre entidades

DiagramaBaseDatos

Una vez descargado el proyecto, hay que ejecutarlo para tomar nota de la url. En nuestro caso es http://localhost:49952/

A continuación, vamos a crear el proyecto siguiendo los siguientes pasos:

  • Creamos un nuevo proyecto usando la plantilla de KendoUI y AngularJS.
  • 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 Anidados</p>
            </footer>
        </div>
    
        @RenderSection("scripts", required: false)
    </body>
    </html>
    
  • 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="appGridAnidados" ng-controller="ctrlGridAnidados">
    
    </div>
    
    <script>
        angular.module("appGridAnidados", ["kendo.directives"])
                .controller("ctrlGridAnidados", 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.

En primer lugar, vamos a añadir el Grid de clientes

@{
    ViewBag.Title = "Index";
}
<div ng-app="appGridAnidados" ng-controller="ctrlGridAnidados">
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Grid Remoto OData - Grid Anidado</h3>
        </div>
        <div class="panel-body">
            <div kendo-grid k-options="opcionesGridClientes">
            </div>
        </div>
    </div>
</div>

<script>
    angular.module("appGridAnidados", ["kendo.directives"])
            .controller("ctrlGridAnidados", function ($scope, $http) {
                $scope.origenDatosClientes = new kendo.data.DataSource({
                    type: "odata-v4",
                    transport: {
                        read: {
                            url: "http://localhost:49952/Clientes?$expand=TipoCliente",
                            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" },
                                TipoClienteId: { type: "number" },
                                TipoCliente: {
                                    defaultValue: { Id: 0, Nombre: "" },
                                    fields: {
                                        Id: { type: "number" },
                                        Nombre: { type: "string" }
                                    }
                                }
                            }
                        }
                    },
                    pageSize: 5,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true,
                    error: function (e) {
                        alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                    }
                });

                $scope.opcionesComboTipoCliente = {
                    dataSource: new kendo.data.DataSource({
                        type: "odata-v4",
                        transport: {
                            read: {
                                url: "http://localhost:49952/TiposCliente",
                            }
                        }
                    }),
                    dataTextField: "Nombre",
                    dataValueField: "Id"
                }
                $scope.comboBoxTipoCliente = function (container, options) {
                    var editor = $('<select kendo-combo-box k-options="opcionesComboTipoCliente" data-bind="value:' + options.field + '"/>')
                    .appendTo(container);
                }
                $scope.opcionesGridClientes = {
                    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: "TipoClienteId", title: "Tipo", editor: $scope.comboBoxTipoCliente, template: "#=TipoCliente.Nombre#" },
                        {
                            command: [
                                {
                                    name: "edit",
                                    text: { edit: "Editar", cancel: "Cancelar Edición", update: "Grabar Cambios" }
                                },
                                {
                                    name: "destroy",
                                    text: "Borrar"
                                }
                            ],
                            title: "&nbsp;",
                            width: "250px"
                        }
                    ],
                    dataSource: $scope.origenDatosClientes,
                    editable: {
                        createAt: "bottom",
                        mode: "inline"
                    },
                    toolbar: [
                        {
                            name: "create",
                            text: "Nuevo Cliente"
                        }

                    ]
                };

                $scope.recuperarClientes = function () {
                    $scope.opcionesGridClientes.dataSource.read();
                };
            }).run(function () {
                kendo.culture("es-ES");
            })
</script>

En un artículo anterior ya explicamos en detalle la edición de grid.

Para este ejemplo, hemos usado la edición “inline”, donde los puntos a destacar son:

  • Usando el parámetro OData “$expand”, hemos configurado la operación “read” del DataSource para que recupera los registros de clientes junto con la entidad asociada “TipoCliente”.
  • La propiedad “schema/model” del Datasource, queda como
                         model: {
                                id: "Id",
                                fields: {
                                    Id: { editable: false, type: "number" },
                                    Nombre: { type: "string" },
                                    Mail: { type: "string" },
                                    Telefono: { type: "string" },
                                    Direccion: { type: "string" },
                                    TipoClienteId: { type: "number" },
                                    TipoCliente: {
                                        defaultValue: { Id: 0, Nombre: "" },
                                        fields: {
                                            Id: { type: "number" },
                                            Nombre: { type: "string" }
                                        }
                                    }
                                }
                            }
    

    Nota: esta información adicional, es necesaria para mostrar la columna “TipoClienteId” como un ComboBox, en lugar del valor número que tenemos en la base de datos.

  • La definición de columnas queda como
                        columns: [
                            { field: "Nombre", title: "Nombre Completo" },
                            { field: "Telefono", title: "Telefono" },
                            { field: "TipoClienteId", title: "Tipo", editor: $scope.comboBoxTipoCliente, template: "#=TipoCliente.Nombre#" },
                            {
                                command: [
                                    {
                                        name: "edit",
                                        text: { edit: "Editar", cancel: "Cancelar Edición", update: "Grabar Cambios" }
                                    },
                                    {
                                        name: "destroy",
                                        text:"Borrar"
                                    }
                                ],
                                title: "&nbsp;",
                                width: "250px"
                            }
                        ]
    

    donde hemos usado la propiedad “editor” y “template” para personalizar el contenido de la celda del grid.

En la siguiente imagen se puede ver el aspecto del grid de clientes
GridClientes

A continuación, vamos a añadir un grid anidado que muestre las facturas de cada cliente. Los pasos a seguir son:

  • Modificamos la sección de detalle (DetailTemplate) del grid Clientes, para incluir el grid kendo de facturas.
                <div kendo-grid k-options="opcionesGridClientes">
                    <div k-detail-template>
                        <div kendo-grid k-options="opcionesGridFacturas(dataItem)">
    
                        </div>
                    </div>
                </div>
    

    Nota: Observa como en este caso, al hacer referencia a las opciones de configuración, hemos pasado como parámetro el objeto “dataItem”, el cual hace referencia a la línea correspondiente del grid de clientes.

  • Modificamos el $scope de angular, para añadir las opciones de configuración del grid de facturas
                    //GRID ANIDADO DE FACTURAS
                    $scope.opcionesComboFormaPago = {
                        dataSource: new kendo.data.DataSource({
                            type: "odata-v4",
                            transport: {
                                read: {
                                    url: "http://localhost:49952/FormasPago",
                                }
                            }
                        }),
                        dataTextField: "Nombre",
                        dataValueField: "Id"
                    }
                    $scope.comboBoxFormaPago = function (container, options) {
                        var editor = $('<select kendo-combo-box k-options="opcionesComboFormaPago" data-bind="value:' + options.field + '"/>')
                        .appendTo(container);
                    }
    
                    $scope.opcionesGridFacturas = function (dataItem) {
                        return {
                            dataSource: {
                                type: "odata-v4",
                                transport: {
                                    read: {
                                        url: "http://localhost:49952/Facturas?$expand=FormaPago",
                                        dataType: "json"
                                    },
                                    create: {
                                        type: "POST",
                                        url: "http://localhost:49952/Facturas",
                                        contentType: "application/json",
                                        dataType: "json",
                                        complete: function (e) {
                                            $scope.recuperarFacturas();
                                        }
                                    },
                                    update: {
                                        url: function (data) {
                                            return "http://localhost:49952/Facturas" + "(" + data.Numero + ")";
                                        },
                                        type: "PUT",
                                        contentType: "application/json",
                                        dataType: "json"
                                    },
                                    destroy: {
                                        url: function (data) {
                                            return "http://localhost:49952/Facturas" + "(" + data.Numero + ")";
                                        },
                                        dataType: "json",
                                        type: "DELETE"
                                    },
                                    parameterMap: function (datos, type) {
                                        var linea,
                                            datosMapeados;
    
                                        datos = datos || {};
                                        type = type || "read";
                                        datosMapeados = {};
    
                                        if ((type == "create") || (type == "update")) {
                                            for (campo in datos) {
                                                if (campo == "FormaPago") {
                                                    datosMapeados[campo] = null;
                                                }
                                                else if (campo == "ClienteId") {
                                                    if (type == "create") {
                                                        datosMapeados[campo] = dataItem.Id;
                                                    } else {
                                                        datosMapeados[campo] = datos[campo];
                                                    }
                                                } else {
                                                    datosMapeados[campo] = datos[campo];
                                                }
                                            }
    
                                            return kendo.stringify(datosMapeados);
                                        }
                                        if (type == "read") {
                                            datosMapeados.$format = "json";
                                            datosMapeados.$filter = "ClienteId eq " + dataItem.Id;
                                            for (campo in datos) {
                                                datosMapeados[campo] = datos[campo];
                                            }
                                            return datosMapeados;
                                        }
                                    }
                                },
                                schema: {
                                    data: function (data) {
                                        return data.value;
                                    },
                                    total: function (data) {
                                        return data['@@odata.count'];
                                    },
                                    model: {
                                        id: "Numero",
                                        fields: {
                                            Numero: { type: "number" },
                                            Fecha: { type: "date" },
                                            ImporteBase: { type: "number" },
                                            ImporteIva: { type: "number" },
                                            Importe: { type: "number" },
                                            ClienteId: { type: "number" },
                                            FormaPagoId: { type: "number" },
                                            FormaPago: {
                                                defaultValue: { Id: 0, Nombre: "" },
                                                fields: {
                                                    Id: { type: "number" },
                                                    Nombre: { type: "string" }
                                                }
                                            }
                                        }
                                    }
                                },
                                serverPaging: true,
                                serverSorting: true,
                                serverFiltering: true,
                                pageSize: 5,
                                error: function (e) {
                                    alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                                }
                            },
                            scrollable: false,
                            sortable: true,
                            pageable: true,
                            columns: [
                                { field: "Numero", title: "Numero" },
                                { field: "Fecha", title: "Fecha", format: "{0: dd/MM/yyyy}" },
                                { field: "Importe", title: "Importe Iva Incluido", format: "{0:c}" },
                                { field: "FormaPagoId", title: "Forma Pago", editor: $scope.comboBoxFormaPago, template: "#=FormaPago.Nombre#" },
                                {
                                    command: [
                                        {
                                            name: "edit",
                                            text: { edit: "Editar", cancel: "Cancelar Edición", update: "Grabar Cambios" }
                                        },
                                        {
                                            name: "destroy",
                                            text: "Borrar"
                                        }
                                    ]
                                }
                            ],
                            editable: {
                                createAt: "bottom",
                                mode: "inline"
                            },
                            toolbar: [
                                {
                                    name: "create",
                                    text: "Nueva Factura"
                                }
    
                            ]
                        };
                    };
    
                    $scope.recuperarFacturas = function () {
                        $scope.opcionesGridFacturas.dataSource.read();
                    };
    

    Este ejemplo, sigue el mismo patrón que el grid de clientes, pero incluye las siguientes modificaciones:

  • La función opcionesGridFacturas incluye el parámetro “dataItem”, para filtrar las facturas en base al cliente.
  • En el objeto Datasource, hemos definido la propiedad “parameterMap” (nos permite personalizar el envío de datos al servicio), donde realizamos las siguientes operaciones:
    • Al hacer una petición “read”, se incluye el parámetro OData filter para filtrar las facturas en base al número de cliente
    • Al hacer una petición “create” o “update”, recorremos todos los campos de la entidad factura, de forma que, si el nombre del campo es “FormaPago”, asignamos un null (Esto lo hacemos para no enviar la entidad FormaPago, sino solo los datos de la entidad cliente). Si el campo es “ClienteId” y estamos creando una nueva factura, entonces el asignamos el valor del campo Id del objeto “dataItem” de clientes. Para el resto de campos, reasignamos el valor de cada campo (Esto lo hacemos porque por defecto, el grid de kendo envía todos los campos de la entidad (CREATE, UPDATE) como string)

      En el siguiente fragmento JSON, podemos ver un ejemplo del envió por defecto.

      {"Numero":2,"Fecha":"2016-01-01T00:00:00+01:00","ImporteBase":"200.00","ImporteIva":"42.00","Importe":"242.00","ClienteId":1,"FormaPagoId":2}
      

      Este JSON genera un error de conversión en el servicio OData, debido a que las propiedades de importe son de tipo “decimal” y, no puede convertir valores string a edm.decimal.

      Con el código del fragmento de código anterior, los datos enviados quedan como

      {"Numero":2,"Fecha":"2016-01-01T00:00:00+01:00","ImporteBase":200.00,"ImporteIva":42.00,"Importe":242.00,"ClienteId":1,"FormaPagoId":2}
      

      Nota: Observa como los campos de tipo “number”, no tienen comillas en su valor.

Nota: Otra forma de evitar este error, consiste en establecer a true la variable OData IEEE754Compatible a true. Si la activamos, nuestro servicio OData, espera que todos los valores estén entre comillas (si alguno llega sin comillas, saltará un error en el servicio OData).

En el siguiente fragmento de código podemos ver los cambios necesarios

      dataSource: {
          type: "odata-v4",
          transport: {
              read: {
                  url: "http://localhost:49952/Facturas?$expand=FormaPago",
                  dataType: "json"
              },
              create: {
                  type: "POST",
                  url: "http://localhost:49952/Facturas",
                  contentType: contentType: "application/json;IEEE754Compatible=true",
                  dataType: "json",
                  complete: function (e) {
                      $scope.recuperarFacturas();
                  }
              },
              update: {
                  url: function (data) {
                      return "http://localhost:49952/Facturas" + "(" + data.Numero + ")";
                  },
                  type: "PUT",
                  contentType: contentType: "application/json;IEEE754Compatible=true",
                  dataType: "json"
              },
              destroy: {
                  url: function (data) {
                      return "http://localhost:49952/Facturas" + "(" + data.Numero + ")";
                  },
                  dataType: "json",
                  type: "DELETE"
              }
          },
          schema: {
              data: function (data) {
                  return data.value;
              },
              total: function (data) {
                  return data['@@odata.count'];
              },
              model: {
                  id: "Numero",
                  fields: {
                      Numero: { type: "number" },
                      Fecha: { type: "date" },
                      ImporteBase: { type: "number" },
                      ImporteIva: { type: "number" },
                      Importe: { type: "number" },
                      ClienteId: { type: "number" },
                      FormaPagoId: { type: "number" },
                      FormaPago: {
                          defaultValue: { Id: 0, Nombre: "" },
                          fields: {
                              Id: { type: "number" },
                              Nombre: { type: "string" }
                          }
                      }
                  }
              }
          },
          serverPaging: true,
          serverSorting: true,
          serverFiltering: true,
          pageSize: 5,
          filter: { field: "ClienteId", operator: "eq", value: dataItem.Id },
          error: function (e) {
              alert("Status: " + e.status + "; Error message: " + e.errorThrown);
          }
      } 

Observa como en este ejemplo, hemos establecido la propiedad cabecera HTTP ContentType como “application/json;IEEE754Compatible=true”, hemos quitado la propiedad “parameterMap” de nuestro servicio y por último, hemos incluido el filtro por el campo “ClienteId” en la propiedad “filter” del “DataSource”.

Si ahora probamos a ejecutar el código, el aspecto del grid maestro-detalle de clientes-facturas

GridClientes_Facturas

En este punto, tenemos un Grid tipo Maestro-Detalle con un nivel de profundidad, pero con Kendo Grid podemos conseguir hasta N niveles. El siguiente fragmento de código mostramos las líneas asociadas a cada factura.

        </div>
        <div class="panel-body">
            <div kendo-grid k-options="opcionesGridClientes">
                <div k-detail-template>
                    <div kendo-grid k-options="opcionesGridFacturas(dataItem)">
                        <div k-detail-template>
                            <div kendo-grid k-options="opcionesGridLineasFactura(dataItem)"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
…
…
        //GRID DE LINEAS DE FACTURA
        $scope.opcionesGridLineasFactura = function (dataItemLineaFactura) {
            return {
                dataSource: {
                    type: "odata-v4",
                    transport: {
                        read: {
                            url: "http://localhost:49952/LineasFactura?$expand=Articulo",
                            dataType: "json"
                        },
                        create: {
                            type: "POST",
                            url: "http://localhost:49952/LineasFactura",
                            contentType: "application/json",
                            dataType: "json",
                            complete: function (e) {
                                $scope.recuperarLineasFactura();
                            }
                        },
                        update: {
                            url: function (data) {
                                return "http://localhost:49952/LineasFactura" + "(" + data.Id + ")";
                            },
                            type: "PUT",
                            contentType: "application/json",
                            dataType: "json"
                        },
                        destroy: {
                            url: function (data) {
                                return "http://localhost:49952/LineasFactura" + "(" + data.Id + ")";
                            },
                            dataType: "json",
                            type: "DELETE"
                        },                                parameterMap: function (datos, type) {
                            var linea,
                                datosMapeados;

                            datos = datos || {};
                            type = type || "read";
                            datosMapeados = {};

                            if ((type == "create") || (type == "update")) {
                                for (campo in datos) {
                                    if (campo == "Articulo") {
                                        datosMapeados[campo] = null;
                                    }
                                    else if (campo == "FacturaNumero") {
                                        if (type == "create") {
                                            datosMapeados[campo] = dataItemLineaFactura.Numero;
                                        } else {
                                            datosMapeados[campo] = datos[campo];
                                        }
                                    } else {
                                        datosMapeados[campo] = datos[campo];
                                    }
                                }
                                return kendo.stringify(datosMapeados);
                            }
                            if (type == "read") {
                                datosMapeados.$format = "json";
                                datosMapeados.$filter = "FacturaNumero eq " + dataItemLineaFactura.Numero;
                                for (campo in datos) {
                                    datosMapeados[campo] = datos[campo];
                                }
                                return datosMapeados;
                            }
                        }
                    },
                    schema: {
                        data: function (data) {
                            return data.value;
                        },
                        total: function (data) {
                            return data['@@odata.count'];
                        },
                        model: {
                            id: "Id",
                            fields: {
                                Id: { editable: false, type: "number" },
                                Fecha: { type: "date" },
                                Unidades: { type: "number" },
                                Importe: { type: "number" },
                                ArticuloId: { type: "number" },
                                FacturaNumero: { type: "number" },
                                Articulo: {
                                    defaultValue: { Id: 0, Nombre: "", Precio: 0 },
                                    fields: {
                                        Id: { type: "number" },
                                        Nombre: { type: "string" },
                                        Precio: { type: "number" }
                                    }
                                }
                            }
                        }
                    },
                    serverPaging: true,
                    serverSorting: true,
                    serverFiltering: true,
                    pageSize: 5,
                    error: function (e) {
                        alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                    }
                },
                scrollable: false,
                sortable: true,
                pageable: true,
                columns: [
                    { field: "Fecha", title: "Fecha", format: "{0: dd/MM/yyyy}" },
                    { field: "Unidades", title: "Unidades" },
                    { field: "Importe", title: "Importe Iva Incluido", format: "{0:c}" },
                    { field: "ArticuloId", title: "Articulo", editor: $scope.comboBoxArticulo, template: "#=Articulo.Nombre#" },
                    {
                        command: [
                            {
                                name: "edit",
                                text: { edit: "Editar", cancel: "Cancelar Edición", update: "Grabar Cambios" }
                            },
                            {
                                name: "destroy",
                                text: "Borrar"
                            }
                        ]
                    }
                ],
                editable: {
                    createAt: "bottom",
                    mode: "inline"
                },
                toolbar: [
                    {
                        name: "create",
                        text: "Nueva Linea Factura"
                    }

                ]
            };
        };

        $scope.opcionesComboArticulo = {
            dataSource: new kendo.data.DataSource({
                type: "odata-v4",
                transport: {
                    read: {
                        url: "http://localhost:49952/Articulos",
                    }
                }
            }),
            dataTextField: "Nombre",
            dataValueField: "Id"
        }
        $scope.comboBoxArticulo = function (container, options) {
            var editor = $('<select kendo-combo-box k-options="opcionesComboArticulo" data-bind="value:' + options.field + '"/>')
            .appendTo(container);
        }
        $scope.recuperarLineasFactura = function () {
            $scope.opcionesGridLineasFactura.dataSource.read();
        };

Como se puede observar el código es muy similar al del grid de facturas.

En la siguiente imagen se puede ver el aspecto del grid con 2 niveles de jerarquia.

GridClientes_Facturas_Lineas

Hasta ahora hemos incluido un grid de cada nivel de la jerarquía, pero esto no es obligatorio. Es decir, dentro de la sección de detalles podemos incluir un control de tipo “TabStrip”, y dentro el incluir Grids o cualquier otro control.

En el siguiente ejemplo, vemos como añadir un “Tabstrip” en el panel de detalle de clientes, para mostrar las facturas y las incidencias, asociadas a cada cliente.

        <div class="panel-body">
            <div kendo-grid k-options="opcionesGridClientes">
                <div k-detail-template>
                    <kendo-tabstrip>
                        <ul>
                            <li class="k-state-active">Facturas</li>
                            <li>Incidencias</li>
                        </ul>
                        <div kendo-grid k-options="opcionesGridFacturas(dataItem)">
                            <div k-detail-template>
                                <div kendo-grid k-options="opcionesGridLineasFactura(dataItem)"></div>
                            </div>
                        </div>
                        <div kendo-grid k-options="opcionesGridIncidencias(dataItem)"></div>
                    </kendo-tabstrip>
                </div>
            </div>
        </div>
…
…
                //GRID DE INCIDENCIAS
                $scope.opcionesGridIncidencias = function (dataItemIncidencias) {
                    return {
                        dataSource: {
                            type: "odata-v4",
                            transport: {
                                read: {
                                    url: "http://localhost:49952/Incidencias",
                                    dataType: "json"
                                },
                                create: {
                                    type: "POST",
                                    url: "http://localhost:49952/Incidencias",
                                    contentType: "application/json",
                                    dataType: "json",
                                    complete: function (e) {
                                        $scope.recuperarIncidencias();
                                    }
                                },
                                update: {
                                    url: function (data) {
                                        return "http://localhost:49952/Incidencias" + "(" + data.Id + ")";
                                    },
                                    type: "PUT",
                                    contentType: "application/json",
                                    dataType: "json"
                                },
                                destroy: {
                                    url: function (data) {
                                        return "http://localhost:49952/Incidencias" + "(" + data.Id + ")";
                                    },
                                    dataType: "json",
                                    type: "DELETE"
                                },
                                parameterMap: function (datos, type) {
                                    var linea,
                                        datosMapeados;

                                    datos = datos || {};
                                    type = type || "read";
                                    datosMapeados = {};

                                    if ((type == "create") || (type == "update")) {
                                        for (campo in datos) {
                                            if (campo == "ClienteId") {
                                                if (type == "create") {
                                                    datosMapeados[campo] = dataItemIncidencias.Id;
                                                } else {
                                                    datosMapeados[campo] = datos[campo];
                                                }
                                            } else {
                                                datosMapeados[campo] = datos[campo];
                                            }
                                        }

                                        return kendo.stringify(datosMapeados);
                                    }
                                    if (type == "read") {
                                        datosMapeados.$format = "json";
                                        datosMapeados.$filter = "ClienteId eq " + dataItemIncidencias.Id;
                                        for (campo in datos) {
                                            datosMapeados[campo] = datos[campo];
                                        }
                                        return datosMapeados;
                                    }
                                }
                            },
                            schema: {
                                data: function (data) {
                                    return data.value;
                                },
                                total: function (data) {
                                    return data['@@odata.count'];
                                },
                                model: {
                                    id: "Id",
                                    fields: {
                                        Id: { editable: false, type: "number" },
                                        Fecha: { type: "date" },
                                        Descripcion: { type: "string" },
                                        Finalizada: { type: "boolean" },
                                        FechaFinalizacion: { type: "date" },
                                        ClienteId: { type: "number" }
                                    }
                                }
                            },
                            serverPaging: true,
                            serverSorting: true,
                            serverFiltering: true,
                            pageSize: 5,
                            error: function (e) {
                                alert("Status: " + e.status + "; Error message: " + e.errorThrown);
                            }
                        },
                        scrollable: false,
                        sortable: true,
                        pageable: true,
                        columns: [
                            { field: "Fecha", title: "Fecha", format: "{0: dd/MM/yyyy}" },
                            { field: "Descripcion", title: "Descripcion" },
                            { field: "Finalizada", title: "Finalizada", template: '<input type="checkbox" #= Finalizada ? \'checked="checked"\' : "" # class="chkbx" />' },
                            {
                                command: [
                                    {
                                        name: "edit",
                                        text: { edit: "Editar", cancel: "Cancelar Edición", update: "Grabar Cambios" }
                                    },
                                    {
                                        name: "destroy",
                                        text: "Borrar"
                                    }
                                ]
                            }
                        ],
                        editable: {
                            createAt: "bottom",
                            mode: "inline"
                        },
                        toolbar: [
                            {
                                name: "create",
                                text: "Nueva Incidencia"
                            }

                        ]
                    };
                };

                $scope.recuperarIncidencias = function () {
                    $scope.opcionesGridIncidencias.dataSource.read();
                };

El código del grid de incidencias es muy similar al del grid de Facturas. Lo único que vamos a destacar es que el detalle del grid de clientes es un control de tipo TabStrip con 2 grids.

GridClientes_TabStrip

Para este articulo podeís descargar 2 proyectos VS2015 comprimidos en zip:
1. Servicio OData Web Api
2. Proyecto Kendo Grid con Grids en formato maestro-detalle.