Los días 22 y 29 de Septiembre estaré dando una charla breve sobre el desarrollo en SharePoint. La charla es libre y se dará en Buffa Sistemas pueden obtener más información sobre la registración en Club BS además hay algunas otras que te pueden interesar.
Sobre la charla… mi intensión es dar una visión general sobre las capacidades que posee SharePoint para utilizarlo como plataforma de desarrollo mostrando casos prácticos del mundo real. Según mi experiencia, SharePoint (y sobre todo WSS 3.0 y MOSS) en proyectos con características determinadas puede utilizarse como una plataforma sobre la cual desarrollar aplicaciones que al componerlas con otras (en este caso aplicaciones de Office) forman una verdadera solución integral, esto no es nuevo y tiene nombre y apellido son las aplicaciones OBA.
Otro objetivo es dar una introducción general sobre las operaciones básicas en el desarrollo sobre SharePoint, mediante SharePoint Designer y mediante Visual Studio 2008. Pueden descargar las ppts del curso de aquí
Sé que es un título muy fuerte para un primer post, así que les contaré la historia completa. Se trata de un proyecto montado sobre SharePoint 2007 (MOSS para ser mas especifico) que básicamente realiza el control de facturas por pagar a proveedores, sin entrar en mucho detalle irrelevante para lo que quiero contarles, les diré que es una aplicación típica en la cual hay un flujo de aprobación simple (el flujo de aprobación que viene nativo con SharePoint). En el cual se tienen que dar de alta facturas pertenecientes a un proveedor, bueno acá pensé es un buen momento de aplicar el servicio de Form Services del cual había leído que es ideal para estos casos.
Y acá realmente empieza la historia. El formulario no tenía mucha complejidad, debía conectarse a una base de datos para extraer datos de proveedores, además de traer datos de dos tablas en esa base de datos para llenar dos combos (dropdown list), y además dar la posibilidad de llenar un campo de texto de múltiple línea, luego este formulario se almacenaría y daba inicio al flujo de aprobación del pago. Hasta acá ningún problema un poco de dificultad para alojar las conexiones a la base de datos en un lugar apropiado, y otras dificultades menores para habilitar que los formularios se muestren sin necesidad del cliente InfoPath, pero eso serán motivo de otro post.
Los formularios se conectaban a la base de datos, llenaban los combos, traían información de los proveedores, se almacenaban correctamente e iniciaban el flujo, todo sin ningún problema (o con todos los problemas superados). Hasta que llego el momento de ponerlo a producción. Me empezaron a reportar problemas de performance con los formularios, e incluso que a causa de la aplicación en SharePoint las estaciones de trabajo se colgaban, upsss :s que podría estar pasando?.
Lo primero que hice fue hacer una réplica idéntica del entorno de producción, note que la información de las tablas que llenaban a los combos era más de la que realmente esperaba, pero esto pueda colgar a un sistema de 1GB de RAM?. Inicie mis pruebas de performance, la verdad es que mi cliente tenía razón, luego de un uso intensivo de los formularios de InfoPath el sistema se iba tornando lento, inmediatamente me puse a monitorear el proceso de Internet Explorer, note que básicamente este proceso IExplorer.exe se estaba comiendo la memoria, llegando a ocupar 700MB!. Bueno dije, la causa puede ser una forma ineficiente de serializar la información dentro de los combos, leí por ahí que es aconsejable si vas a utilizar combos con mucha información, llenarlos a partir de un archivo XML, y eso hice. Debo decir que note una mejora en la velocidad de respuesta, pero la memoria tomada por IExplorer.exe seguía siendo similar. Ante este tipo de problemas lo mejor es aislar las posibles causas. Así que desarrollé (o configuré) rápidamente un formulario de que solo tenga los dos combos, y a probar nuevamente. Si bien la utilización de memoria en la carga del formulario era muy similar al del formulario en producción, esta memoria se liberaba inmediatamente luego de cerrar el formulario, cosa que no pasaba con el formulario original, bueno ahí está la causa del problema, IExplorer no está liberando la memoria utilizada, es decir que cada carga de formulario simplemente va multiplicando el uso de memoria de IExplorer, obviamente todo este problema se solucionaba abriendo y cerrando Internet Explorer pero eso no le podía decir a mi cliente :S
Empecé a mirar que es lo que además de los combos tenía mi formulario, porque era evidente que la causa del problema no era de la mucha información que tenían los combos, o de la forma de obtener esa información. Como les comentaba anteriormente uno de los campos del formulario se llenaba texto de múltiple línea, para el cual había puesto un Rich Text Box que estaba permitido para mantener la compatibilidad Web (que se mostrará en navegadores, muchos controles solo sirven cuando se utiliza el cliente InfoPath), lo primero que hice fue poner un control de texto enriquecido en el formulario que solo tenía los combos, ese formulario que me sirvió para aislar el problema, y adivinen que… el formulario dejo de liberar memoria. Ya tenía el culpable.
Por algún motivo esté control impedía que IE liberara memoria, imagino que hay una causa, o una razón, aun la desconozco, desconozco también si hay algún hotfix que solucione este comportamiento. Me olvide comentar que una de las pruebas fue actualizar a IE 7, note que la performance del formulario en general mejoro mucho, pero el manejo de memoria seguía siendo el mismo. Considerando que ahora no puedo reemplazar el control por uno de "texto simple" porque ya hay muchos formularios cargados (hasta ahora encontre la causa, pero no la solución L ). He decidido prescindir de este control hasta nuevo aviso.
Sé que hay mucho escrito respecto las mejores prácticas de ADO.NET, sin embargo quise resumir las buenas prácticas según mi experiencia.
Esta es la primera y más recomendada practica, empecemos abriendo y cerrando conexiones, a continuación un ejemplo en código.
1. public static int GetProductCount()
2. {
3. string sql = "SELECT COUNT(*) " +
4. "FROM Production.Product " +
5. "WHERE ListPrice > 0";
6. object dbValue;
7.
8. using (SqlConnection connection = ConnectionManager.GetConnection())
9. {
10. using (SqlCommand command = new SqlCommand(sql, connection))
11. {
12. command.CommandType = CommandType.Text;
13. dbValue = command.ExecuteScalar();
14. }
15. }
16.
17. return (int)dbValue;
18. }
La línea de código 8 es clave, ConnectionManager es una clase que se encarga de administrar las conexiones (una clase propia, no pertenece a ADO.NET) esta clase contiene un método GetConnection que devuelve una conexión abierta.
La instrucción using provee una conveniente sintaxis que asegura el correcto uso de los objetos IDisposable, por lo que es ideal para aplicar esta buena práctica. Expliquémosla brevemente.
Cuando se crea una instancia dentro de la instrucción using, como en la línea 8 y 10, se asegura que el método Dispose es llamado sobre el objeto cuando el bloque de using termina, o cuando ocurre una excepción, la clase de la cual nace el objeto en cuestión debe implementar System.IDisposable (tanto SqlConnection como SqlCommand lo implementan).
Por tanto en la línea de código 15 se invoca el método Dispose de la conexión, Dispose y Close son funcionalmente equivalentes.
Ahora apliquemos el concepto en las consultas a datos con el sqlDataReader, es especialmente importante la práctica de cerrar en este caso.
1.//clase de la capa intermedia, que contiene un metodo de negocio que devuelve un data reader.
2. public class MyProductsDAC
3. {
4. public static SqlDataReader GetProductReader()
5. {
6. SqlConnection connection = new SqlConnection(aConnectionString);
7. connection.Open();
8. string sql = "SELECT Name, ListPrice FROM Production.Product";
9. SqlDataReader reader;
13. reader = command.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.CloseConnection);
14. } // Dispose command object.
15. return reader;
16. }
17. }
18.// Componente de interfaz de usuario, que llama al metodo de negocio de la capa intermedia.
19. public class MyUserInterfaceComponent
20. {
21. public void MyMethod()
22. {
23. using (SqlDataReader reader = MyProductsDAC.GetProductReader())
24. {
25. while (reader.Read())
26. {
27. Console.WriteLine("Product name: {0}", reader.GetString(0));
28. Console.WriteLine("Product price: {0}", reader.GetDecimal(1));
29. }
30. }
31. // Se invoca el Dispose del data reader, el cual cierra la conexión.
32. }
33. }
La línea de código 23 es clave, en esta línea se hace el llamado al método GetProductReader que retorna un sqlDataReader con datos. Nuevamente como este objeto es creado dentro del contexto de using al finalizar el bloque se ejecuta el Dispose del objeto el cual cierra la conexión.
Las conexiones son recursos caros desde el punto de vista de la performance, por tanto hay que hacer un manejo cuidadoso de él. Asegurarse de cerrar las conexiones cuando no se utilizan permite devolverlas al pool de conexiones disponibles para ser reutilizadas.