Johnbarquin’s Weblog

La bitácora del Barquín más barco de la Web

ReportViewer y fuentes de datos dinámicas

Publicado por johnbarquin en Abril 1, 2009

Durante las últimas semanas he estado enfrascado en labores de mantenimiento de aplicaciones un poco “viejitas” y entre las tareas está la incorporación de algunos requerimientos hechos por los usuarios de modificaciones a los informes, creación de nuevos, etc.  Por los días en que fueron programadas originalmente, solía usar componentes de terceros para la creación de informes, los cuales resultaron muy eficaces y aún hoy están sirviendo informes felizmente.  Sin embargo no siempre se obtiene ciento por ciento de compatibilidad con las versiones nuevas de .NET; así las cosas, decidí echar una ojeada a ReportViewer que viene incluído con Visual Studio 2008.  A primera vista me pareció un poco frívolo, con pocas posibilidades de personalización, gracias a la manía de Microsoft de ponernos las cosas estúpidamente simples; con el asistente y unos cuantos clicks, es posible en menos de un minuto obtener una forma de Windows o una página web que muestre un informe completamente funcional, eso sí, monolíticamente ligado a una tabla específica o expresión SQL.  A continuación intentaré mostrar cómo utilizar ReportViewer para mostrar informes a partir de fuentes de datos creadas dinámicamente en tiempo de ejecución.

Acerca de ReportViewer

A modo de preámbulo, ReportViewer es un control gratis que permite incluir informes en las aplicaciones web y desktop (http://www.gotreportviewer.com/).  Posee dos modos de operación:  local y remoto.  Cuando trabaja en modo remoto, simplemente muestra un informe creado por un Servidor de Informes de SQL Server.  En modo local, todo el procesamiento, filtrado u ordenamiento ocurren a nivel de cliente.  En este post se asume que estamos trabajando en modo local (propiedad ProcessingMode=local). 

Conceptualmente, para completar un informe con ReportViewer son necesarios dos componentes:

  • La definición del informe, que en este caso es un documento en formato RDLC (Report Definition Language for Client).
  • La fuente de datos, un DataSet.

Uno de los aspectos positivos de ReportViewer es la independencia entre estos componentes; la definición del informe no está ligada a ningún formato específico de base de datos, ni a ninguna base o tabla específica.  De ahí la posibilidad de construir una definición sin conocer el origen exacto de los datos; solo es necesario especificar de forma abstracta los nombres de campos que debe incluir una tabla que sirva de fuente de datos al informe y sus tipos de datos.

Usualmente construyo las aplicaciones empleando las tradicionales capas lógicas (tiers) de acceso a datos, negocios e interfaz; uno de los módulos se especializa en la construcción de los DataSets que sirven de base a los informes.  Este método me resultó siempre muy útil con los componentes de terceros, pues me permitía cambiar de uno a otro sin modificaciones (o mínimas) en el resto de las capas.  Afortunadamente, a ReportViewer le he podido también “enchufar” los DataSets generados por estos módulos previamente construídos.  Solamente he tenido que crear nuevamente las definiciones de los informes en el nuevo lenguaje RDL, aunque esto es fácil con las herramientas visuales de Visual Studio.

Obtención del Conjunto de Datos

Para entrar en materia, voy a emplear un ejemplo muy simple: se desea mostrar en una página .aspx un informe que liste los atletas finalistas en unos Juegos Olímpicos.  En el sistema real se le permite al usuario seleccionar la edición de los Juegos, el deporte y la modalidad, de ahí que la fuente de datos tenga que ser dinámica.  Definiré una función CalcularDataSet() que retorna los finalistas de 100 metros planos en atletismo durante los Primeros Juegos Olímpicos celebrados en Atenas, Grecia en 1896.  Esta función es un patrón que simplifica el verdadero cómputo del DataSet, que puede ser todo lo complicado que se desee y generalmente residiría en la capa de acceso a datos o en la capa de negocios.  Por tanto, incluyo el código por completitud, pero no es importante para comprender el mecanismo que nos ocupa.  El aspecto relevante es que la función devuelve un DataSet que contiene el DataTable que servirá de fuente de datos al informe.

public DataSet CalcularDataSet()
{
    string sql = @"
            SELECT  posiciones.lugar,
                    atletas.nombre,
                    atletas.apellido1,
                    delegaciones.nombre AS Pais,
                    posiciones.resultado,
                    eventos.nombre AS Evento,
                    eventos.sigDeporte
            FROM atletas INNER JOIN integrantesGrupos ON atletas.idAtleta = integrantesGrupos.idAtleta
                    INNER JOIN posiciones
                    INNER JOIN Competencias ON posiciones.idCompetencia = Competencias.IdCompetencia
                    INNER JOIN eventos ON posiciones.idEvento = eventos.idEvento
                    INNER JOIN Categorias ON Competencias.Categoria = Categorias.IdCategoria ON integrantesGrupos.idResultado = posiciones.idPosicion
                    INNER JOIN delegaciones ON posiciones.idDelegacion = delegaciones.idDelegacion
            WHERE categorias.idclase=1
                    AND competencias.categoria=2
                    AND posiciones.idCompetencia=286
                    AND eventos.sigDeporte='ATL'
                    AND posiciones.idEvento=14
                    AND posiciones.idFase=3
                    AND posiciones.sexo='M' ";
    string connstr = "Data Source=localhost;Initial Catalog=historia;Integrated Security=True";

    SqlConnection conn = new SqlConnection(connstr);
    SqlCommand cmd = new SqlCommand(sql, conn);

    SqlDataAdapter da = new SqlDataAdapter();
    da.SelectCommand = cmd;

    conn.Open();

    DataSet ds = new DataSet();
    da.Fill(ds, "Finalistas");

    conn.Close();
    return ds;

}

 Creación de la Definición del Informe (documento RDLC)

 El documento RDLC es simplemente un documento XML que cumple con el esquema del lenguaje RDLC.  Por tanto, en caso extremo, es posible crearlo con cualquier editor de textos.  En la práctica, por supuesto, resulta más conveniente emplear las herramientas visuales que proporciona Visual Studio.   Para ello añadimos al proyecto un nuevo item de tipo Report y lo denominaremos Finalistas.rdlc.   Ello muestra el diseñador de informes con un informe vacío.  Aquí arrastramos desde la barra de herramientas un nuevo Table, añadimos las columnas necesarias y les colocamos sus respectivos títulos en la fila de encabezados (Header).

Si compilamos el proyecto, Visual Studio “protestará” debido a que no se ha definido un DataSource en el informe.  Debemos añadir entonces un DataSet nuevo al proyecto.  En este momento contamos con al menos dos opciones:

  1. Estilo bottom-up: construir un DataSet a partir de la base de datos real, suministrando una consulta en SQL a través de un Table Adapter, un procedimiento almacenado, etc.
  2. Estilo top-down: construir un DataSet “ficticio” con una tabla que solo va a contener los nombres de los campos que vamos a utilizar en el informe.  Este es el enfoque que se sigue aquí.

Ambos métodos son igualmente válidos.  El verdadero objetivo de este paso es crear una fuente de datos que sea “comprensible” para el diseñador de informes de Visual Studio.  De regreso al informe, aparecerá en la ventana “Website Data Sources” el nuevo DataSet creado que denominamos DataSetFinalistas.   En el diseñador de DataSets obtuve una tabla que denominé TableFinalistas que luce así:

tablefinalistas

Para completar la definición del informe solo debemos arrastrar los campos correspondientes desde la ventana “Website Data Sources” hacia la fila de detalles (Detail) de la tabla que insertamos anteriormente y añadir las opciones de formato deseadas.

Luego de guardar el documento, para comprender mejor qué hemos hecho, conviene echar una ojeada al documento RDLC generado.  Por cualquiera de los dos métodos señalados, se creó una sección que contiene la definición del DataSet.  Por cierto, es curioso notar cierta ambiguedad en el término DataSet en este contexto: para RDLC el DataSet es más bien una tabla o la definición de sus campos, contrario a .NET dónde DataSet es una estructura de datos que contiene tablas (DataTable); nada alarmante pero conviene estar claros de esta sutileza para evitar confusiones.  La sección de DataSets del RDLC para nuestro ejemplo, queda de la siguiente forma:

  <DataSets>
    <DataSet Name="DataSetFinalistas_TableFinalistas">
      <Fields>
        <Field Name="lugar">
          <DataField>lugar</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
        <Field Name="nombre">
          <DataField>nombre</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
        <Field Name="apellido1">
          <DataField>apellido1</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
        <Field Name="pais">
          <DataField>pais</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
        <Field Name="resultado">
          <DataField>resultado</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
        <Field Name="evento">
          <DataField>evento</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
        <Field Name="sigDeporte">
          <DataField>sigDeporte</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
      </Fields>
      <Query>
        <DataSourceName>DummyDataSource</DataSourceName>
        <CommandText />
        <rd:UseGenericDesigner>true</rd:UseGenericDesigner>
      </Query>
      <rd:DataSetInfo>
        <rd:DataSetName>DataSetFinalistas</rd:DataSetName>
        <rd:TableName>TableFinalistas</rd:TableName>
      </rd:DataSetInfo>
    </DataSet>
  </DataSets>

Esto, usando el segundo método.  La única diferencia en cuanto a generación entre ambos métodos es que con el primero, se incluye más información en la definición, por ejemplo el comando que se utiliza para generar los datos (CommandText), la cadena de conexión (ConnectString), y otros que en un final son irrelevantes pues todo esto lo vamos a generar en ejecución.  Nótese en la sección <Query> la “fuente de datos tonta” (DummyDataSource).  Todas estas secciones, Query, CommandText, DataSourceName, son obligatorias y es necesario incluirlas aunque estén vacías si no queremos escuchar las quejas del compilador.

En fin, la elección de uno u otro método es una cuestión de gusto y comodidad, si estamos seguros de los campos que vamos a utilizar, entonces el (2) es más simple; si antes queremos jugar un poco con los datos o crear aquí mismo la consulta entonces (1) es más cómodo.  Como quiera, lo único importante para realizar el diseño del informe y crear el correspondiente RDLC, es la definición de los nombres de campos que se van a usar.

Informe = Definición en RDLC + Datos 

Ya tenemos una función que genera DataSets (de .NET), o en una aplicación real, todo un componente dentro de la capa de datos; y tenemos un RDLC que contiene la definición del informe.  Ahora solo resta buscar la forma de aglutinarlos a ambos.

Muy sencillo, supongamos que nuestro proyecto es una aplicación web en el cual tenemos una página llamada finalistas.aspx en la cual depositamos un ReportViewer llamado rv.  Además tenemos un botón “Mostrar finalistas” que carga el informe en el ReportViewer.  El código es el siguiente:

protected void MostrarInforme()
{
    ReportDataSource rds = new ReportDataSource();
    rds.Name = "DataSetFinalistas_TableFinalistas";
    rds.Value = CalcularDataSet().Tables[0];

    rv.LocalReport.DataSources.Clear();
    rv.LocalReport.DataSources.Add(rds);
    rv.LocalReport.ReportPath = "Finalistas.rdlc";
    rv.LocalReport.Refresh();
}

ReportDataSource es la estructura que permite asociar el DataSet referenciado en la definición del informe con un conjunto de datos (DataTable en este caso)  generado dinámicamente en tiempo de ejecución.  El punto importante aquí es que debe coincidir el Name del ReportDataSource con el Name del DataSet en el RDLC (en este caso DataSetFinalistas_TableFinalistas).

informefinalistas

Un par de temas abiertos

Para terminar me agradaría compartir un par de temas abiertos con los que he tropezado en el trabajo con ReportViewer.  El primero es específico a la versión para web.  Si fijamos las dimensiones del control en tiempo de diseño, en ejecución se mostrarán barras de desplazamiento automáticamente en caso que el tamaño real sea mayor que el predeterminado.  Fijando la propiedad SizeToReportContent a True, es posible indicarle a ReportViewer que ajuste el tamaño del control en tiempo de ejecución al tamaño real del informe.  Sin embargo, para que esto funcione, es necesario fijar la propiedad AsyncRendering a False, de lo contrario ni se entera.  Pero con esta configuración (AsyncRendering=False y SizeToReportContent=True) no me funciona el páginado, al hacer click para mostrar la siguiente página, sigue mostrando la primera.

He estado experimentando algunas de las variantes en este post para resolver el problema y creo que la solución está por esa vía jugando un poco con los javascripts, aunque honestamente, me ha faltado el tiempo para probarlo con más profundidad.  Si alguien lo ha hecho, por favor, me deja un comentario por acá.

El segundo se refiere a la generación dinámica del RDLC, o sea, de la definición del informe.  Esto abre una amplia gama de posibilidades, la definición en RDLC es finalmente un XML que podemos construir en tiempo de ejecución.  He incluído en mi lista de tareas utilizar esta variante para construir informes basados en consultas cruzadas, en las cuales es imposible predecir en tiempo de diseño la cantidad de campos.  Recomiendo descargar la aplicación de ejemplo Dynamic Matrixeste post en el cual se construye el RDLC aplicando una transformación XSLT al esquema del DataSet que contiene el resultado de la consulta

En general, ReportViewer resulta muy útil y fácil de usar y aunque no es nueva la idea de separa definición de datos, me parece muy acertada la idea de emplear este enfoque.  Es posible descargar las especificaciones del lenguaje RDL del 2008 desde el sitio de Microsoft de SQL 2008.

6 comentarios para “ReportViewer y fuentes de datos dinámicas”

  1. Juan Noriega escribió

    Como llamas al reporte rdlc por medio de parametros?

  2. Robert escribió

    Gran aporte man….. realmente espectacular. Es muy bueno no guiarnos solamente por los wizard y hacer algo mas inteligente… Palmas para ti

  3. Arturo escribió

    Realmente usted es el primer ser en este planeta que en realidad muestra algo util, estaba varado, llevo mas de un mes buscando alguien que me luces sobre este tema.

    gracias

  4. Arturo escribió

    Algo Adicional:

    el metodo seria el mismo para visual basic??????

  5. johnbarquin escribió

    Hola Arturo, me alegra le haya sido útil esta información. En efecto, es posible implementar el método también en VB pues como cualquier lenguaje sobre .NET, soporta ADO.NET y ReportViewer.

    Saludos

  6. jaime tovar escribió

    Buenos comentarios yo hes estado desarrolonado 2008 pero tengo para problemillas la de la orientacion del papel y la de impresión del lado del cliente he leido varios post y el que me funcion lo de la impresion es el siguiente:
    http://idelabar.blogspot.com/2008/10/problemas-para-imprimir-con-report.html
    mucho agradeciari si tienes solicon para el tamaño y orientacion del papel (horizontal)
    saludos y felicidades por tus comenarios realmente es algo funcional.

Escribe un comentario

XHTML: Puedes usar estas etiquetas: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>