Guía Desarrollo Aplicaciones WEB con HTML 5 Miguel Mejía Agosto 2013 Guía de Desarrollo Aplicaciones Web con Html5 Página 1 de 33 Tabla de Contenido Ciclo de vida de una vista .............................................................................................................. 4 Flujo de la página para la carga y obtención de datos.............................................................. 4 Flujo de la página para una transacción.................................................................................... 5 Descripción del Servidor................................................................................................................ 6 JSON .......................................................................................................................................... 6 JSON vs XML .......................................................................................................................... 6 Newtonsoft.json.................................................................................................................... 6 Framework 3.0 .......................................................................................................................... 7 Token..................................................................................................................................... 7 Security.................................................................................................................................. 7 SecurityPort........................................................................................................................... 7 DBConnector ......................................................................................................................... 8 WEB Api..................................................................................................................................... 8 Restful ................................................................................................................................... 9 Entity Framework ...................................................................................................................... 9 Linq ...................................................................................................................................... 10 Crystal Reports ........................................................................................................................ 10 Descripción del Cliente................................................................................................................ 10 HTML5 ..................................................................................................................................... 11 Vistas Completas: ................................................................................................................ 11 Vistas Parciales: ................................................................................................................... 11 SecurityPort............................................................................................................................. 11 GetMenu ............................................................................................................................. 12 Javascript................................................................................................................................. 13 Jquery: ................................................................................................................................. 13 each: .................................................................................................................................... 13 getJSON ............................................................................................................................... 13 Ajax...................................................................................................................................... 14 $(document).ready(function()) ........................................................................................... 14 Código genérico................................................................................................................... 14 Inyección de código – Kendo UI. ............................................................................................. 19 Globalization ....................................................................................................................... 19 TabStrip ............................................................................................................................... 19 Guía de Desarrollo Aplicaciones Web con Html5 Página 2 de 33 Menu ................................................................................................................................... 20 DataSource .......................................................................................................................... 20 Grid...................................................................................................................................... 22 Forms................................................................................................................................... 27 Reportes .............................................................................................................................. 31 Hojas de Estilo ......................................................................................................................... 32 Plantilla estándar................................................................................................................. 32 Cross Browsing .................................................................................................................... 32 Guía de Desarrollo Aplicaciones Web con Html5 Página 3 de 33 Ciclo de vida de una vista Para tener un control sobre cada una de las vistas de la solución, y sobre todo, para estandarizar el proceso de envío y recepción de datos, de lado del servidor, así como del cliente, se definieron restricciones para garantizar un funcionamiento general sobre cada una de las ventanas del proyecto. - - - - Se definió una Interfaz para obligar a TODOS los controladores RestFul a tener un único método para la consulta de información, así como a validar cada una de peticiones. (IRestService). Para contar con un código que fuese mantenible, la lógica de negocio de cada uno de los Servicios, se encuentra en controladores aparte encargados de gestionar todos los procesos, convirtiendo cada uno de las clases Restful en fachadas encargadas de recibir peticiones y enviar información. Cada uno de los controladores heredara de una clase (BussinesController) para acotar los modos de acceso a este. Sin embargo, dentro del controlador, se podrán tener métodos independientes para mantener una limpieza y control del código. Las vistas únicamente tendrán acceso a los servicios para obtener información. Cada una de las vistas, tiene una representación como Script, encargada de controlar su carga, comportamiento y ejecución dentro del sistema. El uso del DBConnector o de Entity Framework, así como de componentes del Framework 3.0 estará restringido únicamente al controlador, no a los servicios. A continuación se presenta un ejemplo del diagrama para la creación de una ventana. Flujo de la página para la carga y obtención de datos 1. La vista se carga dentro del explorador. 2. La vista importa todos los documentos necesarios para su funcionamiento (Hojas de Estilo, Scripts de Kendo, Script de ejecución). 3. El Script, luego de que la vista está precargada en el navegador, ejecuta gracias al evento $(document).ready() el método LoadForm. Guía de Desarrollo Aplicaciones Web con Html5 Página 4 de 33 4. Se realizan validaciones para determinar si el usuario tiene o no acceso a la vista que solicita. 5. Se ejecuta el método de carga de controles LoadControls 6. Si dentro del método se encuentran llamadas a datos para visualizar, se construye el objeto SecurityPort con la información que se encuentra en memoria donde se le asigna el ObjectId de la página y posteriormente se estructuran los parámetros para enviar al Servicio encargado de la vista. 7. Se realiza el llamado al servicio mediante eventos de kendo o llamadas asíncronas de Jquery. 8. El Servicio recibe la petición con el SecurityPort serializado. Se convierte en un objeto interpretable por el servidor. 9. Se valida dentro del sistema, que el usuario que hace la petición tiene acceso al sistema. En caso de no validarse, se elimina la sesión en el cliente y se saca del sistema. 10. Se verifica que el SecurityPort contenga un parámetro en la colección que corresponda al dataType de donde se extraerá la información dentro del controlador. En caso que no se tenga, se informara al usuario que la petición no tiene un formato adecuado para ejecutarse. 11. Se envían al controlador cada uno de los parámetros que se encuentran dentro del atributo Parameters del securityPort. Esto por si se necesitan en alguna parte del proceso posterior. 12. Luego de la validación del objeto de transporte, aparece un objeto constituido con información, de tipo Security. Este es enviado al controlador bajo las mismas condiciones de los parámetros del SecurityPort. 13. Se ejecuta el método GetData del Controlador, basado en el dataType incluido en el SecurityPort. 14. Luego del proceso de carga y retorno de información, se envía serializada la respuesta para su carga dentro del cliente. 15. Se realizan las llamadas necesarias para obtener todos los datos, cada una independiente, ya que gracias a su tamaño, el tiempo de respuesta es adecuado. 16. Se termina de ejecutar el método LoadForm, y se presenta la página totalmente renderizada. Flujo de la página para una transacción 1. Se realizan las validaciones necesarias en el cliente para la correcta construcción del objeto. 2. Se constituye de la misma manera que para la obtención de datos, el objeto SecurityPort dentro del Script. 3. Se añade serializado el objeto de transacción construido. Esto debido a que JSON es un modelo de comunicación basado en cadenas, y si no se serializa previamente, siempre va a llegar nulo. 4. Se realiza la petición al servidor, teniendo en cuenta que se usara PUT para una edición, POST para agregar y DELETE para eliminar información. 5. En el servidor, se realiza la misma validación que para la obtención de datos, con la diferencia que posteriormente, se realiza un proceso de deserializado del objeto de Guía de Desarrollo Aplicaciones Web con Html5 Página 5 de 33 transacción en su objeto nativo dentro del sistema. Cabe aclarar que para que toda la información pueda ser asignada a cada uno de los parámetros, estos deben tener el mismo nombre tanto en el Script como en el Objeto nativo. 6. Se envía al controlador el objeto, junto al security y a los parámetros transferidos a este, con el fin de ejecutar la lógica necesaria para su trato. 7. Se retorna una respuesta, que en caso de ser positiva, actualiza la vista (donde sea necesario) para mostrar los datos modificados. Descripción del Servidor JSON La definición de JSON (Javascript Object Notation) presenta un modelo universal de comunicación de datos entre un servidor de cualquier lenguaje a Javascript. Mucha de la información que se envía, dependiendo el modelo de comunicación puede representarse como JSON, lo que ayuda a una rápida conversión entre objeto y cadena de texto. JSON vs XML La ventaja que posee JSON sobre XML radica en varios factores, donde uno de los más importantes es el tamaño de la respuesta. Lo que hace que los tiempos de recepción de información sean mejores sobre modelos JSON. Otra importante es la interpretación en WEB, ya que JSON se comporta como un objeto constituido sobre Javascript, haciendo mucho más fácil el acceso a sus propiedades. A continuación se muestra la representación de una cadena JSON que contiene la información de un usuario. [{"PERSONA_ID":"1355","CODIGOBARRAS":"","AREA_ID":1,"AREA":"Administración","CARGO_ID":1,"VA LOR":"Administrador","PERFIL_ID":1,"PERFIL":"Administrador","ACTIVO":true,"NOMBRES":"Ipad","APEL LIDOS":"Miguel","MAIL":"[email protected]","MAIL2":"","TELEFONO1":"3114730781","TELEFON O2":"","LOGIN":"mmobile","PERSONALIZADO1":"","PERSONALIZADO2":""}] Si se quisiera saber el nombre del usuario, bastaría con accesar objeto.NOMBRES, lo cual sería más complejo en XML. Newtonsoft.json Esta librería se encarga de la serialización a formato JSON de los objetos respuesta en el servidor. La necesidad de esta librería contra la que trae .NET por defecto se encontraba en que habían tipos de datos que nativamente no podía serializar, debido a Referencias Circulares, o llamados a si mismo que creaban bucles infinitos. Newtonsoft se encarga de limpiar los objetos y enviar solo lo que se necesite. El método para serializar es: return JsonConvert.SerializeObject(response); Otra de las ventajas que ofrece la librería es la capacidad para deserializar como objetos (mediante cast), y poder continuar con el flujo del código, sin necesidad de funciones complicadas que llevaran el String a una estructura maniobrable en el servidor. El modo para deserializar los objetos es: JsonConvert.DeserializeObject<InventoryTransaction>(port.Parameters.Find(s => s.Key.Equals("InventoryTransaction")).Value)); Guía de Desarrollo Aplicaciones Web con Html5 Página 6 de 33 Donde el parámetro de entrada es el objeto en JSON que se quiera deserializar Framework 3.0 Los cambios realizados en el Framework 3.0 están relacionados a la seguridad y al envió de información principalmente. Uno de los cambios principales que se tiene esta en el Token, o llave de acceso que es el único puente de seguridad entre el servidor y el cliente. Otros como el DBConnector se realizaron para respuestas que pudieran serializarse como JSON para su posterior respuesta al cliente. Token Como Nuevo atributo dentro del Framework, aparece el Token Identifier, encargado de validar si el usuario está o no logueado en el sistema. Esto a razón que se dejan de manejar variables de Sesión (puesto que HTML no las posee) y se empiezan a utilizar componentes de HTML5 como el localStorage (datos validos mientras el navegador no se cierre) o SessionStorage (datos validos solo en la pestaña). Básicamente es un Unique-Identifier que se agrega en una tabla de la Base de datos cuando el usuario inicia Sesión. Y que se invalida si el usuario no ha realizado un movimiento en menos de 20 minutos. Security El objeto de seguridad sigue siendo el mismo, salvo que ahora se crea basado en el token. Este objeto es necesario dentro de muchos de los controladores. Adicionalmente, el método de validación de Sesión requiere un SecurityPort para su funcionamiento. SecurityPort Este nuevo objeto aparece como necesidad de algo que pudiese convertirse en un puente de comunicación estándar entre el cliente y el Servidor. Básicamente el SecurityPort consta del Token, Del objeto que define si tiene o no permiso a lo que está solicitando, y de un diccionario de parámetros que dependiendo su caso, traen información necesaria para la correcta ejecución de un proceso. Comunicación con JavaScript Los llamados a servicios se realizan mediante llamadas asíncronas desde código Javascript, utilizando la librería Jquery. Básicamente, mediante peticiones a los servicios Restful, y con la correcta definición del SecurityPort en JavaScript (tras el login queda en localStorage un objeto con esas estructura serializado), donde luego de una verificación de seguridad, se dará paso a la ejecución del servicio. En caso que falle la comunicación el usuario será notificado con un error o perderá su usuario y deberá realizar el login de nuevo. Validación de Sesión contra el Servidor El método de validación que provee el Framework 3.0 verifica la existencia del token y un último movimiento de menos de 20 minutos dentro de la tabla de usuarios activos en la base de datos. En caso de que sea válido, se le da una vigencia de 20 minutos más desde la petición al usuario y se permite el acceso al sistema. Por otro lado, en caso de que se incumpla el tiempo o que el token no exista, se elimina de localStorage y se redirecciona al login. Cabe aclarar que cada que se inicia sesión, se borra cualquier registro de token invalido dentro de la tabla de usuarios activos. Guía de Desarrollo Aplicaciones Web con Html5 Página 7 de 33 Envió y Recepción de Parametros Como se mencionó anteriormente en el apartado de newtonSoft, desde el servidor se serializa o deserializa el objeto según lo que se necesite. Como se mencionó anteriormente, el SecurityPort contiene una colección de parámetros que ayudan con la ejecución del servicio invocado; dentro de esta colección se pueden almacenar objetos serializados que luego podrán tratarse como objetos nativos del sistema sin ningún inconveniente. DBConnector Frente al desafío que había con la seguridad del sistema, se encontraron inconvenientes referentes a SQL injection sobre los parámetros de entrada a las consultas del conector. Para esto se agregaron métodos de normalización de variables y diccionarios de parámetros, lo cual soluciono el inconveniente. Anteriormente como se mencionó, el DBConnector cuenta con un método que retorna un enumerable, con el objetivo de retornarlo mas fácilmente al cliente. Esto debido a que serializar un dataTable o un dataSet retornaba información innecesaria que podía ensuciar la vista o aumentarle el tamaño a la respuesta. Un ejemplo de cómo utilizarlo se ve en los controladores de inventario. private IEnumerable<object> GetShipmentDocuments() { //Retira parametro de tipo de dato. Parameters.Remove("dataType"); //Prepara parametros string filter = ""; foreach (var item in this.Parameters) { switch (item.Key) { case "FECHAINICIO": filter += " AND INV_DOCUMENTOS.FECHA BETWEEN '" + item.Value + " 00:00:00' AND '" + this.Parameters["FECHAFIN"] + " 23:59:59' "; break; default: filter += " AND INV_DOCUMENTOS." + item.Key + "='" + DBConnector.NormalizeString(item.Value) + "' "; break; } } filter); dbConn.CommandQuery.SqlQuery = String.Format(QryShipmentDocuments, dbConn.CommandQuery.Parameters.Add("@EMPRESA_ID", this.SecurityObject.Company); return dbConn.GetIEnumerable(); } WEB Api El modelo WEB Api, es una interfaz de programación de sistemas de respuesta Request /Response basados en JSON y XML. El ciclo de vida puede explorarse a fondo en el siguiente Vinculo. Guía de Desarrollo Aplicaciones Web con Html5 Página 8 de 33 Restful Es una arquitectura para la definición de servicios que intenta eliminar la complejidad y al mismo tiempo evitar la definición de servicios mediante el uso de un protocolo SOAP. Si bien las aplicaciones actuales están basadas bajo un modelo SOA (Service Oriented Architecture), es necesario definir interfaces que puedan desarrollarse, mantenerse, comunicarse y mantenerse más fácilmente. Si bien el modelo REST omite reglas necesarias para cumplir una arquitectura SOA, el comportamiento que este tiene, así como su enfoque a conectividad, hace que se pueda aplicar dicho diseño. La diferencia que tiene sobre otros protocolos es que se poseen con varios tipos de operaciones para llamados a métodos. Las más utilizadas son GET, POST, PUT y DELETE, que se acoplan al comportamiento de un CRUD dentro de los controladores. Dentro de .NET Framework existen 2 maneras de definir estos métodos. La primera y más fácil es iniciar el nombre del servicio con la operación (ej: getData), lo cual se interpretara como un servicio de tipo GET. La segunda requiere antes de definir el método escribir [HttpPut] la cual definiría un método PUT. Dentro de estos controles, es muy usual (por casos de seguridad) utilizar métodos que necesitan que no sean accesados por el cliente, para esto se utiliza la etiqueta [NonAction] que bloquea el método para un acceso externo. A continuación se puede ver el método GetData, encargado de todas las peticiones GET de un controlador específico. public IEnumerable<Object> GetData(String getData) { SecurityPort port = JsonConvert.DeserializeObject<SecurityPort>(getData); ValidateProfile(port); try { if (!port.Parameters.Exists(p => p.Key == "dataType")) throw new Exception("Paramétro de tipo de consulta no válido"); documentController.Parameters = port.Parameters.ToDictionary(p => p.Key, v => (object)v.Value); documentController.SecurityObject = security; return (IEnumerable<Object>)documentController.GetData(port.Parameters.Find(p => p.Key == "dataType").Value); } catch (Exception ex) { Audit.AddEvent("DocumentRest", EventType.Error, this.security.PersonId, "Error en llamado de método GetData()", ex.Message, String.Empty, String.Empty, this.security.Company); return null; } } Se puede ver como el servicio está desarrollado con mociones comunes de programación. El documentController es el controlador del servicio, el dataType que viene como parámetro permite retornar la información deseada y al ser un IEnumerable<Object> se retorna automáticamente como un objeto serializado de JavaScript. Entity Framework El modelo Entity Framework que provee .NET Framework, define una capa de persistencia y de comunicación entre la base de datos y el servidor, permitiendo el trabajo de información Guía de Desarrollo Aplicaciones Web con Html5 Página 9 de 33 mediante objetos y otorgando herramientas para el uso de este sobre la plataforma. No obstante, transacciones extensas o consultas complejas, se realizan mediante el DBConnector. Del mismo modo, su acceso solamente se puede desde los controladores, con el fin de mantener la arquitectura del sistema. Linq Es un componente de .NET Framework que provee capacidades de consulta a objetos del sistema. En este caso se aplica a los objetos de Entity Framework, con el fin de obtener información puntual sobre estos. A continuación se muestra un ejemplo que retorna una empresa dentro del sistema. FW_EMPRESAS fw_empresas = db.FW_EMPRESAS.Where(f => f.EMPRESA_ID == id).SingleOrDefault(); String image = Convert.ToBase64String(fw_empresas.LOGO); if (fw_empresas == null) return null; return new { EMPRESA_ID = fw_empresas.EMPRESA_ID, IDENTIFICACION = fw_empresas.IDENTIFICACION, RAZONSOCIAL = fw_empresas.RAZONSOCIAL, ABREVIATURA = fw_empresas.ABREVIATURA, CODIGOBARRAS = fw_empresas.CODIGOBARRAS, ACTIVO = fw_empresas.ACTIVO, DIRECCION = fw_empresas.DIRECCION, TELEFONO1 = fw_empresas.TELEFONO1, TELEFONO2 = fw_empresas.TELEFONO2, CONTACTO1 = fw_empresas.CONTACTO1, MAILCONTACTO = fw_empresas.MAILCONTACTO, USUARIOSACTIVOS = fw_empresas.USUARIOSACTIVOS, LOGO = image, FECHA_MODIFICACION = fw_empresas.FECHA_MODIFICACION, PERSONA_MODIFICACION = fw_empresas.PERSONA_MODIFICACION, CIUDAD_ID = fw_empresas.CIUDAD_ID }; Crystal Reports Para este proyecto, se utilizó Crystal Reports como la herramienta para la creación de reportes, sin embargo, para la visualización se empleó un modelo de filtro dinámico por reporte, con el fin de mostrar únicamente lo que se necesita para trabajar sobre un único reporte. Uno de los controladores (MediaController) es el encargado de generar los documentos y retornar los nombres al cliente para su posterior visualización. Descripción del Cliente Las vistas en código WEB puro, permiten un despliegue más rápido dentro del servidor, así como una fuerte ayuda frente a la carga del Servidor, esto debido a que gracias a las tecnologías usadas en este proyecto, el tamaño de envío, así como el de recepción de datos con el servidor, era reducido, lo que creaba conexiones más cortas y mejoraba el comportamiento concurrente de la base de datos. Guía de Desarrollo Aplicaciones Web con Html5 Página 10 de 33 La estructura de cada una de las ventanas del aplicativo, iniciaba con <!doctype html>, lo cual define el archivo como una página HTML5. Seguido de los llamados a código externo, encargados del diseño y definición de los controles a usarse posteriormente. Por último, se importa un documento JavaScript con el mismo nombre del archivo WEB, el cual se encarga del funcionamiento puntual de la página (carga, diligenciamiento, validación, comunicación con el Servidor). HTML5 En el caso de las vistas, se manejaron Vistas completas y Vistas parciales. Las vistas completas constan de páginas .html que poseen todo el código, y que así mismo tienen un comportamiento único dentro del sistema, mientras que las parciales, se pueden replicar mediante variables puntuales, para que tengan un comportamiento semejante dentro de otras vistas. Dichas vistas típicamente son inyectadas dentro de las vistas completas como un componente extra que acompaña la ejecución del sistema. Vistas Completas: Son las ventanas principales del aplicativo, ósea las que van en el menú, a las que se redireccionan según necesidad y las que poseen gran parte del diseño. Las condiciones para el correcto funcionamiento de esta, se basan en los nombres de las capas que se usan, así como en el control de los nombres de las variables, debido a que muchas veces, los errores más comunes son debido a la repetición de variables dentro del Script de Ejecución. Vistas Parciales: Estas vistas, se alimentan de variables del Script de Ejecución, las cuales completan la definición del comportamiento del sistema. Esto, con el fin, como anteriormente se mencionó, de que no haya un replicado de código para funciones comunes. Un ejemplo claro de esto, es el archivo de importado, donde basta con incluir las variables: fileImporttemplateName: nombre de la plantilla. fileImportTitle: Nombre a mostrarse al inicio de la ventana. FileImportXML: Nombre del archivo de configuración XML. Cabe aclarar, que el documento de importación, desde el servidor tiene el mismo comportamiento con cualquier caso. SecurityPort Como se habló anteriormente en el lado del servidor, el securityPort es un objeto de comunicación entre el cliente y los controladores del sistema. El parámetro parameters es uno de los más importantes para el correcto funcionamiento del sistema, ya que es el único objeto de envío y recepción sobre cualquier proceso solicitado desde el cliente. Además de esto, sus demás parámetros, almacenan las credenciales que hacen validas o no la sesión dentro del usuario. De este modo, cualquier cambio que se tenga dentro del cliente, podrá ser detectado y validado automáticamente en el servidor. Guía de Desarrollo Aplicaciones Web con Html5 Página 11 de 33 GetMenu Este método de validación se encarga de generar el menú del sistema, basado en los permisos y el token de seguridad que tenga el usuario. En caso de que el usuario no tenga ese permiso, automáticamente será redireccionado al login, eliminando la credencial en el browser. Aparte de eso, ya que es un método invocado al momento de la carga de la ventana, también es el encargado de construir la cabecera de la ventana y de determinar si se intenta acceder desde un desktop, o un navegador móvil. function getMenu(id) { if (navigator.userAgent.match(/(iphone|ipod|blackberry|android|palm|windows\s+ce|mob ile)/i)) mobileWindow = true; var dato = JSON.parse(localStorage.getItem('user')); if (dato == null) { alert("Su sesión ha caducado, debe ingresar nuevamente."); localStorage.removeItem('user'); window.location = "../Access/Login.html"; return; } dato.ObjectId = id; dato.Parameters = [{ Key: "dataType", Value: "DtHorizontalMenu"}]; /*Llamado AJAX para la obtencion del menu. Basta con llamar el Script y breve*/ $.ajax({ url: "../api/menu/", type: "GET", data: { 'GetData': JSON.stringify(dato) }, success: function (data) { dato = JSON.parse(localStorage.getItem('user')); $("#header").append("<div id='companyName' class='companyName'></div>"); $("#companyName").append("<h1>" + dato.Parameters[2].Value + "</h1>"); $("#companyName").append("<p>Bienvenido " + dato.Parameters[0].Value + "</p>"); $('<div class="menuContainer"><ul id="menu"></ul></div>').insertAfter("#header"); $("#menu").append(data.replace("\\", "")); $("#menu").kendoMenu(); $(".footer").append("<div style='display: block; margin-left: auto; margin-right: auto;text-align:right;'>Versión " + version + " </div>"); $(".footer").append("<div class='companyLogo'><img src='' alt='Insertar logotipo aquí' name='Insert_logo' id='Insert_logo' style='display: block;max-width:311px !important;max-height:70px !important' /></div>"); $(".footer").append("<div class='infotrackLogo'><img src='../images/logo-foot.png' alt='' style='display: block;' /></div>"); var logo = document.getElementById("Insert_logo"); if (localStorage.getItem('logo')) logo.src = "data:image/png;base64," + localStorage.getItem('logo'); }, error: function (jqHRx, error, textError) { switch (jqHRx.status.toString()) { case "401": localStorage.removeItem('user'); Guía de Desarrollo Aplicaciones Web con Html5 Página 12 de 33 } }); } } window.location = "../Access/Login.html"; default: alert("Su sesión ha caducado, debe ingresar nuevamente."); localStorage.removeItem('user'); window.location = "../Access/Login.html"; return; Javascript Dentro de los Scripts del sistema, se cuenta con 4 tipos, cada uno de estos dedicado a partes del comportamiento, pero todos a su vez, conectados entre sí. Jquery: Jquery es una librería OpenSource encargada de simplificar código dentro del sistema, y se comporta como una especie de Framework debido a que muchas de las acciones o tareas que se realizan dentro del sistema, requieren el uso de funciones de Jquery, además del hecho que centraliza todos los procesos. Dentro de esta librería, se utilizaron principalmente 4 funciones (3 de comunicación y una de control). each: Similar a un Foreach, es usada para hacer una iteración sin necesidad de una variable de control, caso necesario para el for. Es muy usual cuando se requiere la iteración de objetos, no solo por su simplicidad, sino porque permite tener una mejor visibilidad sobre el código. $.each(data, function (i, v) { orderStatus.addRow([v.ETAPA, v.CANTIDAD]); orderStatusSource.add({ ETAPA_ID: v.ETAPA_ID, CANTIDAD: v.CANTIDAD, ETAPA: '<a href="javascript:void(0)" onClick="viewDetails(\'ETAPA_ID\',' + v.ETAPA_ID + ')" style="text-decoration: underline;color: #4575D4;font-size: 10px;"><span>' + v.ETAPA + '</span></a>' }); totalOrderStatus += v.CANTIDAD; }); getJSON Utilizado para hacer llamados asíncronos de tipo GET al servidor. Al igual que el each, permite una mejor visualización del código, además necesita parametrizarse menos que la función Ajax. $.getJSON("../api/media/", { token: JSON.stringify(securityPort) }, function (data) { var myPDF = new PDFObject({ url: "../Reports/" + data + ".pdf", pdfOpenParams: { navpanes: 1, view: "FitV", pagemode: "thumbs" } }).embed("pdf"); }); Guía de Desarrollo Aplicaciones Web con Html5 Página 13 de 33 Ajax Función utilizada para llamados al servidor, la cual permite especificar tipos de llamados, variables, si es o no asíncrono, y funciones relacionadas que se ejecutaran al final del llamado (en caso de que este sea asincrónico) $.ajax({ }); async: false, url: "../api/Document", type: "GET", contentType: "application/json; charset=utf-8", dataType: "json", data: { GetData: JSON.stringify(securityPort) }, success: function (data) { result = data[0]; } $(document).ready(function()) Una de las características principales de WEB Forms, era el control que se tenía sobre el ciclo de vida de la página, ya que se contaba con eventos específicos que se ejecutaban en ciertos momentos sobre el renderizado de la vista. En el caso de Javascript, Jquery adiciona estas funciones, las cuales nos dan cierto control sobre lo que se debe o no cargar luego de que el código HTML se encuentra compilado en el navegador WEB. $(document).ready(LoadForm); Código genérico Gracias a los eventos que proporciona Jquery, se logra definir un flujo de ejecución de código para el renderizado de cada una de las vistas y controles que poseerá la página final. En el caso de este proyecto, se definieron 3 funciones principales. Una encargada de la carga del formulario, otra para la carga de controles, y una última enfocada a la carga de datos importantes. LoadForm Método encargado de realizar las validaciones de usuario antes de la carga de información. Esta función valida si el usuario tiene o no acceso a la ventana, además genera automáticamente el menú con las opciones a las que el usuario según su perfil tiene acceso. function LoadForm() { //Carga Menú getMenu(objectPermision); //Carga datos de sesion LoadSesionData(objectPermision); //Inicializa objetos de transacciones LoadObjects(); //Inicializa Controles UI LoadControls(); } LoadControls: Método encargado de la carga de controles, así como de sus datos. Esta función se encarga de convertir el código HTML estático en componentes más robustos dentro de la vista. Guía de Desarrollo Aplicaciones Web con Html5 Página 14 de 33 function LoadControls() { //Carga de controles Kendo UI}; LoadGrid: Esta función es la encargada de cargar el dataSource de una grilla específica en caso de que no se le haya establecido durante su carga (LoadControls). Típicamente esta función se usa en caso de que luego de algún proceso, o por efecto de un timer, se necesite refrescar la tabla sin necesidad de volver a crear los controles. En otros casos, define todo el comportamiento para la carga de la grilla, desde su definición, hasta su proceso de autoRefresh. Generalización de código: Uno de los principales desafíos del proyecto, en vista de que parte de la lógica estaría dentro de Scripts en el cliente, era encontrar un método de que dichos procesos no se convirtieran en código replicado dentro del sistema. Para solucionar esto, se crearon funciones específicas para ciertos procesos. loadWorkflowGrid: Es el método encargado de la carga del historial de flujos. La necesidad de centralizar este método radicaba en que las ventanas que lo solicitaban tenían similitudes, además de que era algo netamente informativo, para lo cual se llegó a lo siguiente: function loadWorkflowGrid(entity, parms, reloadTitle,objectPermission) { var data = JSON.parse(localStorage.getItem('user')); data.ObjectId = objectPermission; data.Parameters = dato1.Parameters.concat(parms); if (entity != null) { if (reloadTitle) $("<h3>Historial de Estados</h3>").appendTo('#workFlowTitle'); $(".workFlowController").show(); $("#workFlowGrid").kendoGrid({ dataSource: { type: "json", transport: { read: "../api/document/?getData=" + JSON.stringify(data) }, pageSize: 5, }, sortable: true, pageable: { pageSizes: true, messages: { display: "{0} - {1} de {2} Estados", empty: "No se registran Estados.", itemsPerPage: "Estados por p&aacute;gina" } }, editable: false, columns: [ { field: "FECHA", title: "Fecha", width: "15%" }, { field: "ETAPA_INICIAL", title: "Etapa Inicial", width: "15%" }, { field: "ETAPA_FINAL", title: "Etapa Final", width: "15%" }, { field: "PERSONA", title: "Persona", width: "20%" }, { field: "OBSERVACIONES", title: "Observaciones", width: "35%" }, ] }); } else { $(".workFlowController").hide(); Guía de Desarrollo Aplicaciones Web con Html5 Página 15 de 33 } } Como se puede ver, el código se encarga de inyectar el panel completamente, basado en si tiene o no permiso para solicitarlo en la ventana (Ver perfiles de usuario). Para centralizar el método, se enviaban una serie de parámetros que se encargaban de la correcta ejecución de este del lado del servidor. Un ejemplo del uso del método seria: loadWorkflowGrid(t, [{ Key: "FLUJO_ID", Value: t.FLUJO_ID.FLUJO_ID }, { Key: "DOC_ID", Value: t.DOC_ID }, { Key: "dataType", Value: "DtWorkflow" }], addTabs, objectPermision); Donde los parámetros enviados, permiten obtener el historial de flujo para un documento específico. SendData: Esta función es utilizada para el envío de la mayoría de los objetos al servidor. Permite la ejecución de llamados GET, POST, PUT y DELETE, con el fin de evitar al máximo la replicación de información, así como de dar un único punto de conexión con la mayoría de los controladores importantes del sistema. function sendData(entity, callType, message, controller, ObjectId, key) { var result; var data = JSON.parse(localStorage.getItem('user')); data.ObjectId = ObjectId; data.Parameters.push({ Key: key, Value: JSON.stringify(entity) }); $.ajax({ async: false, url: "../api/" + controller + "/", type: callType, contentType: "application/json; charset=utf-8", dataType: "json", data: JSON.stringify(data), success: function (data) { $("#Message").text(message); result = data; try { OnSucess(data); } catch (e) { } }, error: function (jqXHR, error, errorThrown) { if (jqXHR.status == 401) { alert("Sesión caducada. ingrese nuevamente."); localStorage.removeItem('user'); window.location = "../Access/Login.html"; } if (jqXHR.status == 409) { alert("No puede eliminar el registro seleccionado porque tiene datos vínculados en el sistema. Considere desactivarlo"); $("#Message").text(""); } if (jqXHR.status == 300) { alert(jqXHR.responseText); $("#Message").text(""); } else { try { Guía de Desarrollo Aplicaciones Web con Html5 Página 16 de 33 } } } OnError(jqXHR.responseText); } catch (e) { } }); return result; loadDropDownList Es el encargado de generar las listas desplegables, tanto para formularios únicos, como para filtros. function loadDropDownList(div, dataValueField, dataTextField, optionLabel, controller, params, variable, viewId) { var data = JSON.parse(localStorage.getItem('user')); data.ObjectId = viewId; data.Parameters = dato1.Parameters.concat(params); $('#' + div).kendoDropDownList( { autoBind: true, dataValueField: dataValueField, dataTextField: dataTextField, optionLabel: optionLabel, dataSource: { type: "json", transport: { read: "../api/" + controller + "/?" + variable + "=" + JSON.stringify(data) } } }); } GetDataSource Encargado de obtener los datos para carga posterior en componentes. function GetDatasource(controller, action) { return new kendo.data.DataSource({ type: "json", transport: { read: "../api/" + controller + "/?" + action + "=" + JSON.stringify(securityPort) } } }); FilterData: Muchos de los filtros que poseían el sistema, al tener en memoria toda la información, no hacían necesario ir de nuevo al servidor para hacer nuevas consultas, de tal modo que se generó una función genérica que pudiese filtrar los objetos en memoria, sin importar los filtros que se tuviesen. Cabe aclarar que para que esto funcione, es necesario que el campo a buscar, se encuentre dentro del dataSource a consultar. function FilterData(filterDiv, filterGrid) { var filter = []; $('#' + filterDiv + ' :input').each(function () { var flag = false; if ($(this).val() != "" && $(this).val().indexOf("Seleccione") != 0) { var field = $(this).attr("id").toString().replace('f', '').replace('', '.').replace('@', ''); var value = $(this).val(); if ($(this).attr("id").indexOf('-') != -1 || $(this).attr("id").indexOf('@') != -1) { value = parseInt($(this).val()); Guía de Desarrollo Aplicaciones Web con Html5 Página 17 de 33 if (isNaN(value) && $(this).val().indexOf("Seleccione") != 0) value = $(this).val(); flag = true; } switch ($(this).attr("type")) { case "checkbox": filter.push({ field: field, operator: "eq", value: $(this).is(":checked") }); break; default: if (flag) filter.push({ field: field, operator: "eq", value: value }); else filter.push({ field: field, operator: "contains", value: value.toString() }); break; } } }); $('#' + filterGrid).data("kendoGrid").dataSource.filter(filter); } Se puede ver dentro del código una función each de Jquery, que dentro de un div definido por el usuario, ubicaba todos los campos de tipo input, generando un array con todos los filtros utilizados por el usuario. Del mismo modo, existe una función encargada del limpiado de estos, donde básicamente, y siguiendo la regla de la construcción de filtros, lo que se le envía al Source es un arreglo vacío que retornara el Source por defecto. ClearFilter function ClearFilter(filterGrid, filterDiv) { $('#' + filterDiv + ' :input').each(function () { $(this).val(''); if (this.className == "k-dropdown") $("#" + this.id).data("kendoDropDownList").select(0); if (this.className == "k-checkbox") { if (this.id != "ACTIVOf") { $("#" + this.id).attr("checked", false); $("#" + this.id).val(false); } } }) $('#' + filterGrid).data("kendoGrid").dataSource.filter({}); } SetFileImport: Como se mencionó en el apartado de Vistas Parciales, algunos de las ventanas, requerían definir “sub ventanas” parametrizables, con el fin de evitar la replicación de algunos comportamientos. Una de estas fue la que se encargaba de la importación de archivos dentro del sistema. Para que esto fuera posible, se definió la función para generar un componente de carga de archivos, con algunos valores que cambiarían dentro de este (visuales y de lógica) así como funciones que serían sobre escritas en los Scripts que las requirieran. Esto con la finalidad de enviar parámetros específicos de la clase o algún comportamiento del control que no fuera común. function setFileImport(templateName, fileImportTitle) { $("#fileImportTitle").text(fileImportTitle); Guía de Desarrollo Aplicaciones Web con Html5 Página 18 de 33 } $("#templateDownloader").attr('href', '../api/media/?type=' + templateName); $("#file").kendoUpload({ async: { saveUrl: "../api/media/", removeUrl: "remove", autoUpload: true }, upload: onUpload, error: onError, multiple: false, showFileList: false, localization: { select: "Seleccionar" }, success: onSuccess }); Inyección de código – Kendo UI. El Framework de componentes Kendo UI, provee de librerías JavaScript específicas para la ampliación en la usabilidad de componentes HTML5, con el fin de generar un sistema más robusto y al mismo tiempo, agradable para el usuario. A continuación se explican cada uno de los componentes utilizados dentro de la aplicación y algunos comportamientos específicos, en caso necesitar aprovechar al máximo cada uno de estos. Cabe aclarar, como se mencionó anteriormente, que los Id’s de los campos, son los accesos a los componentes. Globalization Este apartado es sumamente importante, debido a que la globalización, determinara el lenguaje de algunos componentes. Dentro de la carpeta de Scripts que provee kendo, existe una subcarpeta llamada Cultures, la cual posee todos los lenguajes para algunos controles del sistema. Para poder utilizar estas culturas es necesario insertar código HTML y sobre el Script específicamente. HTML <script src="../Scripts/cultures/kendo.culture.es-CO.min.js"></script> JavaScript function LoadForm() { kendo.culture("es-CO"); /*Código extra*/ } TabStrip El componente TabStrip permite al usuario, crear pestañas dentro de la vista con el fin de ordenar todos los campos que requiera sin necesidad de sobrecargar la ventana, o simplemente para separar procesos que no sean dependientes de otros y dar una mejor visión al usuario de lo que quiere. HTML: <div id="tabs"> <ul> <li class="k-state-active">Datos Generales</li> Guía de Desarrollo Aplicaciones Web con Html5 Página 19 de 33 </ul> </div> <li>Personas</li> <li>&Aacute;reas</li> <li>Catálogos Datos</li> JavaScript: $("#tabs").kendoTabStrip({ animation: { open: { effects: "fadeIn" } } }); En algunos casos, es necesario deshabilitar ciertas pestañas, a menos que el usuario realice acciones puntuales dentro de la ventana. Para esto, se utiliza el siguiente código. En caso de necesitar habilitar la ventana, se usara enable para realizar esa función. $("#tabs").kendoTabStrip().data("kendoTabStrip").disable($("#tabs").kendoTabStrip ().data("kendoTabStrip").items()[i]); $("#tabs").kendoTabStrip().data("kendoTabStrip").enable($("#tabs").kendoTabStrip( ).data("kendoTabStrip").items()[i]); Otro caso que se puede dar, es la necesidad de inyectar código HTML (en este caso para el visualizador de reportes), dentro de la ventana. $("#tabs").data("kendoTabStrip").append({ text: "Previsualización", content: "<div><div id='pdf' style='height: 450px'></div></div>" }); Menu Este componente, mediante el uso de listas desordenadas, nos permite la creación de un menú, ya sea horizontal o vertical, para ordenar y presentar todas las ventanas del sistema. Anteriormente se hablaba de los permisos que tenía cada una de las ventanas, y como esto junto al token, eran los encargados de validar el acceso o no a una ventana por parte de un usuario. El código HTML es devuelto por el servidor cuando la sesión es válida (esto por si en algún momento se modifican permisos sobre el propio perfil), posteriormente es inyectado dentro del div del menu, para luego ser convertido en componente de Kendo. HTML <div id="HomeMenu" style="width: 100%"> <!--Menu Inyectado dinámicamente--> </div> Javascript $("#menu").kendoMenu(); DataSource El modelo de KendodataSource permite definir una estructura de almacenamiento de información accesible desde cualquier componente de Kendo. Es necesario para ciertos componentes definir esquemas u objetos de transporte, con el fin de hacer un llenado o una Guía de Desarrollo Aplicaciones Web con Html5 Página 20 de 33 estructuración para posterior trabajo de información. Para componentes como listas desplegables o autocompletables basta con definirles que campo se mostrara y cual será el valor que se asignara. En las grillas específicamente, se requiere especificar un esquema para el correcto modelamiento de la información y del mismo modo para no perder datos durante una edición. Este componente no tiene una visualización HTML (porque está en memoria), sin embargo, es un poco más complejo de trabajar dentro de Javascript. DataSource generado a base de un array Var a = new kendo.data.DataSource({ data:[ "1","2","3"] }); DataSource generado mediante llamado a controlador RestFul dataSource = new kendo.data.DataSource({ transport: { read: { dataType: "json", url: "../api/products/?getData=" + JSON.stringify(securityPort) } } }); Especificación de un esquema para una grilla (con objetos compuestos). dataSource: { pageSize: 20, schema: { model: { id: "PERSONA_ID", fields: { PERSONA_ID:{validation: { required: { message: "El Código es requerido" } }}, CONSTRASENNA: { validation: { required: true } }, CONSTRASENNA2: { validation: { required: true } }, NOMBRES: { validation: { required: { message: "El Nombre es requerido" } } }, TELEFONO1: { validation: { required: { message: "Telefono requerido" } } }, FW_AREAS: { defaultValue: { AREA_ID: 0, AREA: "" } }, ACTIVO: { type: "boolean", defaultValue:true }, LOGIN: { validation: { required: { message: "Login requerido" } } }, MAIL: {}, MAIL2: {}, PERSONALIZADO1: {}, PERSONALIZADO2: {}, TELEFONO2: {}, APELLIDOS: { validation: { required: { message: "El Apellido es requerido" } } }, CODIGOBARRAS: {}, FW_PERFILES: { defaultValue: {}} } } } }, Guía de Desarrollo Aplicaciones Web con Html5 Página 21 de 33 Grid El componente Grid de Kendo, se encarga de la visualización de información mediante tablas, las cuales pueden ser tablas fijas, dinámicas o maestro-detalle. Dependiendo de la necesidad que se tenga, el código HTML puede variar. Por ejemplo, en caso de que la tabla sea estática (con valores fijos, o que no se necesite inyectar las columnas), el código seria por defecto el de una tabla normal <table></table>. Dentro de este apartado, se explican distintos métodos para crear tablas. Desde estáticas, hasta templates de edición y de visualización de columnas. Algunas tareas como el agrupamiento, el ordenamiento y demás, se convierte en simples booleanos que realizan todas las tareas automáticamente. Nota: Para cualquier tipo de filtro, ir a Filtrar dataSource Tablas Estáticas HTML <table id="OrderStatusGrid"> <thead> <tr> <th data-field="ETAPA" style="text-align: center;color:white" class="headerMonitor"> Etapa </th> <th data-field="CANTIDAD" style="text-align: center;color:white"> Cantidad </th> </tr> </thead> <tbody> <tr> <td colspan="2"></td> </tr> </tbody> </table> Javascript $("#OrderStatusGrid").kendoGrid({ scrollable: false, dataSource: null, }); Tablas Dinámicas HTML <div id="gridAreas"></div> Javascript $("#gridAreas").kendoGrid({ scrollable:false, dataSource: { pageSize: 20 }, toolbar: [{ name: "create", text: "Agregar" }, { template: '<a class="k-button k-button-icontext k-grid-edit" href="\\#" id="edit" onClick="EditData()"><span class="k-icon kedit"></span>Editar</a>' }, { Guía de Desarrollo Aplicaciones Web con Html5 Página 22 de 33 template: '<a class="k-button k-button-icontext k-grid-edit" href="\\#" id="remove" onClick="RemoveData()"><span class="k-icon kdelete"></span>Eliminar</a>' }], columns: [ { field: "AREA_ID", title: "C&oacute;digo", hidden: true }, { field: "AREA", title: "&Aacute;rea" } ], editable: { mode: "popup" }, selectable: true, }); Templates de Edición HTML <script id="popup-editor" type="text/x-kendo-template"> <p style="width:370px"> <label>&Aacute;rea(*):</label><input name="AREA" required data-requiredmsg="Ingrese un nombre para el área" maxlength="120" style="width:80%" class="ktextbox"/><span class="k-invalid-msg" data-for="AREA"></span> </p><br/> </script> Javascript (dentro de la configuración del Grid) editable: { mode: "popup", template: kendo.template($("#popup-editor").html()), confirmation: false }, Templates de Columna HTML <script id="rowTemplate" type="text/x-kendo-tmpl"> <tr> <td>#=data.ETAPA#</td> <td><span>#:data.CANTIDAD#</span></td> </tr> </script> Javascript(dentro de la configuración del Grid) rowTemplate: kendo.template($("#rowTemplate").html()) Textos personalizados y agrupamientos (dentro de la configuración del Grid) pageable: { pageSizes: true, messages: { display: "{0} - {1} de {2} Areas", itemsPerPage: "Areas por p&aacute;gina", empty: "No hay Areas disponibles" } }, groupable: {groupable:true, messages: {empty: "Arrastre una columna aquí para agrupar."}}, Ordenamiento (dentro de la configuración del Grid) sortable: true, Guía de Desarrollo Aplicaciones Web con Html5 Página 23 de 33 Columnas: (dentro de la configuración del grid). Como se puede ver, las columnas también tienen plantillas de visualización, esto se utiliza, o cuando se quiere modificar el dato antes de presentarlo, o cuando se posee un objeto y solo se quiere mostrar una parte de ese (ya que si no se hace, saldrá [Object object] en la celda). columns: [ { field: "PERSONA_ID", title: "C&oacute;digo (*)" }, { field: "NOMBRES", title: "Nombres (*)" }, { field: "APELLIDOS", title: "Apellidos (*)" }, { field: "FW_CATALOGOVALORES", title: "Cargo (*)", hidden: true, editor: CargoDropDownEditor }, { field: "FW_PERFILES", title: "Perfil (*)", editor: PerfilDropDownEditor, template: '#=FW_PERFILES.PERFIL#' }, { field: "FW_AREAS", title: "&Aacute;rea (*)", editor: AreaDropDownEditor, template: '#=FW_AREAS.AREA#' }, { field: "CODIGOBARRAS", title: "C&oacute;digo de Barras", hidden: true }, { field: "MAIL", title: "E-Mail (*)", hidden: true, editor: EmailEditor }, { field: "MAIL2", title: "E-Mail 2", hidden: true, editor: EmailEditor }, { field: "TELEFONO1", title: "Tel&eacute;fono 1 (*)" }, { field: "TELEFONO2", title: "Tel&eacute;fono 2", hidden: true }, { field: "ACTIVO", title: "Activo", type: "boolean", template:'#if(ACTIVO == 1){#Activo#}else{#Inactivo#}#' }, { field: "LOGIN", title: "Login (*)" }, { field: "CONTRASENNA", title: "Contrase&ntilde;a (*)", hidden: true, editor: PasswordEditor }, { field: "CONTRASENNA2", title: "Contrase&ntilde;a 2 (*)", hidden: true, editor: PasswordEditor }, { field: "PERSONALIZADO1", title: "Personalizado 1", hidden: true }, { field: "PERSONALIZADO2", title: "Personalizado 2", hidden: true } ], Editores (Dentro del código Javascript): Por defecto, el Popup que genera kendo para edición únicamente muestra cajas de texto, para estos casos, dentro de la configuración de las columnas, se utiliza un parámetro editor, el cual permite inyectar código distinto a ese Popup. En el siguiente caso se muestra un ejemplo de cómo inyectar una lista desplegable dentro del Popup de edición. Código de la columna { field: "FW_CATALOGOVALORES", title: "Cargo (*)", hidden: true, editor: CargoDropDownEditor }, Código del Editor function CargoDropDownEditor(container, options) { securityPort.Parameters = [{ Key: "Catalog", Value: "1" }, { Key: "dataType", Value: "DtValueCatalog" }]; $('<input style="width:100%;z-index: 100" required data-required-msg="Cargo Requerido" data-text-field="VALOR" data-value-field="CATVALID" data-bind="value:' + options.field + '" name="' + options.field + '" id="' + options.field + '" />') .appendTo(container) .kendoDropDownList( Guía de Desarrollo Aplicaciones Web con Html5 Página 24 de 33 { autoBind: true, dataValueFileld: "CATVALID", dataTextField: "VALOR", optionLabel: "Seleccione un cargo", dataSource: { type: "json", transport: { read: "../api/valueCatalog/?GetData=" + JSON.stringify(securityPort) } } }); } Maestro – detalle: Dentro del grid principal se coloca el siguiente parámetro detailInit: detailInit, Donde detailInit será la función encargada de crear el detalle de la fila. Otro factor importante es tener el modelo correctamente definido con un Id, ya que el Source secundario será producto de este. function detailInit(e) { securityPort.Parameters = [{ Key: "Catalog", Value: e.data.CATTIPOID }, {Key:"dataType",Value:"DtValueCatalog"}]; $('<div id="detailGrid' + e.data.CATTIPOID + '"><div/>').appendTo(e.detailCell).kendoGrid({ dataSource: { type: "json", transport: { read: "../api/valueCatalog/?GetData=" + JSON.stringify(securityPort) }, pageSize: 20, schema: { model: { fields: { VALOR: { validation: { required: { message: "El Valor es requerido" } } }, CATTIPOID: { defaultValue: e.data.CATTIPOID }, DESCRIPCION: {}, PERSONALIZADO1: {}, PERSONALIZADO2: {}, IDENTIFICADOR: {} } } }, }, toolbar: [{ name: "create", text: "Agregar" }, { template: '<a class="k-button k-button-icontext k-grid-edit" href="\\#" onClick="editDetail(\'' + e.data.CATTIPOID + '\')"><span class="k-icon k-edit" ></span>Editar</a>' }, { template: '<a class="k-button k-button-icontext k-grid-edit" href="\\#" onClick="removeDetail(\'' + e.data.CATTIPOID + '\')"><span class="kicon k-delete"></span>Eliminar</a>' }], editable: "popup", selectable: true, scrollable: false, sortable: true, pageable: { messages: { display: "{0} - {1} de {2} Valores", empty: "No hay valores disponibles" Guía de Desarrollo Aplicaciones Web con Html5 Página 25 de 33 } }); } }, columns: [ { field: { field: { field: { field: { field: ] "VALOR", title: "Valor (*)" }, "DESCRIPCION", title: "Descripción" }, "IDENTIFICADOR", title: "Identificador" }, "PERSONALIZADO1", title: "Personalizado1" }, "PERSONALIZADO2", title: "Personalizado2" } EVENTOS: Edición: El evento de edición ayuda bastante a la configuración del Popup de edición, ya que permite cambiar textos por defecto, o en el caso de templates, puede cargar los componentes en ese momento. El siguiente caso muestra un evento de edición donde se cargan controles y se modifican aspectos puntuales de la ventana emergente. edit: function (e) { $("#autoCompleteToolbar").kendoAutoComplete({ minLength: 1, dataTextField: "FILTRABLE", filter: "contains", select: onSelect, close: onClose, dataSource: dataSource, }); updateBtnText = $(".k-grid-update").html().replace("Update", "Guardar"); $(".k-grid-update").html(updateBtnText); cancelBtnText = $(".k-grid-cancel").html().replace("Cancel", "Cancelar"); $(".k-grid-cancel").html(cancelBtnText); headerText = $(".k-window-title").html().replace("Edit", "Gestión de Productos"); $(".k-window-title").html(headerText); } Guardado: El evento de guardado es invocado tras agregar una fila o modificarla en una columna. Mediante e.model.isNew() o métodos distintos de validación, se puede verificar si es o no nuevo el objeto. El siguiente caso fue usado para crear clientes. save: function (e) { e.preventDefault(true); if (e.model.CLIENTE == false && e.model.PROVEEDOR == false) { alert("Debe seleccionar si es cliente o proveedor."); return; } var tmp = e.model.CIUDAD_ID; e.model.CIUDAD_ID = e.model.CIUDAD_ID.CIUDAD_ID; if (e.model.EMPRESA_ID) { Guía de Desarrollo Aplicaciones Web con Html5 Página 26 de 33 sendData(e.model, "PUT", "Empresa editada satisfactoriamente", "AssociatedCompanies", objectPermision, "FW_EMPRESAS"); } else { e.model.EMPRESA_ID = 0; var id = sendData(e.model, "POST", "Empresa agregada satisfactoriamente", "AssociatedCompanies", objectPermision, "FW_EMPRESAS"); e.model.EMPRESA_ID = id; } e.model.CIUDAD_ID = tmp; $("#grid").data("kendoGrid").refresh(); } La línea e.preventDefault(true) es usada para evitar que el Popup se cierre en caso de que no funcione. $("#grid").data("kendoGrid").refresh(); es usada para refrescar el grid terminado el proceso. Forms La parte de controles para formularios contiene todos los componentes relacionados con listas desplegables, calendarios, autocompletables, validadores y cajas numéricas. Controles del tipo de cajas de texto, textarea o Checkbox son controles que se manejan desde clases de estilo para que tomen el diseño de kendo, puesto que estos no tienen un comportamiento complejo dentro del sistema. Este tipo de controles, al igual que otros, permite el uso de eventos para controlar sus comportamientos. TextBox, TextArea y Checkbox Basta con colocar en la definición del input (HTML), class=”k-textbox” o class=”k-checkbox”. Tipos de Inputs Gracias a los cambios que tiene el estándar HTML5 sobre el 4.1, los tipos de inputs tienen algunas variaciones, y cuando se validan, no se necesitan funciones extra para que se cumplan ciertas condiciones. Email: Cuando se utiliza el tipo de entrada email (type=”email”), se define un data-email-msg el cual aparecerá cuando la entrada no cumpla con la estructura de un email válido. <input maxlength="120" type="email" id="MAIL" name="MAIL" class="k-textbox" placeholder="ej. [email protected]" required data-email-msg="Ingrese un formato valido de correo" style="width: 95%; margin: 0 auto;" data-requiredmsg="Email Requerido"/> Password: Basta con colocar Type=”password” para activar este tipo de entrada. Guía de Desarrollo Aplicaciones Web con Html5 Página 27 de 33 NumericTextBox Es una variante al tipo Number de los input, y ayuda a validación como la inserción de letras en el campo, o intervalos de valor (min - max). Además, permite definir cuantos decimales tener en cuenta (decimals). HTML <input type="text" id="CANTIDAD_SOLICITADA" style="width: 95%; margin: 0 auto;" /> Javascript $("#CANTIDAD_SOLICITADA").kendoNumericTextBox({ min: 1, decimals: 0, value: 1, change: onChange }); DatePickers Las entradas de calendario, permiten mostrar un almanaque para seleccionar la fecha. De mismo modo como con los númericos, permiten validar que no se ingresen letras o valores fuera de rango. Este tipo de componente tiene la peculiaridad de que se le debe especificar una cultura para que muestre el lenguaje adecuado, ya que por defecto esta en inglés. Por otro lado, se debe tener cuidado con el formato, ya que el estándar de comunicación JSON para fechas, necesita un formato universal para que la comunicación con la base de datos no provoque excepciones o fallos innecesarios en la lógica. Se debe tener cuidado con la clase de textbox, puesto que si se utiliza, el formato del datePicker podría arruinarse HTML <input type="text" id="FECHA_INICIO" style="width: 95%; margin: 0 auto;" /> Javascript $('#FECHA_INICIO').kendoDatePicker({ format: "yyyy/MM/dd", culture: "ES-CO" }); AutoComplete Las cajas auto completables, permiten hacer búsquedas bajo ciertos criterios dentro de un Source, mostrando un listado de similitudes para seleccionar como lista desplegable, con un único valor de selección. Requieren un datasource previamente. Con el parámetro filter se puede determinar si la búsqueda será eq (igual a) o contains (like). En algunos casos se sugiere que el Source, tenga una columna con la información a filtrar, en caso de que se requiera buscar en varios campos. Esto debe hacerse con cuidado, puesto que ese campo por el que se filtra, es el texto que se muestra. HTML <input type="text" id="autoCompleteToolbar" style="width: 95%; margin: 0 auto;" /> Javascript $("#autoCompleteToolbar").kendoAutoComplete({ minLength: 1, dataTextField: "FILTRABLE", Guía de Desarrollo Aplicaciones Web con Html5 Página 28 de 33 }); filter: "contains", select: onSelect, close: onClose, dataSource: dataSource, DropDownList La lista desplegable, es uno de los componentes más utilizados dentro de Kendo, puesto que por su fácil configuración y su gran alcance, puede ser aplicado en muchos casos puntuales (filtros, editores, formularios, logins, etc). Básicamente tienen el mismo comportamiento en configuración que los demás controles mencionados anteriormente; requieren un DataSource, especificar los campos de donde se traerá la información (en caso de ser dependiente el dataSource debe traer la llave foránea del Source principal), textos por defecto, índices, etc. Al igual que en las grillas, estos controles permiten inyectar templates de previsualización, para mostrar de distintas maneras la información sobre la ventana. Controles independientes HTML <input style="width: 90%; margin: 0 auto;" id="TIPO_DOCUMENTO_ID" class="kdropdown" /> Javascript $("#TIPO_DOCUMENTO_ID").kendoDropDownList( { serverFiltering: true, autoBind: true, dataValueFileld: "BODEGA_ID", dataTextField: "NOMBRE", optionLabel: "Seleccione una division de Bodega", dataSource: { type: "json", transport: { read: "../api/Inventory/?getData=" + JSON.stringify(securityPort) } } }); Controles dependientes Se presentan a continuación 2 controles, el primero que sería el principal, muestra el listado de bodegas de una empresa cualquiera. El segundo, presenta las divisiones de la bodega (previamente todas están cargadas en memoria), donde existe una dependencia contra el primer Source. Para definir la dependencia se utiliza el atributo cascadeFrom, donde ira el nombre de la lista principal. HTML <input style="width: 98%; margin: 0 auto;" id="BODEGA_ID" data-textfield="NOMBRE" data-value-field="BODEGA_ID" data-bind="value:BODEGA_ID" name="BODEGA_ID" class="k-dropdown" /> Guía de Desarrollo Aplicaciones Web con Html5 Página 29 de 33 <input style="width: 98%; margin: 0 auto;" id="DIVISION_ID" data-textfield="NOMBRE" data-valuefield="DIVISION_ID" data-bind="value:DIVISION_ID" name="DIVISION_ID" class="k-dropdown" /> Javascript $("#BODEGA_ID").kendoDropDownList( { serverFiltering: true, autoBind: true, dataValueFileld: "BODEGA_ID", dataTextField: "NOMBRE", optionLabel: "Seleccione una division de Bodega", dataSource: { type: "json", transport: { read: "../api/Inventory/?getData=" + JSON.stringify(securityPort) } } }); $("#DIVISION_ID").kendoDropDownList( { cascadeFrom: "BODEGA_ID", serverFiltering: true, autoBind: true, dataValueFileld: "DIVISION_ID", dataTextField: "NOMBRE", optionLabel: "Seleccione una division de Bodega", dataSource: { type: "json", transport: { read: "../api/Inventory/?getData=" + JSON.stringify(securityPort) } } }); Validators Los validadores en kendo son usados para eliminar el tag <form> de las vistas. Esto con el fin de evitar al máximo los redireccionamientos. La función que tiene es tomar todos los inputs de un div establecido y validar cuales de los que poseen el atributo Required, cumplen con la condición. var validator = $("#purchaseEditForm").kendoValidator().data("kendoValidator"); if (validator.validate()) {} En caso de que alguna condición no se cumpla, el componente automáticamente disparara los mensajes erróneos en cada uno de los campos que incumplan la validación. En algunos casos los mensajes no saldrán, debido a que no hay una conexión entre el validador y el campo. Para realizar dicha conexión, se requiere colocar un Name (preferiblemente igual al id del campo) y posteriormente agregar la siguiente línea: <input maxlength="120" type="text" class="k-textbox" id="NOMBRES" style="width: 95%; margin: 0 auto;" name="NOMBRES" data-required-msg="Nombres Requerida" required /><br/><span class="k-invalid-msg" data-for="NOMBRES"></span> De este modo se mostrara el mensaje al usuario. Guía de Desarrollo Aplicaciones Web con Html5 Página 30 de 33 Reportes Para la visualización de reportes, se utilizó otro de los componentes nuevos de HTML5 que reemplazo el iframe del estándar pasado. El tag <Object> se encargaba de la previsualización de documentos. La idea era consumir un servicio de tal manera que generara el documento y retornara el nombre temporal del archivo, a lo que se enviaba al Object para poder mostrar el PDF con el reporte. Generación de Filtros. Una de las características necesarias en los visores de reportes, es la posibilidad de poder filtrar la información en caso de ser necesario. El modelo definido para esta función se encuentra dentro de la base de datos, y retorna como una columna dentro de la tabla principal (respuesta del servidor). Mediante métodos que generaban dinámicamente los componentes, se podía convertir dicha información en un filtro personalizado para cada uno de los reportes. A continuación se muestra la estructura de la columna en la tabla y la visualización sobre el visor de reportes. El reporte tomado como ejemplo es el de movimientos de Inventario. XML <?xml version="1.0" encoding="UTF-8"?><ReportFilter><Filter FieldName="FECHA" FieldText="Fecha" DataType="rangeCalendar" /><Filter FieldName="DOC_ID" FieldText="Nro documento" ClassType="k-textbox" DataType="text" /><Filter FieldName="DOCRELACIONADO" FieldText="Documento Relacionado" ClassType="k-textbox" DataType="text" /><Filter FieldName="CODIGOEMPRESA" FieldText="Código Empresa" ClassType="k-textbox" DataType="text" /><Filter FieldName="ITEM_ID" FieldText="Producto" DataType="multiselect" ValueField="ITEM_ID" TextField="DESCRIPCION" DataSource="SELECT ITEM_ID,DESCRIPCION FROM FW_ITEMS WHERE EMPRESA_ID = {0} ORDER BY DESCRIPCION"/><Filter FieldName="SERIAL" FieldText="Serial" ClassType="k-textbox" DataType="text" /><Filter FieldName="LOTE" FieldText="Lote" ClassType="k-textbox" DataType="text" /><Filter FieldName="PERSONA_CREACION" FieldText="Persona Creación" DataType="dropDownList" ValueField="PERSONA_CREACION" TextField="NOMBRE" DataSource="select distinct(PERSONA_CREACION),NOMBRES + ' ' + APELLIDOS AS NOMBRE from INV_DOCUMENTOS INNER JOIN FW_PERSONAS ON FW_PERSONAS.PERSONA_ID = INV_DOCUMENTOS.PERSONA_CREACION WHERE INV_DOCUMENTOS.EMPRESA_ID = {0} ORDER BY NOMBRE" DefaultText="Seleccione una Persona" /><Filter FieldName="BODEGA_ORIGEN" FieldText="Bodega" DataType="dropDownList" ValueField="BODEGA_ID" TextField="NOMBRE" DataSource="SELECT BODEGA_ID,NOMBRE FROM INV_BODEGAS WHERE EMPRESA_ID = {0} ORDER BY NOMBRE" DefaultText="Seleccione una bodega" /><Filter FieldName="BODEGA_DESTINO" FieldText="Bodega" DataType="dropDownList" ValueField="BODEGA_ID" TextField="NOMBRE" DataSource="SELECT BODEGA_ID,NOMBRE FROM INV_BODEGAS WHERE EMPRESA_ID = {0} ORDER BY NOMBRE" DefaultText="Seleccione una bodega" /></ReportFilter> Guía de Desarrollo Aplicaciones Web con Html5 Página 31 de 33 Previsualización Hojas de Estilo Kendo UI, posee archivos de estilo que configuran cada uno de sus componentes, estos archivos son bastante extensos, lo cual haría complicado su mantenimiento y tedioso para la búsqueda de errores. Basandose en ese punto de vista, se definio uno orden en el importado de los estilos, eso con el objetivo de hacer los cambios de estilos sobre los componentes al final, y que no fueran sobre escritos por los Default de Kendo. <link rel="stylesheet" type="text/css" href="../Styles/kendo.default.min.css"> <link rel="stylesheet" type="text/css" href="../Styles/kendo.common.min.css"> <link href="../Styles/StyleTemplate.css" rel="stylesheet" type="text/css"> Manteniendo ese orden, se obtiene una única hoja de estilo con todas las características del sitio. Plantilla estándar Dentro del modelo de hojas de estilo. Se definió una plantilla estándar para el cliente que constaba de 3 capas principales: Header, Content y Footer. El header, se comporta como la capa que contendrá cualquier información que este sobre el menú (nombre de la empresa, nombre del usuario, logo de la StoreIT). El Footer es la capa que contiene todo lo que esta después de las pestañas (logo de la empresa, logo de Infotrack). El content es el div encargado del menú y de todas las pestañas que componen el aplicativo. Cross Browsing Una de las necesidades principales de un cliente WEB, radica en que este se comporte de la misma manera en cualquier navegador (Safari, IE, Firefox, Chrome, Opera, Mobile, etc). Para esto, kendo utiliza Webkit para definir comportamientos en navegadores específicos, con el fin de estandarizar el diseño del sitio sobre todas las plataformas. La siguiente línea muestra como mediante WEBkit se puede hacer que una ventana emergente se vea de la misma manera en todos los exploradores. Guía de Desarrollo Aplicaciones Web con Html5 Página 32 de 33 .k-window-titlebar, .k-grouping-header { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #89888a), color-stop(100%, #4c4b4c)); background-image: -webkit-linear-gradient(#89888a,#4c4b4c); background-image: -moz-linear-gradient(#89888a,#4c4b4c); background-image: -o-linear-gradient(#89888a,#4c4b4c); background-image: linear-gradient(#89888a,#4c4b4c); background-position: 50% 50%; background-color: #4c4b4c; } Guía de Desarrollo Aplicaciones Web con Html5 Página 33 de 33