En la publicación anterior comenté algo sobre "eficiencia" y es porque estoy convencido que los desarrolladores siempre debemos tener esa palabra en mente; ahora vamos a realizar un ejercicio en el que implementaremos un DataSet con varias tablas, relaciones y además utilizaremos el Cache del servidor ASP para mantener los datos.
En la parte 4 de esta serie de publicaciones hicimos una implementación "económica" para acceder a la información de Provincias, Departamentos y Localidades permitiendo que el usuario seleccione los distintos ítems hasta obtener una Localidad en particular. Digo económica porque utilizamos objetos del tipo SqlDataReader que son más chicos y más rápidos que los DataSet, sin embargo cada vez que un usuario accede a todo el proceso de selección de una localidad se producen innumerables accesos al motor de la base de datos.
Ocurre que la información de Provincias, Departamentos y Localidades rara vez cambia (casi nunca) de manera que esta información es una excelente candidata para ser "cacheada" en RAM evitando de ese modo miles de accesos al servidor de datos. Por supuesto habrá que pensar algún mecanismo para actualizar el cache, lo que dejo para el final de esta publicación.
Por otro lado, lo que vamos a realizar es un objeto complejo que tendrá toda la información necesaria para brindar a cualquier usuario los datos de acuerdo a la selección que va realizando. Esto quiere decir que este objeto complejo tendrá toda la información de Provincias, Departamentos y Localidades con las relaciones necesarias para realizar el filtrado de datos correspondiente.
El ejercicio es en realidad un demo, que puede servir como base para que Uds. desarrollen componentes específicos de una aplicación.
La idea es mostrar en pantalla la lista de provincias, cuando el usuario selecciona una de ellas deben aparecer (al lado) los departamentos de esa provincia y cuando el usuario seleccione uno de los departamentos deberán aparecer las localidades de dicho departamento. A continuación está una imagen del diseño que puede funcionar para el ejercicio:

Obviamente lo que estamos por hacer no se puede hacer con los asistentes de Visual Studio (hay que ensuciarse las manos y de una buena vez ponerse a escribir código para eso somos desarrolladores), de modo que el código de la interfaz de usuario puede quedar como sigue:
1: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm7.aspx.cs" Inherits="ADO2.WebForm7" %>
2:
3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4:
5: <html xmlns="http://www.w3.org/1999/xhtml" >
6: <head runat="server">
7: <title></title>
8: </head>
9: <body>
10: <form id="form1" runat="server">
11: <div style="font-size:small;">
12: <div>
13: <asp:Label ID="lblDataOrigin" runat="server" Text=""></asp:Label>
14: </div>
15: <div>
16: <div style="float:left; padding-right:15px; " >
17: <asp:GridView ID="gvProvincia" runat="server" AllowPaging="True" PageSize="5"
18: onpageindexchanging="gvProvincia_PageIndexChanging"
19: onselectedindexchanged="gvProvincia_SelectedIndexChanged" SelectedRowStyle-ForeColor="White" SelectedRowStyle-BackColor="#660066">
20: <Columns>
21: <asp:CommandField ShowSelectButton="True" />
22: </Columns>
23: </asp:GridView>
24: </div>
25: <div>
26: <div>
27: <div style="float:left; padding-right:15px; ">
28: <asp:GridView ID="gvDepartamento" runat="server"
29: onselectedindexchanged="gvDepartamento_SelectedIndexChanged" >
30: <Columns>
31: <asp:CommandField ShowSelectButton="True" />
32: </Columns>
33: </asp:GridView>
34: </div>
35: <div>
36: <asp:GridView ID="gvLocalidad" runat="server" >
37: </asp:GridView>
38: </div>
39: </div>
40: </div>
41: </div>
42: </div>
43: </form>
44: </body>
45: </html>
Aprovecho la oportunidad para mostrarles como se realiza una página o formulario web sin utilizar tablas
, en este caso los componentes de la interfaz de usuario flotan unos al lado de los otros.
Observen que los componentes solamente incorporan lo necesario para capturar el evento cuando cambia el índice de la selección, y además voy a mostrar cómo se puede hacer para paginar un GridView desde código (esto va sin cargo en esta publicación).
Bien como dije, vamos a crear un objeto complejo que tendrá todo lo necesario para mostrar la información de estas tres tablas. De manera que vamos a codificar un método que crea este objeto; no vamos a definir una clase porque el objeto es un DataSet que facilita todo el comportamiento que necesitamos.
1: /// <summary>
2: /// Carga un DataSet con la información desde la base de datos
3: /// Establece las relaciones existentes entre las tablas cargadas
4: /// </summary>
5: /// <returns>Objeto DataSet</returns>
6: protected DataSet MyDataLoad()
7: {
8: SqlConnection mySqlConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["Postal"].ConnectionString);
9: SqlCommand mySqlCommand = new SqlCommand();
10: mySqlCommand.Connection = mySqlConnection;
11: DataSet myDataSet = new DataSet();
12: SqlDataAdapter mySqlDataAdapter;
13:
14: mySqlCommand.CommandText = "SELECT [ID], [Nombre] FROM [Provincia]";
15: mySqlCommand.CommandType = CommandType.Text;
16: mySqlDataAdapter = new SqlDataAdapter(mySqlCommand);
17: mySqlDataAdapter.Fill(myDataSet, "Provincia");
18:
19: mySqlCommand.CommandText = "SELECT [ID], [idProvincia], [Nombre] FROM [Departamento]";
20: mySqlCommand.CommandType = CommandType.Text;
21: mySqlDataAdapter = new SqlDataAdapter(mySqlCommand);
22: mySqlDataAdapter.Fill(myDataSet, "Departamento");
23:
24: DataColumn myParentColumn = myDataSet.Tables["Provincia"].Columns["ID"];
25: DataColumn myChildColumn = myDataSet.Tables["Departamento"].Columns["idProvincia"];
26: DataRelation myRealtion = new DataRelation("Provincia_Departamento", myParentColumn, myChildColumn);
27: myDataSet.Relations.Add(myRealtion);
28:
29: mySqlCommand.CommandText = "SELECT [ID], [idDepartamento], [Nombre] FROM [Localidad]";
30: mySqlCommand.CommandType = CommandType.Text;
31: mySqlDataAdapter = new SqlDataAdapter(mySqlCommand);
32: mySqlDataAdapter.Fill(myDataSet, "Localidad");
33:
34: myParentColumn = myDataSet.Tables["Departamento"].Columns["ID"];
35: myChildColumn = myDataSet.Tables["Localidad"].Columns["idDepartamento"];
36: myRealtion = new DataRelation("Departamento_Localidad", myParentColumn, myChildColumn);
37: myDataSet.Relations.Add(myRealtion);
38:
39: return myDataSet;
40: }
Comentemos lo que hacemos en este método. En las líneas 8 a 17 se declaran y crean los objetos necesarios para obtener desde la base de datos todas las Provincias y se cargan en el DataSet, observen que para ello se "bautiza" la tabla con un nombre (línea 17).
En las líneas 19 a 22 se hace lo mismo con la información de los Departamentos y se cargan en el DataSet también "bautizando" la tabla (interna) que los contiene.
En las líneas 24 a 27 se crea la relación que existen entre cada Provincia y sus Departamentos, esta relación se agrega (línea 27) a la colección de relaciones que mantiene el DataSet.
En las líneas 34 a 37 se hace lo mismo para las Localidades y su relación con los Departamentos.
Finalmente este método devuelve el objeto DataSet con toda esta información.
Ahora tenemos que desarrollar un mecanismo que nos permita enlazar los datos con la interfaz de usuario, eso es lo que normalmente se conoce como DataBind. Recordemos que esto es "muy eficiente" de manera que es en este punto donde vamos a incorporar el manejo del Cache que nos brinda el Servidor ASP.
El Cache es un objeto (pedazo de memoria) que está disponible en el servidor ASP para nuestras aplicaciones (un cache por cada aplicación), cuando se debe utilizar y cuando no hacerlo es una decisión arquitectónica y también de desarrollo, en este caso el ejemplo viene como anillo al dedo para utilizarlo.
Voy a describir el método que enlaza los datos por partes para no mezclar la gestión del cache con el proceso de enlazar los datos propiamente dicho:
1: /// <summary>
2: /// Enlaza los datos con los controles de la interfaz de usuario
3: /// La primera vez lo hace desde el motor de base de datos y
4: /// almacena el dataset en el cache del servidor,
5: /// las siguientes veces toma el dataset del cache.
6: /// </summary>
7: protected void MyDataBind()
8: {
9: DataSet myDataSet;
10:
11: if (Cache["MyDataSet"] == null)
12: {
13: myDataSet = MyDataLoad();
14: Cache["MyDataSet"] = myDataSet;
15: lblDataOrigin.Text = "Datos desde el <b>Motor de Base de Datos</b>";
16: }
17: else
18: {
19: myDataSet = (DataSet)Cache["MyDataSet"];
20: lblDataOrigin.Text = "Datos desde el <b>Cache</b>";
21: }
22:
Este es el código que realiza la tarea, en primer lugar se verifica si el Cache está vació (esto es la primera vez) entonces se obtiene el DataSet del método explicado antes, se guarda el DataSet en el cache y para que veamos como ocurre se indica en un label (que no debe estar en la aplicación final); caso contrario, cuando encontramos algo en el cache y como eso que encontramos lo pusimos nosotros ya sabemos de que se trata de manera que lo podemos tomar en nuestra variable del tipo DataSet y avisamos en el label que lo recuperamos desde el Cache y no del motor de base de datos.
El cache es un objeto que implementa el comportamiento de un Dictionary de modo que se puede acceder mediante un clave (en este caso "MyDataSet").
Ahora seguimos con el código que enlaza los datos con la interfaz de usuario:
23: gvProvincia.DataSource = myDataSet.Tables["Provincia"];
24: gvProvincia.DataKeyNames = new string[] { "ID" };
25: gvProvincia.DataBind();
26:
27: if (gvProvincia.SelectedIndex != -1)
28: {
29: DataView myDataView = new DataView(myDataSet.Tables["Provincia"]);
30: myDataView.Sort = "ID";
31: object key = gvProvincia.SelectedValue;
32: int index = myDataView.Find(key);
33: DataRowView myDataRowView = myDataView[index];
34:
35: gvDepartamento.DataSource = myDataRowView.CreateChildView("Provincia_Departamento");
36: gvDepartamento.DataKeyNames = new string[] { "ID" };
37: gvDepartamento.DataBind();
38:
Lo que se hace es indicar que el origen de datos para el GridView de Provincias es la tabla "Provincia" que se encuentra dentro del DataSet, se indica que la clave de cada renglón será el campo "ID" (para eso hay que crear un arreglo de string, porque la clave podría estar compuesta por varios campos), se enlaza los datos con el GridView.
A continuación en la línea 27 se averigua si el usuario ha seleccionado algún renglon correspondiente a Provincias y en caso de ser así se procede a enlazar los datos correspondientes a los Departamentos de esa Provincia. Para ello se utiliza una "vista" de la tabla de provincias, observen que el valor seleccionado (SelectedValue) del GridView de Provincias se utiliza para buscar dentro del DataView que nos devuelve el índice que corresponde a la fila donde se encuentra esa información.
Finalmente se indica que el origen de datos del GridView de Departamentos es una vista que se obtiene a partir del renglón (fila) donde se encuentra la provincia por medio de un método que utiliza la relación que existe dentro del DataSet para traer solamente los departamentos correspondientes a esa provincia. Nuevamente se indica que la clave de ordenación (necesaria para poder buscar) es el campo "ID" en este caso el de los Departamentos (hace falta para hacer lo mismo con las Localidades) y se "enlaza" los datos con la interfaz de usuario.
A continuación está el código semejante para las Localidades:
39: if (gvDepartamento.SelectedIndex != -1)
40: {
41: myDataView = new DataView(myDataSet.Tables["Departamento"]);
42: myDataView.Sort = "ID";
43: myDataRowView = myDataView[myDataView.Find(gvDepartamento.SelectedValue)];
44:
45: gvLocalidad.DataSource = myDataRowView.CreateChildView("Departamento_Localidad");
46: gvLocalidad.DataBind();
47: }
48: else
49: { // Esto hace falta para que desaparezca el GridView
50: gvLocalidad.DataSource = null;
51: gvLocalidad.DataBind();
52: }
53: }
54: else
55: {
56: gvLocalidad.DataSource = null;
57: gvLocalidad.DataBind();
58: gvDepartamento.DataSource = null;
59: gvDepartamento.DataBind();
60: }
61: }
Observen que en este caso se utilizan directamente los valores devueltos por la propiedad SelectedValue y el método Find.
En caso que no haya una selección válida nos aseguramos que los GridView tanto de Localidades y Departamentos desaparezcan, porque no deben mostrarse si no se ha seleccionado nada.
El código que atrapa los eventos de cambio de índice seleccionado es el siguiente:
1: /// <summary>
2: /// Captura el cambio de selección en el GridView de Provincias
3: /// </summary>
4: /// <param name="sender">Objeto que ejecuta el evento</param>
5: /// <param name="e">Información del evento</param>
6: protected void gvProvincia_SelectedIndexChanged(object sender, EventArgs e)
7: {
8: gvDepartamento.SelectedIndex = -1;
9: MyDataBind();
10: }
11: /// <summary>
12: /// Captura el cambio de selección en el GridView de Departamentos
13: /// </summary>
14: /// <param name="sender">Objeto que ejecuta el evento</param>
15: /// <param name="e">Información del evento</param>
16: protected void gvDepartamento_SelectedIndexChanged(object sender, EventArgs e)
17: {
18: MyDataBind();
19: }
Observen que al cambiar la selección de Provincia nos aseguramos que se anule la actual selección de Departamentos lo que provocará la "desaparición" del GridView correspondiente a las Localidades.
Finalmente el código que permite paginar un GridView:
1: /// <summary>
2: /// Captura el intento de cambio de página en el GridView de Provincias
3: /// </summary>
4: /// <param name="sender">Objeto que ejecuta el evento</param>
5: /// <param name="e">Información del evento</param>
6: protected void gvProvincia_PageIndexChanging(object sender, System.Web.UI.WebControls.GridViewPageEventArgs e)
7: {
8: // Si cambiamos de página en las provincias
9: // dejan de ser validos los otros GridView
10: gvProvincia.SelectedIndex = -1;
11: gvDepartamento.SelectedIndex = -1;
12: gvProvincia.PageIndex = e.NewPageIndex;
13: MyDataBind();
14: }
Observen que al cambiar la página de Provincias se debe cancelar la selección actual. Es importante destacar que para hacer esto se debe capturar el evento "antes que se cambie la página" este evento es el "Changing" no el "Changed".
Como dije al principio, esta cuestión de poner "cosas" en el cache del servidor ASP necesita de un mecanismo que "refresque" el cache cada tanto tiempo. Bien para eso les muestro cómo funciona el siguiente método:
1: if (Cache["MyDataSet"] == null)
2: {
3: myDataSet = MyDataLoad();
4: //Cache["MyDataSet"] = myDataSet;
5: Cache.Insert("MyDataSet", myDataSet, null, DateTime.UtcNow.AddMinutes(1), System.Web.Caching.Cache.NoSlidingExpiration);
6: lblDataOrigin.Text = "Datos desde el <b>Motor de Base de Datos</b>";
7: }
Se reemplaza la línea 4 por la línea 5, en donde estamos utilizando el método Insert que publica el objeto cache. Este método nos permite insertar un objeto en el cache indicando el nombre o clave con el que accederemos, el objeto mismo, un valor null (que por ahora no voy a explicar porque complicaría todo), un valor de tiempo (en este caso estoy tomando la fecha y hora UTC actual más 1 minuto) que sirve para decirle al objeto cache que cuando se cumpla ese tiempo SUPRIMA EL OBJETO DEL CACHE !!!, y el último parámetro que indica que no se debe reiniciar la cuenta cada vez que se accede al objeto en el cache.
Lo que se logra con esto es que cada 1 minuto se volverá a tomar los datos desde la base de datos, de manera que si alguien "agregó" o "cambió" los datos en la misma, esos cambios aparecerán en el cache. Por supuesto se debe decidir cada cuanto tiempo se "refresca" el cache.
Espero que les sirva, espero comentarios.
Nota: No se olviden de invocar a MyDataBind() desde el Page_Load (solamente cuando se carga la página).
Para leer fuera de línea: Un Poco de ADO NET - parte 5.pdf (107,01 kb)