) le permiten organizar salida tabular sin mucho esfuerzo. Seguro, es una tarea exigente tratar con todas esas cadenas de texto del tamaño de un niño para generar la página, pero también hay manera de sortearlas. En el capítulo 13 analicé XSLT (transformación XSL, XSL Transformations), una manera de tomar datos XML y darle cualquier formato que desee (incluidos estupendos trabajos de arte creados por Miguel Ángel, o HTML bellamente ensamblado). Aunque obtiene HTML, también cuenta con una opción de desplegar métodos. El método más directo incluye el almacenamiento de HTML generado en un archivo de disco, y el lanzamiento del explorador predeterminado del usuario para desplegarlo usando un comando como: Process.Start("c:\temp\MiInforme.htm") Si quiere que el informe tenga un aspecto más integrado en su aplicación, puede desplegar el contenido HTML en un control del explorador. Hicimos esto en el código del proyecto para el capítulo 17, cuando desplegamos los detalles de un elemento de biblioteca como HTML. Documentos XPS La base de presentación de Windows (WPF, Windows Presentation Foundation) se usa principalmente para que su interfaz de usuario baile con color y acción. Pero una parte de esa tecnología existe para generar documentos estáticos tipo XML conocidos como XPS (XML Paper Specification, especificación XML para el papel). De la misma manera en que puede generar informes con HTML, puede hacerlo con el estándar XPS. En el capítulo 18 se incluye un breve análisis de WPF y XPS. Si la generación de informes en XPS le parece interesante, revise la documentación incluida con Visual Studio. 582 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 582 17/2/10 15:32:20 Servicios y controles de informes Visual Studio incluye un conjunto de clases en el espacio de nombres Microsoft.Reporting que están específicamente diseñados para crear informes de datos. La clase clave en este espacio de nombres es ReportViewer (identificada como MicrosoftReportViewer en su otro control). En realidad, es dos controles: uno para Windows Forms y otro para Web Forms. Estos controles están basados, en parte, en la tecnología encontrada en Microsoft SQL Server Reporting Services, aunque puede usar los controles sin SQL Server. El Proyecto Biblioteca usará el control WinForms.ReportViewer para sus informes integrados. Dedicaremos la mayor parte de este capítulo a analizar el control y su uso en aplicaciones de Windows Forms. No analizaré la versión de Web Forms del control aquí, aunque su uso es muy similar al de la versión de Windows Forms. Verá lo simple que es agregar el control MicrosoftReportViewer a un proyecto existente. Pero Visual Studio también incluye una nueva plantilla de proyecto que se concentra en el control MicrosoftReportViewer. La creación de un nuevo proyecto “Aplicación de informes” usa un asistente para proyectos que le ayuda a configurar un informe personalizado. El resultado final puede ser su aplicación de informe completo o puede usarla como base para mayor personalización. Crystal Reports Si tiene por lo menos la Professional Edition de Visual Studio 2008, recibió una copia de gracia de Crystal Reports. La versión incluida es un subconjunto funcional de la versión oficial de Crystal Reports 2008. Si es nuevo en Visual Basic, tal vez se haya saltado las versiones anteriores de Crystal Reports que se han incluido con el lenguaje desde sus primeras versiones. Debido a esta relación de largo tiempo con Visual Basic, Crystal Reports se ha vuelto uno de los paquetes de creación de informes de uso más amplio en el mercado. Crystal Reports es un producto de terceros, que es propiedad de una empresa llamada Business Objects. El producto ha cambiado de propietario varias veces desde que se asoció con Visual Basic, pero al parecer Business Objects se está ocupando de ello por el momento. No analizaré más Crystal Reports en este libro. Integración con Microsoft Office Visual Basic ha sido el lenguaje principal de macros para la suite de aplicaciones Microsoft Office desde la muerte final de WordBasic. Pero estoy hablando de los tiempos anteriores a .NET Visual Basic, que era no administrado. Por fortuna, también puede usar el mundo administrado de Visual Basic 2008 para interactuar con aplicaciones de Microsoft Office. La manera en que interactúa con Office depende de si el documento de Office o la aplicación de Visual Basic es el eje principal para el usuario. Opciones de informe en .NET | 583 21_PATRICK-CHAPTER_21.indd 583 17/2/10 15:32:20 Si su objetivo es mejorar una aplicación de “línea de negocios” empleando aplicaciones de Office como portal (por ejemplo, mostrando las últimas cifras de ventas dentro de Microsoft Outlook) considere la generación de una aplicación de negocios de Office (OBA, Office Business Application). Las OBA representan una nueva manera de diseñar programas integrados empleando Visual Studio, Microsoft Office, Microsoft SharePoint Services y otros sistemas relacionados. Desde el mundo de Visual Studio, su trabajo de desarrollo sucede mediante las herramientas para Office de Visual Studio (Visual Studio Tool for Office, VSTO), incluidas con las ediciones Professional y Team System de Visual Studio. Si su objetivo es crear sus propios complementos de “barra de tareas” dentro de aplicaciones de Office, use las nuevas plantillas de proyecto integradas de Office incluidas con Visual Studio. Se trata de extensiones fáciles de desarrollar que le permiten personalizar la experiencia de usuario al personalizar el conjunto de características de Office. Los complementos también se consideran parte de VSTO y sólo están disponibles con las ediciones Visual Studio 2008 Professional y Team System. Si el usuario sólo accederá indirectamente a las características de Office mediante su aplicación de Visual Basic (por ejemplo, si quiere que su programa inicie una combinación de correspondencia de Microsoft Word), use los ensamblados primarios de interoperación (Primary Interop Assemblies, PIA) de Microsoft Office proporcionados por Microsoft. Estas bibliotecas proporcionan acceso a las características específicas de la aplicación de Office mediante el espacio de nombres Microsoft.Office. Como VSTO, estas bibliotecas vinculan su código .NET con Microsoft Office, pero con el enfoque en su código en lugar del documento de Office. Uso de controles de informe en .NET Dedicaremos el resto de este capítulo a analizar las herramientas de informes estándar proporcionadas en Visual Studio. Como ya se mencionó, hay dos clases ReportViewer en Visual Studio: una para el desarrollo de escritorio y otra para el desarrollo Web. Hablaré sólo de la variante de escritorio en este capítulo. El diseñador acostumbrado al desarrollo de estos informes no diferenciará entre el destino del informe (escritorio o explorador). Hay algunas diferencias en la implementación, pero tendré que dejar la Web para un futuro libro de programación de altas ventas, o para que usted haga su propia investigación. El control MicrosoftReportViewer se integra directamente con Microsoft SQL Server Reporting Services, desplegando páginas completas generadas por ese sistema basado en servidor. Como estamos suponiendo que usted está empleando SQL Server Express Edition para su desarrollo (que no incluye Reporting Services), me concentraré en cambio en la presentación del modo “local” del control. Esto le permite desplegar cualquier dato desde cualquier fuente que elija en cada página de despliegue de informes, incluido SQL Server. En la línea de “Quienes sepan, háganlo; quienes no puedan, enseñen”, recorreremos los pasos necesarios para diseñar visualmente un informe simple empleando la clase ReportViewer. Crearemos un informe que presente listas de los registros en la tabla Actividad del Proyecto Biblioteca, una tabla que tendrá datos, en caso de que aún no haya usado el programa Biblioteca. Esto funciona mejor si lo sigue frente al equipo de cómputo, porque leer acerca del diseño de 584 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 584 17/2/10 15:32:20 informes es como leer sobre cirugía cerebral: es más interesante si la practica. Empiece por crear una nueva aplicación de Windows Forms. Y ahora, las malas noticias El control MicrosoftReportViewer no es el control más fácil de usar del mundo, pero es aún más difícil de usar cuando ni siquiera viene con su copia de Visual Studio. Si está usando Visual Basic 2008 Express Edition, no encontrará el control MicrosoftReportViewer en su cuadro de herramientas. Microsoft lo pone a su disposición como una descarga separada (acceda al área de descargas de Microsoft, http://www.microsoft.com/downloads, y revise “Microsoft Report Viewer Redistributable 2005 SP1”), pero eso sólo lo dejará a la mitad del camino. Estaré analizando un diseñador de informes visual que tampoco es parte de la Express Edition. Aunque aún pueda crear manualmente el contenido XML que suele generarse en el diseñador visual, no es nada divertido. Si está usando la Express Edition, aún puede utilizar el código de proyecto de este libro. No podrá diseñar visualmente nuevos informes. Pero sí podrá ejecutar los informes prescritos que ya incluí, porque sólo tienen contenido XML. Si, después de todo eso, aún es un usuario de la Express Edition, por favor descargue e instale el archivo Microsoft Report Viewer Redistributable 2005 SP1 del sitio Web de Microsoft. Adición de un origen de datos Agregue un origen de datos al proyecto que hace referencia a la tabla Actividad de la base de datos. Ya hicimos esto en el capítulo 10, en la sección “Creación de un origen de datos”. Seleccione el comando de menú Datos → Agregar nuevo origen de datos, y use el Asistente para la configuración de orígenes de datos para localizar su base de datos Biblioteca. Cuando alcance la lista de objetos de base de datos, revise el cuadro junto a la tabla Actividad y haga clic en el botón Finalizar. Ahora debe tener un origen de datos llamado BibliotecaDataSet. En la figura 21-1 se muestran los elementos agregados al Explorador de soluciones y el panel Orígenes de datos para esta acción. Figura 21-1. El BibliotecaDataSet como un origen de datos, y como un archivo XML “.xsd”. Uso de controles de informe en .NET | 585 21_PATRICK-CHAPTER_21.indd 585 17/2/10 15:32:21 Adición de una superficie de diseño de informes Use el comando de menú Proyecto → Agregar nuevo elemento, para agregar un nuevo elemento “Informe”. En la figura 21-2 se muestra el elemento de informe en el cuadro de diálogo Agregar nuevo elemento. Asegúrese de elegir Informe y no Crystal Report de la lista. Figura 21-2. Adición de un nuevo informe al proyecto. Haga clic en el botón Agregar para insertar el informe en el proyecto. Aparece un nuevo archivo Report1.rdlc en su proyecto, y su diseñador se abre automáticamente. “RDLC” es la abreviatura de lenguaje de definición de informes de cliente (Report Definition Lenguaje – Client) y los archivos de este tipo contienen XML que describe el diseño de un informe diseñado en el equipo local. En la figura 21-3 se muestra el diseñador para el archivo Report1.rdlc agregado, además de los controles en la barra de herramientas que puede agregar a la superficie del informe. Haré referencia a los informes creados mediante este diseñador como “informes RDLC” en el resto de este capítulo. Figura 21-3. El diseñador de informes y la barra de herramientas relacionada. 586 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 586 17/2/10 15:32:22 Diseño de la superficie del informe Si ha escrito informes en Microsoft Access o en alguna otra herramienta común de creación de informes, tal vez ya esté familiarizado con los informes “en bandas”. Estos informes tienen “bandas” o tiras separadas que representan una parte de la página impresa. Las bandas incluyen encabezados y pies de página, encabezados y pies de informe, la sección de detalle del registro y encabezados y pies de grupo usados para agrupar de manera visual y lógica las entradas de detalle. Mientras se ejecutan los informes, una línea imaginaria horizontal a lo largo de la página corre de la parte superior a la inferior de la página. Cuando la línea llega a cada banda, el informe procesa los campos en esa banda hasta que ya no hay más registro que procesar. Los RDLC son un poco diferentes de los informes en bandas. Sólo tienen tres bandas: encabezado de página, pie de página y todo lo demás (una banda llamada “Cuerpo”). En lugar de agregar bandas para registros y grupos, se agregan regiones de datos. Estos controles especiales procesan los registros vinculados con el informe de acuerdo con la forma de la región de datos. Hay cuatro controles de región de datos en el cuadro de herramientas. Table Esta región presenta un número ilimitado de filas, pero con un conjunto predefinido de columnas de datos. Está diseñada para presentación tabular de registros de datos, y cada columna suele desplegar un solo campo de datos de origen o calculados. Cada fila de la tabla representa un registro de datos de origen. Matrix Este control es similar a la región Table, pero permite un número flexible de columnas de datos, no sólo filas. List La región List proporciona una sección de despliegue de forma libre para cada registro entrante. Puede agregar cualquier número de campos o controles de despliegue a la sección de registro. Chart Chart usa los datos recolectados del informe para presentar gráficas de línea, barras y pastel al usuario. Los registros de los conjuntos de datos están siempre unidos a regiones de datos. Si su informe incluye datos de varios orígenes de datos distintos, cada origen se vinculará exactamente con una región de informe, y todas las regiones aparecerán en la banda Cuerpo. Usaremos una región de datos Lista para este informe de ejemplo. Siga adelante y agregue un control Lista a la banda Cuerpo de la superficie del informe. Ahora puede agregar otros elementos a la propia banda o a la superficie del control Lista. Los elementos agregados al control Lista se reprocesan para cada registro en el origen de datos entrante. Estos elementos pueden ser controles del cuadro de herramientas o campos de la base de datos desplegados en el panel Orígenes de datos. Mediante el uso de la tabla Actividad en el panel Orígenes de datos, arrastre el campo NombreCompleto a la superficie del control Lista. En la figura 21-4 se muestra el despliegue justo después de realizar esta operación de arrastre. Uso de controles de informe en .NET | 587 21_PATRICK-CHAPTER_21.indd 587 17/2/10 15:32:22 Figura 21-4. Un control Lista con un campo de un conjunto de datos. Cuando arrastramos el campo del origen de datos al control Lista, Visual Studio establece un vínculo entre ellos. El campo DataSetName del control list1 ahora hace referencia a BibliotecaDataSet_Actividad, el nombre del origen de datos. También se agrega un control TextBox a la superficie de la lista, y se agrega una expresión (=Fields!NombreCompleto.Value) que despliega el contenido del campo de la base de datos para cada registro procesado. Voy a cambiar el tamaño del control Lista, el cuadro de texto y la banda Cuerpo para que el campo del cuadro de texto NombreCompleto sea casi todo lo que hay en informe (véase la figura 21-5). Figura 21-5. Una versión con un nuevo tamaño del informe. El informe está listo para usarse. Mientras diseñábamos la superficie del informe, Visual Studio estaba ocupado generando XML y almacenando en el archivo Report1.rdlc. Uso de un control de informe El archivo RDLC es sólo una definición XML de un informe, no tiene la capacidad de desplegarse a sí mismo. Para ver el informe, debemos agregar un control de informe a un formulario o una página Web que sepa cómo combinar apropiadamente el contenido de diseño XML con los datos del origen de datos especificado. Regrese a Form1 y agregue un control MicrosoftReportViewer a su superficie a partir del cuadro de herramientas (está en la sección Informe del cuadro de herramientas en mi sistema). El control agregado incluye un pequeño botón de “etiqueta inteligente” en su esquina superior derecha. Al hacer clic en este botón se despliega la ventana lateral tareas de ReportViewer Tasks, que aparece en la figura 21-6. El control MicrosoftReportViewer presenta una experiencia basada en formulario para despliegue de informes. La mayor parte del control es un área en blanco donde aparece el informe. También incluye una barra de herramientas usada para navegar por las páginas del informe. El usuario también pude iniciar una exportación o la impresión de un informe mediante estos controles. Si no necesita la barra de herramientas o uno de sus controladores, use las diversas propiedades Show... del control MicrosoftReportViewer para ocultar los elementos innecesarios. 588 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 588 17/2/10 15:32:23 Figura 21-6. El control MicrosoftReportViewer en la superficie del formulario. El visor de informes es genérico e independiente del informe. Si tiene archivos RDLC en su proyecto, puede desplegar cualquiera de ellos (uno a la vez) mediante el mismo visor de informes. Sólo tenemos un informe en nuestro proyecto, de modo que conectémoslo (InformeSimple.Report1.rdlc) al visor usando la tarea Elegir informe, del botón de etiqueta inteligente del visor. Además, haga clic en la tarea “Anclar en contenedor principal” en la ventana lateral, para expandir el informe al tamaño del formulario. El informe RDLC, los datos del origen y el control MicrosoftReportViewer están todos unidos en un glorioso despliegue de informe por la magia de la unión de datos. Cuando vinculó el informe al control del visor, aparecieron tres controles más en el formulario: BibliotecaDataSet, ActividadBindingSource y ActividadTableAdapter. BibliotecaDataSet es una referencia al origen real de datos que agregamos antes. Los otros dos controles envuelven esos datos en un formulario que puede unirse al visor de informes. Aunque no puede verlo en el diseñador, el código del formulario oculto conecta estos controles y el informe XML al visor. ReportDataSource1.Name = "BibliotecaDataSet_Actividad" ReportDataSource1.Value = Me.ActividadBindingSource Me.ReportViewer1.LocalReport.DataSources.Add( _ ReportDataSource1) Me.ReportViewer1.LocalReport.ReportEmbeddedResource = _ "InformeSimple.Report1.rdlc" Sí, tal vez no comprendo bien. Pero es correcto. Visual Studio conectó todo. Ejecución del informe Oprima F5 y vea los resultados de sus esfuerzos. En la figura 21-7 ajusté la vista al hacer clic en el botón de la barras de herramientas Configurar página y establecí el nivel de acercamiento en Ancho de página. Bueno, ese informe es adecuado, en lo que se refiere a la tabla Actividad, pero lo podemos aderezar un poco más. Uso de controles de informe en .NET | 589 21_PATRICK-CHAPTER_21.indd 589 17/2/10 15:32:23 Figura 21-7. Informe del contenido esencial de la tabla Actividad. Adición de un encabezado y un pie de página Creo que el informe necesita un título que signifique algo en la parte superior de cada página, además de un número en la esquina inferior derecha. Regresemos al diseñador de informes RDLC y agreguémoslos. Una vez allí, haga clic con el botón derecho en el fondo del informe (no en el cuerpo, que es la cuadrícula con marcas) como se muestra en la figura 21-8. Figura 21-8. Adición de encabezados y pies de página. Desde este menú, seleccione Encabezado de página, luego despliegue el menú de nuevo y seleccione Pie de página. Cada nueva banda aparece en la superficie del informe. Si es texto estático, sin cambio, o texto que se genere dinámicamente a partir de un origen de datos, el control Cuadro de texto es el que debe elegir para mostrar el contenido de texto. Agregue un control Cuadro de texto del cuadro de herramientas para las secciones de encabezado y pie de página. Haga clic dentro del cuadro de texto del encabezado y escriba lo siguiente: ="El informe de la tabla Actividad" Puede usar el panel Propiedades para ajustar el aspecto de este control, incluida su fuente de despliegue. En el cuadro de texto del pie, agregue este texto: ="Page " & Globals!PageNumber 590 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 590 17/2/10 15:32:24 El seudoobjeto Globals incluye unos cuantos miembros que puede usar en el informe. ¿Cómo supe que debo usar Globals!PageNumber? Construí la expresión visualmente empleando el Editor de expresiones. Para accederlo, haga clic con el botón derecho en el control Cuadro de texto y seleccione Expresión, del menú contextual. El editor, que se muestra en la figura 21-9, le permite generar una expresión usando listas de funciones y nombres de campo. Las funciones reales sólo son (¡hurra!) funciones de Visual Basic. Figura 21-9. El editor de expresiones. Soporte para agrupación y ordenamiento La agrupación de datos es común en informes impresos. Para agregar agrupaciones a nuestro informe, necesitamos incrustar nuestro control Lista existente (del registro de detalle) dentro de otro control Lista (del grupo) y establecer varias propiedades en el control Lista del grupo para determinar el método de agrupación de datos. Probémoslo. Agregue otro control Lista (llamado list2) al cuerpo del informe y déle el doble del alto del control Lista existente (llamado list1). Luego, arrastre list1 (el registro de detalle) a list2 (el nuevo grupo), colocándolo hacia la parte inferior. Su informe debe parecerse al de la figura 21-10. Para configurar el grupo, haga clic con el botón derecho en él y seleccione Propiedades del menú contextual. Aparece el formulario Propiedades de la lista. En su ficha General, haga clic en el botón “Editar grupo de detalles”, que establece la agrupación. En el formulario Propiedades de agrupación y ordenación, que aparece, ingrese el siguiente texto en la primera fila del campo “Agrupar por”: =Left(Fields!NombreCompleto.Value, 1) Uso de controles de informe en .NET | 591 21_PATRICK-CHAPTER_21.indd 591 17/2/10 15:32:25 Figura 21-10. Una lista de agrupación agregada al informe. Esta expresión le indica al control list2 que agrupe sus resultados de detalle por el primer carácter del campo de nombre. En este mismo formulario, agregue el siguiente texto al campo “Etiqueta de mapa de documento”: ="Letter: " & Left(Fields!NombreCompleto.Value, 1) El mapa de documento habilita una lista de hipervínculo en que se puede hacer clic en los diferentes grupos del informe. Cuando ejecutemos el informe un poco después, veremos este mapa a la izquierda de la superficie de despliegue del informe. Los registros de la tabla Actividad están ordenados para conveniencia del programador (yo). Pero el usuario del informe tal vez quiera verlos ordenados en la misma manera razonable. Haga clic en la ficha Ordenación y agregue el siguiente texto en el campo “Ordenar por”, en la columna Expresion: =Fields!NombreCompleto.Value Como es de esperar, esto ordenará los datos por el campo NombreCompleto. Haga clic en los botones Aceptar hasta salir de todos los cuadros de diálogo y regresar a la superficie del informe. Aún necesitamos agregar algo que hará que cada grupo se destaque. Agregue un control Cuadro de texto al control de agrupación list2. Póngalo en la esquina superior izquierda de ese control principal, y escriba el siguiente texto en él (o en su propiedad Value): =Left(Fields!NombreCompleto.Value, 1) También establecí su propiedad BackgroundColor en “Black”, su propiedad Color en “White” y su propiedad Font en “Normal, Arial, 12pt, Bold” para mejorar el aspecto. Al ejecutar el informe se presentan los resultados de la figura 21-11. Observe el mapa del documento a lo largo de la orilla izquierda de la ventana, y los títulos agrupados de una sola letra antes de cada sección agrupada. Formato de estilo mejorado Tal vez la característica más agradable de los informes RDLC es que muchas de las propiedades para elementos colocadas en la superficie del informe pueden incluir expresiones condicionales. Esto significa que puede alterar condicionalmente, digamos, las propiedades visuales de un control Cuadro de texto con base en el valor de un campo en el registro actual. 592 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 592 17/2/10 15:32:25 Figura 21-11. El informe completo, con agrupación y ordenamiento habilitados. En la sección “Proyecto” de este capítulo escribiremos un informe que usa fechas de vencimiento para artículos prestados. Si el artículo se encuentra vencido, quiero mostrar la fecha de vencimiento en rojo. Por lo general, la propiedad Color del control Cuadro de texto (que controla el color de fuente) es “Black”. Para que el campo responda a los artículos vencidos, reemplazaré “Black” con la siguiente expresión: =IIf(Fields!FechaVencimiento.Value < Today, "Red", "Black") Uso de datos personalizados Aunque es muy común generar informes a partir de bases de datos, en realidad puede usar datos de casi cualquier origen. Cuando usa el control MicrosoftReportViewer, cualquier origen de datos que implemente la interfaz IEnumerable es buena. Eso incluye todas las colecciones, matrices y resultados de consulta LINQ. El informe no es tan exigente, siempre y cuando los datos estén formados como lo espera. Para el informe que acabamos de hacer, podemos hacer a un lado los datos reales y proporcionar nuestros propios datos falsos. Esta intercepción y sustitución de datos es algo sacado de una novela de espías. Pero debemos seguir unas cuantas reglas para que funcione: • Cuando arrastramos el campo Actividad.NombreCompleto del origen de datos a la superficie del informe, éste (en realidad, el control list1) tiene esta idea divertida de que todos los datos tienen que venir de un origen de datos llamado BibliotecaDataSet_Actividad. Cualquier origen de datos que usemos en lugar del real debe tener este nombre. • El origen de los datos falsos debe incluir el campo NombreCompleto, porque es lo que esperan los campos del informe. Esas reglas no están mal. Así que esto es lo que necesitamos hacer: crear un origen de datos falso, interceptar el informe justo antes de que trate de obtener los datos de la base de datos Biblioteca e insertar, en cambio, nuestros propios datos. Uso de controles de informe en .NET | 593 21_PATRICK-CHAPTER_21.indd 593 17/2/10 15:32:26 Para un origen de datos falso, necesitaremos una clase que incluya por lo menos el campo NombreCompleto. Public Class RegistroActividadFalso Private IDAlmacenado As Long Private NombreCompletoAlmacenado As String Public Sub New(ByVal cualID As Long, _ ByVal cualNombreCompleto As String) IDAlmacenado = cualID NombreCompletoAlmacenado = cualNombreCompleto End Sub Public Property ID( ) As Long Get Return IDAlmacenado End Get Set(ByVal valor As Long) IDAlmacenado = valor End Set End Property Public Property NombreCompleto( ) As String Get Return NombreCompletoAlmacenado End Get Set(ByVal valor As String) NombreCompletoAlmacenado = valor End Set End Property End Class Los campos expuestos deben ser propiedades, y no sólo campos públicos; el visor de informes no reconoce campos de miembro estándar. Si revisa el código fuente de Form1, encontrará que el código siguiente se agregó al manejador de eventos Form_Load cuando vinculamos el visor del informe con el informe RDLC: Me.ActivityTableAdapter.Fill(Me.BibliotecaDataSet.Actividad) Me.ReportViewer1.RefreshReport( ) Es la primera línea que carga los datos de la tabla Actividad de la base de datos Biblioteca, y lo vincula con el informe. Necesitamos reemplazar estas dos líneas generadas por el asistente con código que corte los datos reales a cada paso. ' ----- Crear una tabla falsa de registros falsos. Dim origenFalso As New Collections.Generic.List( _ Of RegistroActividadFalso) ' ----- Agregar cada uno de los registros falsos. origenFalso.Add(New RegistroActividadFalso(1, "Ponte a trabajar")) origenFalso.Add(New RegistroActividadFalso(2, "Toma una siesta")) origenFalso.Add(New RegistroActividadFalso(3, "Escribe un programa")) ' ----- El informe esta listo para unirse al origen de datos 594 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 594 17/2/10 15:32:26 ' real. Eliminarlo. Me.ReportViewer1.LocalReport.DataSources.Clear( ) ' ----- Generar un nuevo origen de datos. Recuerdese que debe ' tener el mismo nombre. Dim origenInformeFalso As New _ Microsoft.Reporting.WinForms.ReportDataSource origenInformeFalso.Name = "BibliotecaDataSet_Actividad" origenInformeFalso.Value = origenFalso ' ----- Conectar el origen de datos al informe y es todo. Me.ReportViewer1.LocalReport.DataSources.Add(origenInformeFalso) Me.ReportViewer1.RefreshReport( ) En la figura 21-12 se muestra el informe con los datos falsos en el despliegue. Figura 21-12. Estos datos falsos no se autodestruirán en cinco segundos. Suministro de orígenes de datos personalizados La sustitución de datos en el último segundo es buena, pero ¿qué pasa si quiere diseñar un informe que no dependa en absoluto de la base de datos? También puede hacerlo, al proporcionar un origen de datos completamente personalizado. Los informes RDLC requieren una especie de esquema de origen de datos en tiempo de diseño; sólo que no puede suministrar datos completamente personalizados al vuelo cuando se ejecute el informe. Pero sí puede proporcionar un esquema basado en una clase en su aplicación. En el caso de la clase, nos apegaremos al RegistroActividadFalso que creamos en la sección anterior. Luego diseñaremos un origen de datos de esta clase. Seleccione el comando de menú Datos → Agregar nuevo origen de datos. Cuando ha aparecido el Asistente para la configuración de orígenes de datos en el pasado, siempre ha seleccionado Base de datos como origen de los datos. Esta vez, seleccione Objeto, como se muestra en la figura 21-13. Cuando haga clic en el botón Siguiente, aparecerá una jerarquía de todas las clases de su aplicación. Expanda las clases, luego localice y seleccione RegistroActividadFalso. Haga clic en el botón Finalizar. Ahora RegistroActividadFalso aparece como un origen de datos en el panel Orígenes de datos. Uso de controles de informe en .NET | 595 21_PATRICK-CHAPTER_21.indd 595 17/2/10 15:32:26 Figura 21-13. Creación de un origen de datos basada en un objeto personalizado. Ahora puede arrastrar y colocar este campo NombreCompleto del origen de datos en la superficie de diseño del informe RDLC. Agregue un nuevo informe a su proyecto y siga los mismos pasos que usamos antes para diseñar el primer informe. Esta vez, use el origen de datos RegistroActividadFalso en lugar de BibliotecaDataSet. Para probar este nuevo informe, eliminé el Form1 original del proyecto y agregué un nuevo Form1. También agregué un control MicrosoftReportViewer a su superficie y lo anclé, pero no lo vinculé con el informe RDLC. Esto mantiene las cosas un poco más limpias mientras que no hay controles de origen de unión y cosas por el estilo de qué preocuparse. Luego agregué este código al manejador de eventos Load: ' ----- Vincular con el diseño del informe RDLC. Me.ReportViewer1.LocalReport.ReportEmbeddedResource = _ "InformeSimple.Report2.rdlc" ' ----- Crear una tabla falsa de registros falsos. Dim origenFalso As New Collections.Generic.List( _ De RegistroActividadFalso) ' ----- Agregar cada uno de los registros falsos. origenFalso.Add(New RegistroActividadFalso(1, "Desayuno")) origenFalso.Add(New RegistroActividadFalso(2, "Comida")) origenFalso.Add(New RegistroActividadFalso(3, "Cena")) ' ----- Generar un nuevo origen de datos. Recuerde que debe tener ' el mismo nombre. Dim origenInformeFalso As New _ Microsoft.Reporting.WinForms.ReportDataSource origenInformeFalso.Name = "InformeSimple_RegistroActividadFalso" origenInformeFalso.Value = origenFalso 596 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 596 17/2/10 15:32:27 ' ----- Conectar el origen de datos al informe y es todo. Me.ReportViewer1.LocalReport.DataSources.Add(origenInformeFalso) Me.ReportViewer1.RefreshReport( ) Es muy similar al código personalizado anterior, aunque el nombre del origen de datos es ahora InformeSimple_RegistroActividadFalso, el nombre que este nuevo informe espera (lo que encontré al ejecutar el informe y leer el mensaje de error). He guardado una copia de ambos informes personalizados en el directorio de instalación de los ejemplos de código fuente del libro. Búsquela en el subdirectorio llamado InformeSimple. Resumen Aunque este capítulo incluyó muchas bonitas imágenes y una gran cantidad de instrucciones, sólo rascamos la superficie de las características disponibles en los controles de informes incluidos con .NET. Pienso que me raspé el cerebro cuando traté de estudiar cada característica disponible, pero tal vez su cerebro esté mejor preparado para la tarea. Aún así, si no lo encuentra exactamente a su gusto, puede usar una de las demás características de creación de informes que presenté al principio del capítulo, o aún optar por una solución de terceros. Los informes son una parte importante de la vida diaria del desarrollador de negocios. Encontrar la herramienta correcta de informes y sentirse cómodo con sus características no sólo es una buena sugerencia, es necesario en el mundo de los usuarios de software hambrientos de informes. Proyecto Cuando dejamos por última vez el documento Kit de recursos técnicos para el Proyecto Biblioteca, incluía cinco informes integrados: • Informe #1: Informe de artículos prestados • Informe #2: Informe de artículos vencidos • Informe #3: Informe de artículos faltantes • Informe #4: Informe de multas adeudadas por clientes • Informe #5: Informe de estadísticas de la base de datos de biblioteca En este capítulo agregaremos estos cinco informes al proyecto. Antes de escribir cualquier código, necesitamos imaginar cómo vamos a obtener los datos. Debido a que éstos provienen de la base de datos Biblioteca, sólo necesitamos generar la instrucción SQL para cada informe que se vinculará con el informe diseñado. El quinto informe, “estadísticas”, informará cosas como el número de artículos, el número de clientes y valores estadísticos similares de la base de datos Biblioteca. Como estos datos en realidad no pueden venir de una sola instrucción SQL, extraeremos los datos de la base de datos y generaremos un origen de datos personalizado que los alimente al informe. Proyecto | 597 21_PATRICK-CHAPTER_21.indd 597 17/2/10 15:32:27 Armado de las instrucciones SQL El primer informe, “artículos prestados”, presenta una lista del nombre del cliente y el título del artículo para cada archivo que ha pedido prestado el cliente. Incluye la tabla Cliente (para obtener el nombre del cliente), la tabla CopiaCliente (el evento de préstamo o salida), la tabla CopiaArticulo (el elemento real prestado) y la tabla ArticuloConNombre (donde aparece el título del elemento). También incluiremos la tabla CodigoTipoMedio, que nos indica si el elemento es un libro, un CD o algún otro tipo de medio. Microsoft SQL Server Management Studio Express incluye un diseñador de consulta que podemos usar para diseñar la consulta. En la figura 21-14 se muestran las cinco tablas necesarias como las vinculó el diseñador. Figura 21-14. Las cinco tablas en la consulta de artículos prestados. Si utiliza el diseñador de consultas o construye la instrucción SQL a mano, con el tiempo saldrá con algo similar a lo siguiente, que usaremos dentro de la aplicación Biblioteca: /* ----- Informe #1: Informe de articulos prestados. */ SELECT PA.Apellido + ', ' + PA.Nombre AS NombreCliente, PA.CodigoBarras AS CodigoBarrasCliente, PC.FechaVencimiento, IC.CopiaNúmero, IC.CodigoBarras AS CodigoBarrasArticulo, NI.Title, CMT.NombreCompleto AS NombreMedio FROM Cliente AS PA INNER JOIN CopiaCliente AS PC ON PA.ID = PC.Cliente INNER JOIN CopiaArticulo AS IC ON PC.CopiaArticulo = IC.ID INNER JOIN ArticuloConNombre AS NI ON IC.IDArticulo = NI.ID INNER JOIN CodigoTipoMedio AS CMT ON NI.TipoMedio = CMT.ID WHERE PC.Regresado = 0 AND PC.Faltante = 0 AND IC.Faltante = 0 ORDER BY NI.Titulo, IC.CopiaNúmero, PA.Apellido, PA.Nombre Esta consulta vincula todas las tablas, y luego solicita cada registro que no se ha regresado (PC. Regresado = 0). Ignora cualquier artículo marcado como faltante (PC.Faltante = 0 AND IC.Faltante = 0). Esta consulta con el tiempo orienta el informe. Pero por ahora, tenga en 598 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 598 17/2/10 15:32:27 mente que los informes RDLC en realidad no necesitan una instrucción SQL real o una tabla de base de datos para el esquema del informe. También podemos construir un esquema compatible a mano usando una clase. Esto resulta que es un poco más limpio porque no tenemos muchos archivos relacionados con conjuntos de datos dispersos en todo el código fuente del proyecto. (¡El origen de datos BibliotecaDataSet que creamos en el informe de ejemplo en este capítulo agregó cuatro archivos y casi 50 KB de código fuente al proyecto, sin contar el informe RDLC! El origen de datos basado en la clase no agregó otro código que la propia definición de clase, y un poco de XML al archivo RDLC.) En cuanto al esquema de origen de datos, podemos extrapolarlo a partir de la cláusula SELECT de la consulta SQL. Si fuéramos a diseñar una clase con un esquema coincidente, tendría un aspecto como éste (sin el código de detalle de propiedades): Class EsquemaInforme1 Public Property NombreCliente As String Public Property CodigoBarrasCliente As String Public Property FechaVencimiento As Date Public Property CopiaNumero As Integer Public Property CodigoBarrasArticulo As String Public Property Titulo As String Public Property NombreMedio As String End Class Los dos informes siguientes son para “artículos vencidos” y “artículos faltantes”. Para mí, el esquema del informe #1 es exactamente lo que quiero ver en estos otros dos informes, así que sólo usemos la misma instrucción SQL. Todo lo que necesitamos hacer es cambiar la cláusula WHERE. Para el informe de artículos vencidos, use esta cláusula WHERE: WHERE PC.Regresado = 0 AND PC.Faltante = 0 AND IC.Faltante = 0 AND PC.FechaVencimiento < GETDATE( ) Los artículos faltantes usarán esta cláusula WHERE: WHERE PC.Faltante = 1 Or IC.Faltante = 1 El cuarto informe despliega el monto de multas que aún deben los clientes, de modo que necesitará un esquema diferente. He aquí su instrucción SQL, que usa algunas características de agrupación agregados: /* ----- Informe #4: Informe de multas adeudadas por clientes. */ SELECT PA.Apellido + ', ' + PA.Nombre AS NombreCliente, PA.CodigoBarras AS CodigoBarrasCliente, SUM(PC.Multa - PC.Pagado) AS MultasVencidas FROM Cliente AS PA INNER JOIN CopiaCliente AS PC ON PA.ID = PC.Cliente GROUP BY PA.Apellido + ', ' + PA.Nombre, PA.CodigoBarras HAVING SUM(PC.Multa - PC.Pagado) > 0 ORDER BY NombreCliente Proyecto | 599 21_PATRICK-CHAPTER_21.indd 599 17/2/10 15:32:28 He aquí el esquema que va en el informe #4: Class EsquemaInforme4 Public Property NombreCliente As String Public Property CodigoBarrasCliente As String Public Property MultasVencidas As Decimal End Class Para el informe final, sólo usaremos un esquema con dos valores de cadena: un nombre estático y su valor relacionado. He aquí su esquema: Class EsquemaInforme5 Public Property NombreEntrada As String Public Property ValorEntrada As String End Class Bueno, eso es suficiente preparación. Empecemos la codificación. Acceso al proyecto Cargue el proyecto Cap21 (Antes) código mediante las plantillas de Nuevo proyecto o accediendo directamente al proyecto desde el directorio de instalación. Para ver el código en su forma final, cargue, en cambio, Cap21 (Final) código. Adición de esquemas de informe El archivo EsquemasInformes.vb, ya agregado al proyecto, incluye los tres esquemas usados para los cinco informes integrados. Sólo para recordar los miembros, he aquí las definiciones de propiedades públicas incluidas en cada clase, menos Get y Set, y menos los miembros de clase privados: Public Class EsquemaInformeArticulosCliente ' ----- Usado por los siguientes informes: ' Informe #1: Informe de articulos prestados ' Informe #2: Informe de articulos vencidos ' Informe #3: Informe de articulos faltantes Public Property NombreCliente( ) As String Public Property CodigoBarrasCliente( ) As String Public Property FechaVencimiento( ) As Date Public Property NumeroCopias( ) As Integer Public Property CodigoBarrasArticulo( ) As String Public Property Titulo( ) As String Public Property NombreMedio( ) As String End Class Public Class EsquemaInformeMultasCliente ' ----- Usado por los siguientes reportes: ' Informe #4: Informe de multas adeudadas por cliente Public Property NombreCliente( ) As String Public Property CodigoBarrasCliente( ) As String Public Property MultasVencidas( ) As Decimal End Class 600 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 600 17/2/10 15:32:28 Public Class EsquemaInformeEstadistica ' ----- Usado para los siguientes reportes: ' Informe #5: Informe de estadisticas de la base de datos de biblioteca Public Property NombreEntrada( ) As String Public Property ValorEntrada( ) As String End Class Una vez que las clases de esquema están en el proyecto, necesitará generar el proyecto antes de que esas clases puedan usarse en informes RDLC como orígenes de datos. En el proyecto Biblioteca, genere el proyecto ahora con el comando de menú Generar → Generar Biblioteca. Los tres esquemas deben entonces aparecer como orígenes en el panel Orígenes de datos (véase la figura 21-15). Si el panel Orígenes de datos está cerrado, ábralo usando el comando de menú Datos → Mostrar orígenes de datos. Figura 21-15. Los tres esquemas de origen de datos. Adición de informes Como ya creamos juntos un informe RDLC al principio del capítulo, seguí adelante y agregué los cinco informes integrados: InformePrestados.rdlc Este archivo implementa el informe #1, el informe “artículos prestados”. Usa el esquema de clase EsquemaInformeArticulosCliente, e incluye tres columnas en los datos principales: nombre de cliente/código de barras, nombre de artículo/código de barras/detalles, y fecha de vencimiento. Para el campo de nombre de artículo, quería presentar información adicional cuando esté disponible. El nombre del artículo, el número de copias y el tipo de medios son valores obligatorios, pero el código de barras de artículo es opcional. He aquí el formato que deseaba: Nombre articulo (#CopiaNumero, TipoMedio, CodigoBarras) Proyecto | 601 21_PATRICK-CHAPTER_21.indd 601 17/2/10 15:32:28 Para obtener este resultado, tuve que unir los diversos campos de origen, y usé una función condicional (IIf) para incluir de manera opcional el código de barras y su coma: =Fields!Titulo.Value & " (#" & ↵ CStr(Fields!CopiaNumero.Value) & ", " & ↵ Fields!NombreMedio.Value & ↵ IIf(IsNothing(Fields!CodigoBarrasArticulo.Value), "", ↵ ", " & Fields! CodigoBarrasArticulo.Value) & ")" Como ya lo mencioné, el campo de fecha de vencimiento tiene una expresión en su propiedad Color que vuelve el texto rojo cuando el artículo se vence. InformeVencidos.rdlc Este informe muestra una lista de todos los artículos vencidos en el sistema. Como todo estará vencido, establecí que el campo de fecha de vencimiento use siempre rojo como fuente de color. Aparte de eso y el título, el informe es básicamente idéntico al informe de artículos prestados. InformeFaltantes.rdlc Este informe muestra una lista de todos los artículos marcados como faltantes. Aunque el esquema incluye un campo de fecha de vencimiento, no lo uso en este informe. El resto del informe es básicamente idéntico al informe de artículos vencidos. InformeMultasClientes.rdlc Este informe muestra una lista de todos los clientes que aún deben multas, y la cantidad de la multa adeudada. Usa el esquema de clase EsquemaInformeArticulosCliente. El campo que despliega la multa tiene una “C” en su propiedad Formato. Esta configuración de formato fuerza el valor decimal para que se despliegue como moneda usando los parámetros culturales del sistema local. Esta propiedad Format usa los mismos códigos reconocidos por el método String.Format. InformeEstadisticas.rdlc El informe #5 despliega la cuenta de registros a partir de las tablas en la base de datos Biblioteca. Éste es el único informe que usa el esquema de clase EsquemaInformeEstadisticas. El propio informe sólo despliega dos cadenas por registro: un nombre y un valor. Depende del código de llamada para formar de manera apropiada esos campos. Adición de un visor de informes Es hora de agregar un control MicrosoftReportViewer. Como un solo control Microsoft ReportViewer puede desplegar cualquier tipo de informe RDLC, sólo agregaremos un solo formulario para manejar los cinco informes integrados. Agregue un nuevo formulario llamado InformeVisorIntegrado.vb al proyecto. Establezca su propiedad Text en Informe de Biblioteca y su propiedad WindowState en Maximized. Además, cargue el ícono proyecto (Libro.ico) en la propiedad Icon. Encontrará una copia de este archivo en el directorio de instalación del proyecto. Si quiere, puede cambiar el tamaño del formulario a un punto de partida razonable para el informe (usé “680, 400”), pero cada informe empezará maximizado cuando se use. 602 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 602 17/2/10 15:32:28 Agregue un control MicrosoftReportViewer llamado InformeContenido al formulario, y establezca su propiedad Dock en Fill. Asigne False a las propiedades ShowBackButton y ShowDocumentMapButton. El código que agregaremos a este formulario es una variación del código que escribimos antes en este capítulo. El código que empieza cada informe pasará a este formulario el nombre del archivo de informe RDLC, el nombre del esquema de datos usado y los datos reales. Debido a que estos informes son sin modo (puede mantenerlos abiertos mientras aún usa otras partes del programa Biblioteca), no podemos permitir que el código de llamada espere a que el usuario cierre el informe antes de que descartemos los datos de informe. Dejemos que el informe deseche los datos. Para esto, necesitamos mantener una referencia a esos datos. Agregue la siguiente instrucción a la clase del formulario InformeVisorIntegrado. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 1. Private AlmacenarTablaDatos As Object Recuerde que los informes pueden usar diversos formatos de origen de datos, incluidas conexiones de base de datos, matrices y colecciones. De informe #1 a #4 usarán una instancia System. Data.DataTable y el informe #5 pasará una colección genérica Lista. El mejor momento para desechar los datos es cuando se cierra el informe. Agregue el siguiente manejador de eventos al formulario, que confirma que los datos dan soporte al proceso de desecho antes de llamar al método Dispose. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 2. Private Sub InformeVisorIntegrado_FormClosing( _ ByVal sender As Object, ByVal e As _ System.Windows.Forms.FormClosingEventArgs) _ Handles Me.FormClosing ' ----- Deshacerse de los datos. If (TypeOf AlmacenarTablaDatos Is IDisposable) Then CType(AlmacenarTablaDatos, IDisposable).Dispose( ) End If End Sub El código que abre este formulario de despliegue pasará en los valores de informe esenciales mediante un método público llamado IniciarInforme. Agregue su código ahora. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 3. Proyecto | 603 21_PATRICK-CHAPTER_21.indd 603 17/2/10 15:32:28 Public Sub IniciarInforme(ByVal cualInforme As String, _ ByVal cualEsquemaDatos As String, _ ByVal cualesDatos As Object) ' ----- Ejecutar uno de los informes integrados. cualInforme es ' el nombre del archivo de informe RDLC, en el formato ' "Biblioteca.xxx.rdlc." cualEsquemaDatos proporciona el ' nombre del esquema a usar, en el formato "Biblioteca_xxx". ' cualConjntoDatos son los datos reales que se vinculan ' con el informe, que debe coincidir con el esquema. Dim origenDatosPersonalizados As New _ Microsoft.Reporting.WinForms.ReportDataSource ' ----- Conectar el visor, el informe y los datos. ReportContent.LocalReport.ReportEmbeddedResource = _ cualInforme origenDatosPersonalizados.Name = cualEsquemaDatos origenDatosPersonalizados.Value = cualesDatos ReportContent.LocalReport.DataSources.Add( _ origenDatosPersonalizados) ' ----- Desplegar el informe. AlmacenarTablaDatos = cualesDatos Me.Show( ) End Sub Este código le indica al visor cuál informe usar como un recurso incrustado y luego adjunta los datos como un origen de datos personalizado. “Local” en estos nombres de propiedades indica un informe local (cliente) en lugar de un informe de “servidor” que se ejecute dentro de SQL Server. Cuando estábamos jugando antes con los informes, vimos que el modo de despliegue predeterminado era “rellenar toda la pantalla con contenido de página”. De manera personal, me gusta ver estos límites de página falsos. El control MicrosoftReportViewer no incluye una propiedad que nos permita cambiar esta vista predeterminada (¿por qué no?), pero aún podemos ajustar el estilo de despliegue inicial mediante métodos en el control. Cuando agregamos el visor de informe al formulario, Visual Studio también agregó la siguiente instrucción al manejador de eventos Load formulario: InformeContenido.RefreshReport( ) Agregue el siguiente código justo antes de esa instrucción. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 4. ' ----- Generar y desplegar el informe. InformeContenido.SetDisplayMode( _ Microsoft.Reporting.WinForms.DisplayMode.PrintLayout) InformeContenido.ZoomMode = _ Microsoft.Reporting.WinForms.ZoomMode.Percent InformeContenido.ZoomPercent = 100 604 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 604 17/2/10 15:32:28 Adición de informes integrados Ya olvidé hace cuánto agregué el formulario SeleccionarInforme.vb que dirige la creación de informes, pero ya está en el proyecto. En caso de que olvide su aspecto (yo lo hice), la figura 21-6 nos la recuerda. Figura 21-16. El formulario de selección de informes. Ya antes agregamos soporte para nuestros cuatro informes integrados en este código de formulario. En un tributo a la interminable realidad de olvidos para finalizar todo el código, necesitamos agregar algún código que subestimamos antes. Si utiliza un archivo de configuración de informe XML para rellenar la lista del informe, y proporciona una descripción para cada informe en XML, cada entrada despliega esa descripción en la mitad inferior del formulario de selección de informes. Pero si no utiliza un archivo de configuración, y sólo depende del formulario para agregar los cinco informes integrados como opción predeterminada (que lo hace), el formulario no desplegará descripciones asociadas, porque olvidamos agregarlos. Agregue una función a la clase SeleccionarInforme que devuelve una descripción corta para cada uno de los cinco informes. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 5. Private Function ObtenerDescripcionInformeIntegrado( _ ByVal cualInforme As ElementoInformeEnum) As String ' ----- Regresar una descripcion predefinida para los ' informes integrados. Select Case cualInforme Case ElementoInformeEnum.PrestadoIntegrado Return "Despliega todos los articulos prestados " & _ "ordenados por nombre." Case ElementoInformeEnum.VencidoIntegrado Return "Displiga todos los articulos vencidos, ordenados por nombre." Case ElementoInformeEnum.FaltanteIntegrado Return "Despliega todos los articulos faltantes, ordenados por nombre." Proyecto | 605 21_PATRICK-CHAPTER_21.indd 605 17/2/10 15:32:29 Case ElementoInformeEnum.MultasDebidasIntegrado Return "Despliega todas las multas no pagadas adeudadas por " & _ "clientes, ordenados por nombre de cliente." Case ElementoInformeEnum.estadisticasIntegrado Return "Despliega algunas cuentas de registros de la " & _ "base de datos Biblioteca." Case Else Return "No hay descripcion de este informe." End Select End Function Llamaremos a este código desde dos lugares. El primero es el método CargarGrupoInformes. Este código carga el archivo de configuración del informe XML. Si ese archivo incluye uno de los informes integrados, pero no proporciona una descripción con él, nosotros proporcionaremos la descripción. En la parte media de ese código encontrará estas líneas: ' ----- Entonces que tipo de entrada es? If (nodoRastreo.Attributes("type").Value = "integrado") Then En unas cinco líneas abajo se encuentra la siguiente instrucción: entradaInforme.TipoElemento = CType(CInt( _ entradaInforme.RutaInforme), ElementoInformeEnum) Agregue el siguiente código justo después de esa instrucción. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 6. If (entradaInforme.Descripcion = "") Then _ entradaInforme.Descripcion = _ ObtenerDescripcionInformeIntegrado(entradaInforme.TipoElemento) La segunda descripción integrada necesaria aparece en el método ActualizarListaInformes. Este método hace que la llamada a CargarGrupoInformes recupere la configuración en XML. Pero si después de eso la lista de informes aún está vacía, ActualizarListaInformes agrega los cinco informes predeterminados, cada uno de los cuales requiere una descripción. Cerca del final del método, dentro de un bucle For...Next, encontrará esta instrucción de cierre: ' ----- Agregar la entrada del informe a la lista. TodosLosInformes.Items.Add(entradaInforme) Agregue el siguiente código justo después de esa instrucción. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 7. EntradaInforme.Descripcion = ObtenerDescripcionInformeIntegrado( _ EntradaInforme.TipoElemento) 606 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 606 17/2/10 15:32:29 Muy bien, ése es todo el código de corrección. Ahora regresemos a escribir los informes reales. El código para empezar cada uno de los cinco informes ya existe en el manejador de eventos AccEjecutar_Click del formulario SeleccionarInforme. Casi todo ese código incluye una instrucción Select Case que actúa como tablero de selección para el informe seleccionado. He aquí la parte que llama a los cinco informes integrados: Case ' ' Case ' ' Case ' ' Case ' ' Case ' ' ElementoInformeEnum.PrestadoIntegrado ----- Articulos prestados TODO: Escribir InformeBasicoPrestado( ) ElementoInformeEnum.VencidoIntegrado ----- Articulos vencidos TODO: Escribir InformeBasicoVencido( ) ElementoInformeEnum.FaltanteIntegrado ----- Articulos faltantes TODO: Escribir InformeBasicoFaltante( ) ElementoInformeEnum.MultasDebidasIntegrado ----- Multas debidas por los clientes TODO: Escribir InformeBasicoMultas( ) ElementoInformeEnum.estadisticasIntegrado ----- Biblioteca de estadisticas de base de datos TODO: Escribir InformeBasicoEstadisticas( ) Evidentemente, este código no hace mucho. Cambie cada una de las líneas TODO, eliminando la parte TODO: Escribir de la instrucción. De modo que la línea que dice: ' TODO: Escribir InformeBasicoPrestado( ) cambie el código a: InformeBasicoPrestado( ) Haga esto con cada una de las cinco líneas TODO. La exposición de estas cinco llamadas a método significa que tenemos que escribir esos métodos, caray. Estos métodos recuperarán los datos para el informe, y envíe esos datos al visor de informes, junto con el nombre del archivo RDLC. En realidad es muy corto y simple, considerando los hermosos informes que obtendrá con ellos. Empecemos por agregar el método InformeBasicoPrestado a la clase SeleccionarInforme. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 8. Private Sub InformeBasicoPrestado( ) ' ----- Ejecutar el informe #1 integrado: informe de articulos prestados. Dim textoSql As String Dim datosInforme As Data.DataTable Dim formularioInforme As InformeVisorIntegrado On Error GoTo ErrorHandler ' ----- Recuperar los datos como un conjunto de datos. textoSql = "SELECT PA.Apellido + ', ' + " & _ Proyecto | 607 21_PATRICK-CHAPTER_21.indd 607 17/2/10 15:32:29 "PA.Nombre AS NombreCliente, " & _ "PA.CodigoBarras AS CodigoBarrasCliente, " & _ "PC.FechaVencimiento, IC.CopiaNumero, " & _ "IC.CodigoBarras AS CodigoBarrasArticulo, " & _ "NI.Title, CMT.NombreCompleto AS NombreMedio " & _ "FROM Cliente AS PA " & _ "INNER JOIN CopiaCliente AS PC ON PA.ID = PC.Cliente " & _ "INNER JOIN CopiaArticulo AS IC ON PC.CopiaArticulo = IC.ID " & _ "INNER JOIN ArticuloConNombre AS NI ON IC.IDArticulo = NI.ID " & _ "INNER JOIN CodigoTipoMedio AS CMT ON " & _ "NI.TipoMedio = CMT.ID " & _ "WHERE PC.Regresado = 0 " & _ "AND PC.Faltante = 0 " & _ "AND IC.Faltante = 0 " & _ "ORDER BY NI.Titulo, IC.CopiaNumero, " & _ "PA.Apellido, PA.Nombre" datosInforme = CrearTablaDatos(textoSql) ' ----- Revisar que no hay datos. If (datosInforme.Rows.Count = 0) Then datosInforme.Dispose( ) MsgBox("No hay articulos prestados.", MsgBoxStyle.OkOnly _ Or MsgBoxStyle.Exclamation, TituloPrograma) Return End If ' ----- Enviar los datos al informe. formularioInforme = New InformeVisorIntegrado formularioInforme.IniciarInforme("Biblioteca.InformePrestado.rdlc", _ "Biblioteca_EsquemaInformeArticulosCliente", datosInforme) Return ErrorHandler: ErrorGeneral("SeleccionarInforme.InformeBasicoPrestado", _ Err.GetException( )) Return End Sub El código recupera los registros específicos del informe de la base de datos y se asegura de que se incluyó por lo menos un registro. (Pudimos agregar la instrucción SQL a la base de datos Biblioteca como procedimiento almacenado o una vista y llamarlo, en cambio. Para los fines de este tutorial, era más simple almacenar la instrucción directamente en código.) Luego llama al visor del informe, pasando el nombre del archivo RDLC, el nombre del esquema (en el formato NombreProyecto_NombreClase) y la tabla de datos. A continuación, agregue los métodos informeBasicoVencido e InformeBasicoFaltante. No mostraré el código aquí porque, excepto por el nombre del archivo RDLC y la cláusula WHERE en la instrucción SQL, son idénticas a InformeBasicoPrestado. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 9. 608 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 608 17/2/10 15:32:29 Agregue el método InformeBasicoMultas, que maneja el informe integrado #4. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 10. También es muy similar al método InformeBasicoPrestado, pero usa la instrucción SQL que diseñamos antes para la recuperación de multas del cliente. También usa un esquema diferente y un nombre de informe. formularioInforme.IniciarInforme("Biblioteca.InformeMultasCliente.rdlc", _ "Biblioteca_EsquemaInformeMultasCliente", datosInforme) El último método que se agrega a SeleccionarInforme.vb es InformeBasicoEstadisticas, que maneja el informe #5 integrado. Es un poco diferente de los otros cuatro porque reúne datos de seis tablas diferentes, de uno en uno. En cada caso, recupera una cuenta del número de registros en una tabla de base de datos. Los resultados son luego almacenados en una colección genérica (System.Collections.Generic.List), donde cada entrada de la lista es una instancia de InformeBasicoEstadisticas, la clase que usamos para el esquema de datos del quinto informe. ¡Qué coincidencia! He aquí el código de InformeBasicoEstadisticas para que lo agregue ahora a la clase de formulario SeleccionarInforme. Inserción de fragmento de código Inserte el fragmento de código Cap21, Elemento 11. Private Sub InformeBasicoEstadisticas( ) ' ----- Ejecutar el informe #5integrado: informe de estadisticas ' de la base de datos Biblioteca. Dim textoSql As String Dim datosInforme As Collections.Generic.List( _ Of EsquemaInformeEstadistica) Dim unaEntrada As EsquemaInformeEstadistica Dim formularioInforme As InformeVisorIntegrado Dim valorResultado As Integer Dim contador As Integer Const conjuntosTablas As String = "Autor,Editor," & _ "Tema,ArticuloConNombre,CopiaArticulo,Cliente" Const titulosTabla As String = "Autores,Editores," & _ "Encabezados de tema,Articulos,Copias de articulos,Clientes" On Error GoTo ErrorHandler ' ----- Generar los datos del informe. Todo cuenta a partir de las ' diferentes tablas. datosInforme = New Collections.Generic.List( _ Of EsquemaInformeEstadistica) For contador = 1 To ContarSubCadena(conjuntosTablas, ",") + 1 Proyecto | 609 21_PATRICK-CHAPTER_21.indd 609 17/2/10 15:32:29 ' ----- Procesar una tabla. textoSql = "SELECT COUNT(*) FROM " & _ ObtenerSubCadena(conjuntosTablas, ",", contador) valorResultado = ObtenerEnteroBD (EjecutarSQLRegresar(textoSql)) ' ----- Agregarlo a los datos del informe. unaEntrada = New EsquemaInformeEstadistica unaEntrada.NombreEntrada = _ ObtenerSubCadena (titulosTabla, ",", contador) unaEntrada.ValorEntrada = CStr(valorResultado) datosInforme.Add(unaEntrada) Next contador ' ----- Enviar los datos al informe. formularioInforme = New InformeVisorIntegrado formularioInforme.IniciarInforme("Biblioteca.InformeEstadisticas.rdlc", _ "Biblioteca_EsquemaInformeEstadistica", datosInforme) Return ErrorHandler: ErrorGeneral("SeleccionarInforme.InformeBasicoEstadistica", _ Err.GetException( )) Return End Sub Debido a que realmente necesitamos obtener la misma información (COUNT(*)) para cada una de las seis tablas afectadas, acabo de implementar el código como un bucle, y genere la instrucción SQL para cada una a medida que recorremos el bucle. Un nombre de tabla amigable y la cuenta de registros son almacenados entonces en la lista genérica, que se termina enviando al informe. Ahora puede ejecutar la aplicación y usar los cinco informes integrados. Debe iniciar sesión como bibliotecario o administrador, y luego acceder al panel Imprimir informes en el formulario principal. Aunque usted no lo crea, hemos terminado casi con la aplicación. Lo único importante que queda por hacer es procesar los artículos vencidos de cliente para ver si se necesitan las multas. Agregaremos este código en el siguiente capítulo, y también echaremos un vistazo a la asignación de licencias. 610 | Capítulo 21: Creación de informes 21_PATRICK-CHAPTER_21.indd 610 17/2/10 15:32:30 Capítulo 22 Otorgue licencia a su aplicación El licenciamiento de contenido .NET apropiado puede hacer la diferencia entre el dominio de mercado y la bancarrota financiera. Y sólo estoy hablando acerca de entender los acuerdos de licencia incluidos con Visual Studio. Todavía tiene que descubrir un método apropiado para el licenciamiento de su propia aplicación antes de mandarla a sus clientes. El licenciamiento y los acuerdos de licencia son medios esenciales para proteger la propiedad intelectual que tanto trabajo le ha costado desarrollar. ¿Cómo funciona el licenciamiento? La clave se encuentra en la raíz de la palabra: licencia viene de “li-” (decir una mentira) y “-cence” (de “centavos”). Juntas, significan “decir mentiras acerca de pequeñas unidades monetarias”. La confusión creada al tratar de descubrir lo que esto significa mantiene a los malos perplejos y lo suficientemente ocupados como para que no roben su aplicación. Si este método no funciona, existen algunas soluciones, algunas de las cuales revisaré en este capítulo. Parte del análisis se enfoca en diseñar un sistema de licenciamiento que aparecerá en el proyecto Biblioteca. .NET Framework incluye clases de licenciamiento de componentes pero se usan, sobre todo, para diseñadores de controles usados por otros programadores dentro de Visual Studio IDE, y no para aplicaciones de usuario. No cubriremos estas características de licenciamiento en este capítulo. Si tiene curiosidad acerca de esas características, empiece por leer acerca de License Compiler (lc.exe) en la ayuda en línea de Visual Studio. Opciones de licenciamiento de software En los primeros años del software, el licenciamiento no era un problema: si podías llegar a la computadora, era porque estabas autorizado. Toda la interacción de usuario con el sistema era a través de los programadores y técnicos. Si algún usuario quería robar algo, era en forma de 20 toneladas de acero, cables y bulbos. ¿Divertido? Sí. ¿Fácil? No. 611 22_PATRICK-CHAPTER_22.indd 611 18/2/10 11:20:02 Hoy en día, es una historia diferente. La mayoría de los usuarios no son técnicos, y algunos no son éticos. Entonces, ahora tenemos acuerdos de licencia y equipos de abogados para respaldarlo. Pero también tenemos software, que puede imponer delicadamente algunas de las reglas. Para una pieza de software particular, existe todavía la pregunta: “¿Cuánto código de implementación de licenciamiento agrego a mi aplicación?” La cantidad de control de software que incluya caerá en alguna parte del continuo “Libertad-Seguridad” que se muestra en la figura 22-1. ¿Probablemente aquí? Libertad Seguridad Figura 22-1. El continuo de implementación de licenciamiento: ¿Dónde está usted? Si va a la parte de Libertad del espectro (“conveniente para los usuarios y hackers”), tendrá que ir por la confiabilidad de los usuarios, y cualquier guardia armado que mande a sus oficinas, para que se siga cumpliendo con el programa. En la parte de Seguridad de la escala (“seguro para los programadores y despachos de abogados con honorarios altos”), el software implementa prácticas y políticas que aseguran que sólo los usuarios con licencia de la aplicación lo usen o lo instalen; no se necesitan guardias armados. En el resto de esta sección se analizan las posibles opiniones que puede seleccionar dentro del rango Libertad-Seguridad. Sólo acuerdo de licencia El método de sólo acuerdo de licencia opta claramente por libertad en vez de la seguridad. Cuando le proporciona al usuario el software, viene con un acuerdo de licencia redactado con todo cuidado y que describe los términos de uso para el usuario y el proveedor de software. Por lo general, le da al usuario ciertos derechos como la instalación, el uso y la distribución del software. Cuando escribe una aplicación para uso sólo dentro de una organización específica o por parte de un grupo pequeño de usuarios con los que tendrá contacto regular, el método de sólo acuerdo de licencia puede ser lo que necesite. En realidad, apostaría que la mayor parte de las aplicaciones de Visual Basic se orientan en este sentido. Microsoft ha anunciado a través de los años que la mayoría de los programadores de Visual Basic orientan sus aplicaciones para uso en una organización de negocios específica, unida a una base de datos personalizada específica. Esos sistemas a menudo requieren muy poco en la forma de imposición de la licencia, porque la aplicación no sirve cuando se lleva fuera del edificio en que debe residir. Aunque su software logre una distribución amplia, este esquema de licenciamiento puede todavía ser el camino a seguir. Muchas aplicaciones de fuente abierta, incluido un sistema operativo importante que rima con “Plinux”, usa la licencia GNU General Public License de Free Software Foundation (http://www.fsf.org/licensing/licenses/gpl.html) como su licenciamiento principal y su política de distribución. 612 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 612 18/2/10 11:20:03 Clave de licencia generada general Si necesita un poco más de control sobre la distribución, instalación y uso de una aplicación, puede imponer una clave de licencia generada general (en esencia, una contraseña que permite que la aplicación se instale o use). Estas claves a menudo se insertan al inicio del proceso de instalación, donde se pide al usuario una clave específica. Sin la clave, se le dice adiós a la instalación. El vendedor de software necesitará una forma de generar un buen conjunto de claves de instalación únicas. Existen un par de opciones: • Sólo generar un número de serie secuencial y combinarlo con un ID de producto y un número de versión. Lo grandioso acerca de esa clave es que resulta fácil generarlo. El programa de instalación no necesita realizar ninguna lógica de verificación compleja en la clave. Sólo necesita asegurarse de que el formato general es correcto. Uno de los productos que utilicé para desarrollar documentación de ayuda en línea para mis antiguas aplicaciones de Visual Basic 6.0 utilizaba esa clave de licencia. En una forma, no es más seguro que usar sólo un acuerdo de licencia, porque cualquiera que conoce el formato general puede inventar su propia clave. • Usar una clave con hash o revuelta, basada en algún número de serie original o una fórmula que pueda verificar el programa de instalación. Un algoritmo de hash bien hecho puede generar un amplio rango de claves, pero dificulta que otros que no saben la fórmula generen sus propias claves falsas. Aunque no conozco los secretos de los procesos internos de Microsoft, éste parece el método que usa para sus “Claves de CD” de 25 caracteres, incluido el que se proporciona con Visual Studio. Aunque es difícil que se inventen claves sin bases, la naturaleza pública hace que estén sujetas a ser compartidas. Para algunos de sus productos, Microsoft combina una clave de CD con un registro en línea o telefónico para mejorar la seguridad. • Proporcionar una clave de hash o cifrada basada en un número de serie que (secretamente) se proporciona con el programa de instalación o medio de distribución. Cuando el usuario inserta la clave, se descifra o se prepara de otra forma, y después se compara con el número de serie. Sólo si coincide, la instalación del software se completará apropiadamente. Clave de licencia generada personalizada Una clave de licencia generada personalizada es similar a una clave general generada, pero usa información personal proporcionada por el usuario como parte del proceso de generación. Esa clave es más interactiva, y requiere que el usuario se comunique específicamente con el vendedor de software (o una aplicación en su sitio Web) para completar el proceso de instalación. Durante la compra o el proceso de instalación, el usuario hace disponible información específica (como el nombre del dueño y la fecha de compra) al vendedor de software. El vendedor entonces usa cifrado de clave pública-pública (criptografía asimétrica) para cifrar por completo o firmar digitalmente la información relevante. La firma cifrada entonces se regresa al usuario para la instalación. El proceso de instalación usa la porción pública del par de clave para asegurar que la firma es válida. Opciones de licenciamiento de software | 613 22_PATRICK-CHAPTER_22.indd 613 18/2/10 11:20:03 Usaremos este método de clave de licencia en el Proyecto Biblioteca, de modo que tendré más qué decir en el futuro. Clave de licencia con identidad o candado de hardware Para los vendedores de software paranoicos, o para quienes tienen una necesidad legítima de mantener las riendas en su base de instalación, existen soluciones que incluyen acceso regular a hardware o servicios para confirmar que el software previamente instalado es legal y válido. Un método popular usa un “dongle”, que suele ser un dispositivo basado en puerto USB al que el software debe acceder cada vez que se ejecuta. El vendedor de software proporciona un dongle con el software que tiene licencia, y puede codificarlo con límites basados en fecha o basados en uso. Con el predominio de Internet, los vendedores de software también tienen la opción de verificación en tiempo real mediante Web. Cada vez que un programa se ejecuta, puede acceder a un sitio de vendedor conocido para activar un proceso de verificación de uso. Este sistema permite el monitoreo continuo del software por parte de vendedores que pueden tener un negocio o razón gubernamental para limitar el uso de software. Para el proyecto de uno de mis clientes debo acceder a un sitio Web de terceros cada mes y descargar datos de propietario para usarlo con el software de ese vendedor. Éste requiere que siempre acceda a su sitio Web desde una máquina específica con una dirección IP específica. Se rehusará a proporcionar los datos si intento conectarme desde cualquier otra computadora. Si tengo una necesidad real de usar una dirección IP nueva (si, por ejemplo, cambio de proveedor de servicio de Internet), debo mandar un escrito al vendedor informándole de mi nueva dirección IP. Parece molesto, y es una irritación. Pero los datos que proporcionan son únicos y valiosos, y sienten que tienen una necesidad de negocios para proteger esa inversión. Como mi cliente requiere los datos, no tengo otra opción más que aceptar los procedimientos de verificación mensuales. Acceso controlado El nivel más alto de seguridad requiere una falta de confianza evidente del usuario, aunque puede haber muy buenas razones para esto. En el caso de aplicaciones demasiado confidenciales, el vendedor de software puede poner su producto a disposición de un número limitado de clientes, y después sólo en una base de arrendamiento. Como parte de este acuerdo de arrendamiento, el cliente accede a tener un miembro de personal entrenado del vendedor de software en el sitio, ejecutando y manteniendo la aplicación para el cliente. Como mínimo, el vendedor requerirá que uno de sus empleados esté disponible de manera inmediata al cliente cuando la aplicación se usa. En un mundo de aplicaciones de software comerciales, parece inconcebible que ese sistema pueda existir. Pero en situaciones de alto riesgo, las preocupaciones de seguridad se elevan a tal 614 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 614 18/2/10 11:20:03 nivel que ninguna parte está dispuesta a asumir completamente el riesgo de instalar y usar la aplicación, sin el otro. Aunque estuve tentado a usar este sistema para el proyecto Biblioteca, creo que nos quedaremos con nuestro plan original de emplear una clave de licencia personalizada generada. Acuerdos de licencia Un acuerdo de licencia es un documento en que la parte denominada primera parte a partir de aquí presta amigablemente a la parte denominada segunda parte ciertos derechos, por una remuneración, bonos del tesoro y otros beneficios; a cambio, la denominada segunda parte hará lo mismo para la denominada primera parte sin respeto por ninguna otra parte, partida o entera. Intentemos eso de nuevo. Un acuerdo de licencia le dice a un usuario “Continúa, instala y usa el software, pero tienes que seguir estas reglas”. Aunque a menudo están escritas en condiciones legales, también pueden aparecer en un lenguaje real, como el español. También tienen un rango en derechos otorgados, que va desde “Puede usar esto, pero cuando termine, debe destruir todas las copias” hasta “Úselo, y siéntase libre de pasar una copia del programa y su código fuente a sus amigos y conocidos”. El software Biblioteca proporcionado con este libro incluye un acuerdo de licencia. (Lo incluí en el apéndice B.) Cuando instaló el código de ejemplo, accedió a los términos del acuerdo de licencia, incluida la parte acerca de mantener bien a mi familia, financieramente en mis años de retiro. Pero ya es suficiente sobre mí; hablemos acerca de los acuerdos de licencia que tal vez quiera usar para sus aplicaciones. Si está desarrollando un programa de catálogo de DVD para su primo Fernando, tal vez se salte la parte del acuerdo de licencia. Pero cualquier software que haga en las instalaciones de un trabajo para uso fuera de su propia compañía, debe incluir algún tipo de acuerdo entre usted (o su compañía) y el usuario del software. Este acuerdo debe definirse como parte del contrato que estableció el proyecto de desarrollo de software (esto es típico para consulta de software), o puede incluir el acuerdo como un componente del software (común para programas comerciales). Cualquiera que sea el método que seleccione, es importante que lo ponga por escrito, porque puede ahorrarle dolores en el camino. Una vez tuve un cliente que insistió que yo le pasara una copia del código fuente para una aplicación que le escribí, para que pudieran mejorarlo y vender la nueva versión a otro negocio (¡que descaro!). Por fortuna, teníamos un contrato escrito que decía las reglas de compromiso. Tenían derecho a una copia del código fuente para propósitos de archivado, pero no podían usarlo o derivar productos de éste sin consentimiento mío por escrito. Esto les dio un nivel de seguridad mientras todavía proporcionaban los medios para que yo le diera el mejor soporte posible para su organización. Por fortuna, todo llegó a una conclusión feliz, y como ese código de Visual Basic 3.0 ya no corre, es un punto debatible. Acuerdos de licencia | 615 22_PATRICK-CHAPTER_22.indd 615 18/2/10 11:20:03 Un acuerdo de licencia suele existir para proteger los derechos del vendedor de software, pero sería inútil si tampoco tuviera derechos significativos para el usuario (y algunos de los derechos pueden ser generosos). ¿Sabía que el estándar del acuerdo de licencia de consumidor para Microsoft Office le permite instalar el producto en dos diferentes sistemas usando una sola copia de licencia del programa? No es un completo festival de instalación. Las computadoras deben pertenecer a la misma persona, y una debe ser de escritorio mientras la otra tiene que ser un dispositivo portátil (una computadora portátil). Pero es todavía un beneficio significativo para el usuario típico. El departamento legal en O’Reilly Media quiere recordarle que Tim Patrick no tiene una comprensión suficiente de la ley, y no puede darle consejos sobre el contenido de ningún acuerdo de licencia que quiera crear para sus proyectos. Ofuscación Aludí un poco a las características de ofuscación en Visual Studio 2008 en los capítulos 1 y 5, pero es buen momento para que echemos un vistazo a las características. Visual Studio incluye una versión reducida de Dotfuscator, de una compañía llamada PreEmptive Solutions (no es parte de Microsoft, todavía). Para acceder al programa, use el comando de menú Tools → Dot fuscator Community Edition en Visual Studio. La interfaz principal aparece en la figura 22-2. Figura 22-2. ¡Es tiempo de ofuscar! Al momento de escribir este libro, Dotfuscator Community Edition no se incluía con Visual Basic 2008 Express Edition. 616 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 616 18/2/10 11:20:04 Aunque ésta es la versión básica del producto, puede ver que tiene una cantidad enorme de opciones. Si quiere profundizar en estas características mejoradas para su proyecto, es grandioso. Cubriré el uso básico aquí. Recordemos rápidamente por qué querría ofuscar su código, o incluso usar la palabra ofuscar en mala compañía. Aquí se muestra parte del código del Proyecto Biblioteca: Public Function CentrarTexto(ByVal textoOriginal As String, _ ByVal anchoTexto As Integer) As String ' ----- Centrar una pieza de texto en un ancho de campo. Si el ' texto es demasiado ancho, truncarlo. Dim textoResultado As String textoResultado = Trim(textoOriginal) If (Len(textoResultado) >= anchoTexto) Then ' ----- Truncar lo necesario. Return Trim(Left(textoOriginal, anchoTexto)) Else ' ----- Empezar con espacios adicionales. Return Space((anchoTexto - Len(textoOriginal)) \ 2) & _ textoResultado End If End Function Este código es muy sencillo de entender, sobre todo con los comentarios y el método significativo y los nombres de variables. Aunque la ofuscación de .NET funciona en el nivel MSIL, pretendamos que el ofuscador funcionó directamente en el código de Visual Basic. La ofuscación de este código puede producir resultados similares al siguiente: Public Function A(ByVal AA As String, _ ByVal AAA As Integer) As String Dim AAAA As String AAAA = Trim(AA) If (Len(AAAA) >= AAA) Then Return Trim(Left(AA, AAA)) Else Return Space((AAA - Len(AA)) \ 2) & AAAA End If End Function En una rutina tan simple, aún podemos descubrir la lógica, pero con más esfuerzo que en la versión original. Naturalmente, la ofuscación real va mucho más allá de esto, revolviendo la legibilidad del código en el nivel IL, y confundiendo a los lectores de código y hackers. Para ofuscar un ensamblado: 1. Genere su proyecto en Visual Studio usando el comando de menú Generar → Generar [nombre de proyecto]. 2. Inicie Dotfuscator al usar el comando de menú Tools → Dotfuscator Community Edition, en Visual Studio. Ofuscación | 617 22_PATRICK-CHAPTER_22.indd 617 18/2/10 11:20:04 3. Cuando se le pida un tipo de proyecto, seleccione Crear nuevo proyecto, y haga clic en el botón Aceptar. 4. En la ficha Entrada de la ventana de aplicación Dotfuscator, haga clic en el botón de la barra de herramientas “Examinar y agregar ensamblado a la lista”. Es el botón del extremo izquierdo (que parece un archivo de carpeta con una pequeña flecha encima) en el panel que se muestra en la figura 22-2. 5. Cuando se le pida un nombre de ensamblado, explore su aplicación compilada, y haga clic en el botón Aceptar. El ensamblado que habrá de usarse estará en el subdirectorio bin\Release del directorio de código fuente del proyecto. 6. Seleccione el comando de menú Archivo → Generar para generar el ensamblado ofuscado. Se le pedirá que guarde el archivo de proyecto Dotfuscator (un archivo XML) antes de que la generación comience. Guarde este archivo en un nuevo directorio. Cuando ocurra la generación, guardará la salida del ensamblado en un subdirectorio Dotfuscated en el mismo directorio que contiene el archivo de proyecto XML. 7. La generación se completa, y aparece un resumen, como se muestra en la figura 22-3. Su archivo ofuscado ya está en uso. El proceso también genera un archivo Map.xml, que documenta todos los cambios de nombre hechos a los tipos y miembros dentro de su aplicación. Sería malo distribuir este archivo con el ensamblado. Es sólo para usarlo en la depuración. Para probar que la ofuscación tuvo lugar, use la herramienta IL Disassembler que se incluye con Visual Studio para examinar cada ensamblado. (En mi sistema, este programa se accede por medio de Inicio → [Todos los] Programas → Microsoft Windows SDK v6.0A → Tools → IL Disassembler.) En la figura 22-4 se muestran las variables globales incluidas en el archivo General.vb del proyecto Biblioteca. La versión ofuscada de estas mismas variables aparece en la figura 22-5. No realizaré ofuscación en el proyecto Biblioteca en estas secciones de tutorial del libro. Siéntase en la libertad de probarlo por sí mismo. El sistema de licenciamiento de biblioteca Las herramientas y los procedimientos que usaremos para diseñar el sistema de licenciamiento del proyecto Biblioteca pueden generarse a partir de características que ya se analizaron en los capítulos anteriores: • El archivo de licencia contiene XML. (Capítulo 13.) • La licencia aparece como un archivo separado en el mismo directorio que el ensamblado Biblioteca.exe. El software Biblioteca lee contenido del archivo de licencia. (Capítulo 15.) • La licencia incluirá una firma digital, que está basada en cifrado de clave pública-privada. (Capítulo 11.) 618 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 618 18/2/10 11:20:04 Figura 22-3. Resumen de la ofuscación, con algo de publicidad. Figura 22-4. Variables globales antes de la ofuscación. 22_PATRICK-CHAPTER_22.indd 619 El sistema de licenciamiento de biblioteca | 619 18/2/10 11:20:05 Figura 22-5. Variables globales después de la ofuscación. Cada vez que se ejecuta la aplicación Biblioteca, intenta leer el archivo de licencia. Si no existe, o si contiene datos o una firma no válidos, el programa degrada las características disponibles, deshabilitando las características para las que se considera que no tienen licencia. Diseño del archivo de licencia El archivo de licencia del Proyecto de biblioteca contiene información básica de propiedad y derechos relacionados al usuario que compró derechos del software. Aquí se muestra el contenido XML que se me ocurrió: Eso parece suficiente. El proceso que genera la firma digital también almacena una firma cifrada dentro del contenido XML. Generación del archivo de licencia En el “Proyecto” de este capítulo, generaremos una nueva aplicación que sólo existe para generar archivos de licencia para la aplicación Biblioteca. Tendrá tres componentes principales: 1. Generar y administrar las claves pública y privada utilizadas en su proceso de firma. 620 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 620 18/2/10 11:20:05 2. Pedir al usuario la fecha de licencia, fecha de expiración, versión cubierta, nombre del propietario de la licencia y número de serie para una sola licencia. Éstos son los valores que aparecen en el contenido XML del archivo de licencia. 3. Dar salida al archivo XML de licencia y firmarlo digitalmente mediante la clave privada. Instalación del archivo de licencia En la sección “Proyecto” de este capítulo se mostrará cómo generar un archivo genérico de licencia. Este archivo XML se distribuirá e instalará con la aplicación Biblioteca al usar un programa de instalación que generaremos en el capítulo 25. El archivo se denominará LicenciaBiblioteca. lic (como opción predeterminada) y siempre aparecerá en el mismo directorio que el archivo de aplicación Biblioteca.exe. Si estuviera desarrollando una aplicación real para pagar a los clientes, y tuviera un sitio Web que soportara un servicio Web (del que hablaré en el capítulo 23), aquí se muestra un diseño para instalar el archivo de licencia que podría usar: 1. Ejecute el programa de instalación para instalar la aplicación en la estación de trabajo del usuario. 2. Durante la instalación, el programa de instalación pide al usuario los detalles de licencia que aparecerán en el archivo de licencia XML. 3. El programa de instalación se pone en contacto con el servicio Web en mi sitio de vendedor y pasa los valores proporcionados por el usuario al servicio de registro. 4. El servicio de registro regresa un archivo XML firmado digitalmente que contiene el contenido de licencia. 5. El programa de instalación instala este archivo junto con la aplicación. 6. Si por cualquier razón el licenciamiento no puede completarse con éxito durante la instalación, la aplicación principal contiene código de licenciamiento idéntico, y puede comunicarse con el propio servicio de registro. Uso del archivo de licencia Cada vez que la aplicación Biblioteca se ejecute, ésta lee el archivo de licencia XML y realiza muchas revisiones para asegurarse de que la licencia sea válida para la aplicación de instalación actual. Si la licencia no es válida por cualquier razón, la aplicación bloquea el acceso a las características administrativas mejoradas incluidas en el sistema Biblioteca. Resumen Ya que a menudo pasará decenas o centenas de horas diseñando y desarrollando una aplicación de Visual Basic con calidad, es importante usar el licenciamiento apropiado y la tecnología de 22_PATRICK-CHAPTER_22.indd 621 Resumen | 621 18/2/10 11:20:05 ofuscamiento para proteger su trabajo duro. Otorgar licencias es otra de esas tareas de programación comunes que no se agregaron a .NET Framework como una clase fácil de usar (a menos que esté generando y distribuyendo controles en tiempo de diseño). Para el resto de nosotros, es tiempo de hacerlo mientras continúa. Por fortuna, .NET tiene herramientas de soporte grandiosas, así que agregar soporte de licenciamiento no es muy difícil. Proyecto En el código de proyecto de este capítulo seguiremos dos de los cuatro pasos de licenciamiento analizados en la sección “El sistema de licenciamiento de biblioteca”, en páginas anteriores de este capítulo: generar y usar el archivo de licencia. El diseño que creamos antes es suficientemente bueno para nuestras necesidades, aunque todavía necesitamos registrarlo en la documentación técnica del proyecto. No instalaremos formalmente el archivo de licencia hasta que creemos el programa de configuración, en el capítulo 25. Actualización de documentación técnica Ya que estaremos agregando un nuevo archivo externo que será procesado por el proyecto Biblioteca, necesitamos documentar su estructura en el kit de recursos técnicos del proyecto. Agreguemos la siguiente nueva sección a ese documento. Archivo de licencia El proyecto Biblioteca lee un archivo de licencia específico de cliente generado por la aplicación de soporte Library License Generation. Este programa genera un archivo de licencia XML firmado digitalmente que incluye información de licencia. Aquí se muestra un ejemplo de contenido de archivo de licencia: Las etiquetas 22_PATRICK-CHAPTER_22.indd 622 18/2/10 11:20:05 Cada componente puede incluir un número de 0 a 9999, o el caracter *, que indica todos los valores válidos para esa posición. La sección La ruta del archivo Library License en esta estación de trabajo. Si no se proporciona, el programa buscará un archivo denominado LibraryLicense.lic en la misma carpeta que la aplicación. Aplicación auxiliar de Library License Generar archivos de licencia y firmas digitales a mano con el Bloc de notas sería..., bueno, ni siquiera pensemos en eso. En cambio, dependeremos de una aplicación personalizada para crear los archivos y firmas. Ya he desarrollado esa herramienta personalizada. La encontrará en el directorio de instalación del código de este libro, en el subdirectorio LicenciaBiblioteca. Esta aplicación de soporte incluye dos formularios principales. La primera (FormularioUbicacionClave.vb, mostrada en la figura 22-6) ubica o crea los archivos de clave pública-privada usados en el proceso de firma digital. Figura 22-6. Forma de soporte para firmas digitales. Proyecto | 623 22_PATRICK-CHAPTER_22.indd 623 18/2/10 11:20:06 La mayor parte de la forma del código ayuda a ubicar y verificar la carpeta que contendrá los dos archivos de clave (uno privado, uno público). Algo del código en el manejador de eventos AccGenerar_Click crea los archivos reales. Dim claveDosPartes As RSA Dim archivoPublico As String Dim ArchivoPrinvado As String ...algun codigo omitido aqui, entonces... ' ----- Generar las claves. claveDosPartes = New RSACryptoServiceProvider claveDosPartes = RSA.Create( ) ' ----- Guardar la clave publica. My.Computer.FileSystem.WriteAllText(archivoPublico, _ claveDosPartes.ToXmlString(False), False) ' ----- Guardar la clave privada. My.Computer.FileSystem.WriteAllText(ArchivoPrivado, _ claveDosPartes.ToXmlString(True), False) ¡Eso es realmente simple! La clase System.Security.Cryptography.RSA y la clase relacionada RSACryptoServiceProvider hacen todo el trabajo. Lo único que tiene que hacer es llamar al método RSA.Create, y después generar las claves XML relevantes al usar el método ToXml String, pasando un argumento False para la clave pública y True para la clave privada. Si quiere ver algunas de las claves de ejemplo, abra el subdirectorio LicenseFiles en el directorio origen de instalación del libro. Encontrará dos archivos, uno es la clave pública y otro es la clave privada. Imprimiría uno de éstos aquí, pero todo parece caracteres aleatorios. La otra forma de soporte es FormularioPrincipal.vb, que genera el archivo de licencia de usuario real, y aparece en la figura 22-7. Figura 22-7. Formulario de soporte para la generación de archivo de licencia. 624 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 624 18/2/10 11:20:06 Al igual que el primer formulario, casi todo este código fuente simplemente asegura que los archivos de clave privada y pública estén intactos, y que el usuario insertó datos válidos antes de la generación. El manejador de eventos AccGenerar_Click es donde está la verdadera diversión. Primero, necesitamos algo de contenido XML, que generamos en el método GenerarContenidoLicenciaXml. Crea el contenido elemento por elemento, al usar métodos que aprendimos en el capítulo 13. Por ejemplo, aquí está la parte de código que agrega el número de serie. elementoDatos = result.CreateElement("NumeroSerie") elementoDatos.InnerText = Trim(NumeroSerie.Text) elementoRaiz.AppendChild(elementoDatos) Después viene la firma digital, por medio de la función FirmaContenidoLicenciaXml, que principalmente aparece aquí: Private Function FirmaContenidoLicenciaXml( _ ByVal origenXml As XmlDocument) As Boolean ' ----- Agregar una firma digital para un documento XML. Dim archivoClavePrivada As String Dim clavePrivada As RSA Dim firma As SignedXml Dim metodoReferencia As Reference ' ----- Cargar la clave privada. archivoClavePrivada = My.Computer.FileSystem.CombinePath( _ UbicacionClave.Text, NombreArchivoClavePrivada) clavePrivada = RSA.Create( ) clavePrivada.FromXmlString( _ My.Computer.FileSystem.ReadAllText(archivoClavePrivada)) ' ----- Crear el objeto que genera la firma. firma = New SignedXml(origenXml) firma.SignedInfo.CanonicalizationMethod = _ SignedXml.XmlDsigCanonicalizationUrl firma.SigningKey = clavePrivada ' ----- La firma aparecera como un elemento ' La firma digital ocurre por medio de la clase SignedXml (en el espacio de nombre System. Security.Cryptography.Xml). Esta clase usa unos cuantos métodos de firma diferentes. El que seleccioné (XmlDsigCanonicalizationUrl) se usa para XML típico e ignora los comentarios incrustados. Proyecto | 625 22_PATRICK-CHAPTER_22.indd 625 18/2/10 11:20:06 Esta firma aparece como etiquetas y valores en la salida XML, agregada a través de la instrucción AppendChild al final de la rutina. Ya que no queremos que la firma en sí se considere cuando volvamos a revisar el archivo XML en busca de contenido válido, la clase SignedXml agrega la firma como una etiqueta Aquí se muestra un ejemplo de archivo de licencia XML que incluye una firma digital. Ésta es la que he incluido en el directorio LicenseFiles en la instalación de origen del libro (con algunas líneas ajustadas para que quepan en esta página). La firma digital aparece como contenido desordenado dentro de la etiqueta 22_PATRICK-CHAPTER_22.indd 626 18/2/10 11:20:07 En vez de usar una firma digital, pudimos sólo haber cifrado el archivo de licencia completo con la clave privada, y después utilizar la clave pública para descifrarlo y examinar su contenido. Pero me gusta más la firma digital, porque permite a cualquiera abrir el archivo de licencia y revisar los parámetros de licencia por sí mismo mientras previene todavía cualquier cambio. Adición de la licencia al programa Biblioteca Regresemos a la aplicación Biblioteca ya en progreso. Acceso al proyecto Cargue el proyecto Cap22 (Antes) código mediante las plantillas de Nuevo proyecto o accediendo directamente al proyecto desde el directorio de instalación. Para ver el código en su forma final, cargue, en cambio, Cap22 (Final) código. El programa ajustará su comportamiento dependiendo de si está licenciado o no. Pero para hacer tal determinación, necesita asegurarse de que el contenido del archivo de licenciamiento es válido y no ha sido saboteado. Para hacer esto, necesita una forma de volver a ordenar la firma y compararla con el resto de la licencia para asegurarse de que coincide. Generamos la firma al usar la clave privada; debemos volverla a ordenar mediante la clave pública. Podemos almacenar la clave pública en su propio archivo fuera del programa, pero entonces tal vez se pierda (al igual que mis claves reales). En vez de eso, almacenaremos la clave pública como un recurso de aplicación, encontrada de manera externa en el código fuente de la carpeta Recursos. Ya he agregado el recurso a su copia del programa, y la he denominado ClavePublicaLicencia. Con esta clave incrustada en la aplicación, cualquier regeneración de las claves pública o privada requerirá modificación de su recurso. En el código, nos referimos al contenido XML de la clave pública al usar su nombre de recurso: My.Resources.ClavePublicaLicencia Algunas de las características de seguridad usan clases encontradas en el espacio de nombre System.Security.Cryptography.Xml. Esto no es uno de los nombres de espacio incluidos como opción predeterminada en las nuevas aplicaciones de Visual Basic, así que tenemos que agregarla nosotros mismos. Abra la ventana de propiedades de proyecto y seleccione la pestaña Referencias. Justo debajo de la lista Referencias, haga clic en el botón Agregar, y después seleccione System.Security de la pestaña .NET de la ventana Agregar referencia que aparece. Mientras tenemos todavía la ventana de propiedades del proyecto abierta, haga clic sobre la pestaña Configuración. Agregue una nueva configuración String y use “UbicacionArchivoLicencia” como nombre. Usaremos esta configuración para agregar la ruta al archivo de licencia. Guarde y cierre la ventana de propiedades del proyecto. Nuestras necesidades generales de licencia a través de la aplicación son muy simples. Sólo necesitamos saber el estado actual del archivo de licencia, y tener acceso a algunos de los valores para que podamos desplegar un mensaje corto acerca de la licencia. Tal vez necesitemos hacer esto en varias partes del programa, así que agreguemos algo de código genérico al módulo General.vb. Abra ese módulo ahora. Proyecto | 627 22_PATRICK-CHAPTER_22.indd 627 18/2/10 11:20:07 Justo en la parte superior de ese archivo, el código ya incluye referencia al nombre de espacio System.Security.Cryptography, desde que incluimos esa contraseña de cifrado de usuario. Pero esto no cubre las cosas XML estándar o seguras. Así que también agregamos dos nuevas instrucciones Import. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 1. Imports System.Xml Imports System.Security.Cryptography.Xml Usaremos una enumeración para indicar el estado de la licencia. Agréguelo ahora al módulo General. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 2. Public Enum EstatusLicencia LicenciaValida ArchivoLicenciaFaltante ArchivoLicenciaCorrupto FirmaNoValida AunNoLicenciada LicenciaExpirada VersionNoCoincidente End Enum También agreguemos una estructura simple que comunique los valores extraídos del archivo de licencia. Agregue este código al módulo. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 3. Public Structure DetalleArchivoLicencia Public Estatus As EstatusLicencia Public Licenciatario As String Public FechaLicencia As Date Public FechaExpiracion As Date Public VersionCubierta As String Public NumeroSerie As String End Structure 628 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 628 18/2/10 11:20:07 Como opción predeterminada, el archivo de licencia aparece en el mismo directorio que la aplicación, al usar el nombre LicenciaBiblioteca.lic. Agregue la constante global al módulo General que identifica este nombre predeterminado. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 4. Public Const ArchivoLicenciaPredeterminado _ As String = "LicenciaBiblioteca.lic" Todo lo que necesitamos ahora es algo de código para llenar en la estructura ArchivoLicenciaPredeterminado. Agregue la función ExaminarLicencia al módulo General. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 5. Public Function ExaminarLicencia( ) As DetalleArchivoLicencia ' ----- Examinar el archivo de licencia de la aplicacion y ' reportar lo que hay dentro. Dim resultado As New DetalleArchivoLicencia Dim usarRuta As String Dim ContenidoLicencia As XmlDocument Dim clavePublica As RSA Dim documentoFirmado As SignedXml Dim nodosCoincidentes As XmlNodeList Dim partesVersion( ) As String Dim contador As Integer Dim parteComparar As String ' ----- Ver si existe el archivo de licencia. resultado.Estatus = EstatusLicencia.ArchivoLicenciaFaltante usarRuta = My.Settings.LicenseFileLocation If (usarRuta = "") Then usarRuta = _ My.Computer.FileSystem.CombinePath( _ My.Application.Info.DirectoryPath, ArchivoLicenciaPredeterminado) If (My.Computer.FileSystem.FileExists(usarRuta) = False) _ Then Return resultado ' ----- Tratar de leer el archivo. resultado.Estatus = EstatusLicencia.ArchivoLicenciaCorrupto Try ContenidoLicencia = New XmlDocument( ) ContenidoLicencia.Load(usarRuta) Catch ex As Exception ' ----- Error silencioso. Return resultado End Try Proyecto | 629 22_PATRICK-CHAPTER_22.indd 629 18/2/10 11:20:07 ' ----- Preparar el recurso de clave publica para uso. clavePublica = RSA.Create( ) clavePublica.FromXmlString(My.Resources.LicensePublicKey) ' ----- Confirmar la firma digital. Try documentoFirmado = New SignedXml(ContenidoLicencia) nodosCoincidentes = ContenidoLicencia.GetElementsByTagName( _ "Firma") documentoFirmado.LoadXml(CType(nodosCoincidentes(0), _ XmlElement)) Catch ex As Exception ' ----- Todavia un documento corrupto. Return resultado End Try If (documentoFirmado.FirmaRevisada(clavePublica) = False) Then resultado.Estatus = EstatusLicencia.FirmaNoValida Return resultado End If ' ----- El archivo licencia es valido. Extraer sus miembros. Try ' ----- Obtener el nombre del licenciatario. nodosCoincidentes = ContenidoLicencia.GetElementsByTagName( _ "Licenciatario") resultado.Licenciatario = nodosCoincidentes(0).InnerText ' ----- Obtener la fecha de licencia. nodosCoincidentes = ContenidoLicencia.GetElementsByTagName( _ "FechaLicencia") resultado.FechaLicencia = CDate(nodosCoincidentes(0).InnerText) ' ----- Obtener la fecha de expiracion. nodosCoincidentes = ContenidoLicencia.GetElementsByTagName( _ "FechaExpiracion") resultado.FechaExpiracion = CDate(nodosCoincidentes(0).InnerText) ' ----- Obtener el numero de version. nodosCoincidentes = ContenidoLicencia.GetElementsByTagName( _ "VersionCubierta") resultado.VersionCubierta = nodosCoincidentes(0).InnerText ' ----- Obtener el numero de serie. nodosCoincidentes = ContenidoLicencia.GetElementsByTagName( _ "NumeroSerie") resultado.NumeroSerie = nodosCoincidentes(0).InnerText Catch ex As Exception ' ----- Todavia un documento corrupto. Return resultado End Try ' ----- revisar fechas fuera de rango. If (resultado.FechaLicencia > Today) Then 630 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 630 18/2/10 11:20:07 resultado.Estatus = EstatusLicencia.AunNoLicenciada Return resultado End If If (resultado.FechaExpiracion < Today) Then resultado.Estatus = EstatusLicencia.LicenciaExpirada Return resultado End If ' ----- Revisar la version. partesVersion = Split(resultado.VersionCubierta, ".") For contador = 0 To UBound(partesVersion) If (IsNumeric(partesVersion(contador)) = True) Then ' ----- The version format is ' major.minor.build.revision. Select Case contador Case 0 : parteComparar = _ CStr(My.Application.Info.Version.Major) Case 1 : parteComparar = _ CStr(My.Application.Info.Version.Minor) Case 2 : parteComparar = _ CStr(My.Application.Info.Version.Build) Case 3 : parteComparar = _ CStr(My.Application.Info.Version.Revision) Case Else ' ----- Corromper numero version. Return resultado End Select If (Val(parteComparar) <> _ Val(partesVersion(contador))) Then resultado.Estatus = EstatusLicencia.VersionNoCoincidente Return resultado End If End If Next contador ' ----- Todo parece en orden. resultado.Estatus = EstatusLicencia.LicenciaValida Return resultado End Function Es mucho código, pero la mayor parte sólo carga y extrae valores del archivo de licencia XML. La parte de revisión de firma es relativamente corta. clavePublica = RSA.Create( ) clavePublica.FromXmlString(My.Resources.LicenciaClavePublica) documentoFirmado = New SignedXml(ContenidoLicencia) nodosCoincidentes = ContenidoLicencia.GetElementsByTagName( _ "Firma") documentoFirmado.LoadXml(CType(nodosCoincidentes(0), XmlElement)) If (documentoFirmado.FirmaRevisada(clavePublica) = False) Then ' ----- No valido End If Proyecto | 631 22_PATRICK-CHAPTER_22.indd 631 18/2/10 11:20:07 El objeto SignedXml (que también se usa para generar el archivo de licencia original) necesita saber exactamente qué etiqueta XML de su contenido representa la firma digital. Pensaría que tener un elemento denominado Despliegue la licencia en el formulario Acerca de Cuando agregamos el formulario Acerca de al proyecto hace unos cientos de páginas, incluimos un control Label denominado InfoLicencia. Actualmente despliega siempre “Sin licencia”, pero ahora tenemos las herramientas para desplegar una licencia apropiada, si está disponible. Abra el código fuente para el formulario ProgramaAcercaDe.vb, y agregue el siguiente código para iniciar el manejador de eventos ProgramaAcercaDe_Load. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 6. ' ----- Preparar el formulario. Dim detallesLicencia As DetalleArchivoLicencia ' ----- Desplegar el licenciatario. detallesLicencia = ExaminarLicencia( ) If (detallesLicencia.Estatus = EstatusLicencia.LicenciaValida) Then InfoLicencia.Text = _ "Licenciado a " & detallesLicencia.Licenciatario & vbCrLf & _ "Serial number " & detallesLicencia.NumeroSerie End If En la figura 22-8 se muestra el formulario AcercaDe en uso con detalles desplegados desde el archivo de licencia. Sólo por diversión, cambie el número de versión en mi archivo de licencia de “1.*” a “2.*” sin actualizar la firma digital. Seguramente, cuando despliegue el formulario AcercaDe nuevamente, desplegará “Sin licencia”, porque la revisión de la firma falló. ¿Cómo probé el código tan temprano? Copié el archivo LicenciaBiblioteca.lic del subdirectorio ArchivosLicencia instalado del libro y coloqué esa copia en el subdirectorio bin\Debug del código fuente del proyecto. Más adelante podrá colocar el archivo en cualquier lugar que quiera y explorarlo, pero estamos adelantándonos. Imposición de la licencia En algún momento, una licencia faltante o no válida debe tener un impacto negativo en el uso de la aplicación. Cuando eso pasa, debemos darle al usuario una oportunidad de corregir el problema al localizar un archivo de licencia válido. Haremos esto a través del nuevo formulario LocalizarLicencia.vb. Ya he agregado ese formulario a su proyecto. Aparece en la figura 22-9. 632 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 632 18/2/10 11:20:07 Figura 22-8. Despliegue de una licencia válida. Figura 22-9. La forma gentil de implementar una licencia de producto. Este formulario inicia con una llamada a su función CambiarLicencia, que regresa True si el usuario cambia la licencia. Casi todo este código de formulario administra el despliegue, presentando razones detalladas de por qué la licencia es válida o no al usar los resultados de la función ExaminarLicencia. Si por cualquier razón la licencia no es válida, un clic en el botón Localizar permite al usuario explorar una versión mejor. Private Sub ProgramaAcercaDe_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load ' ----- Actualiza el numero de version. Proyecto | 633 22_PATRICK-CHAPTER_22.indd 633 18/2/10 11:20:08 With My.Application.Info.Version VersionPrograma.Text = "Version " & .Major & "." & _ .Minor & " Revision " & .Revision End With ' ----- Preparar el formulario para posterior desvanecimiento. Me.Opacity = 0.99 ' ----- Preparar el formulario. Dim detallesLicencia As DetalleArchivoLicencia ' ----- Desplegar el licenciatario. detallesLicencia = ExaminarLicencia( ) If (detallesLicencia.Estatus = EstatusLicencia.LicenciaValida) Then InfoLicencia.Text = _ "Licenciado a " & detallesLicencia.Licenciatario & vbCrLf & _ "Serial number " & detallesLicencia.NumeroSerie End If End Sub La variable de nivel de formulario UbicacionModificada se vuelve a enviar al que llama como un desencadenante para actualizar el estado de la licencia. Para el Proyecto Biblioteca en particular, no vi ninguna razón para implementar la licencia al inicio, ya que no es culpa del cliente que la biblioteca robe este trabajo de software importante. En cambio, regreso el proceso de verificación hasta que un administrador o bibliotecario intente acceder a las características mejoradas de la aplicación. Después, si la revisión de licencia falla, el usuario debe ser capaz de explorar el disco en busca de un archivo de licencia válido. Pienso que el mejor lugar para agregar la revisión de licencia es justo después de que el administrador proporciona de manera correcta una contraseña. Si revisamos antes de ese punto, daría a los clientes ordinarios la capacidad de explorar el disco, que es tal vez un no no, ya que cualquiera y su tío pueden entrar y usar la estación de trabajo del cliente. Abra el código fuente del formulario CambiarUsuario.vb, localice el manejador de eventos AccAceptar_Click y ubique el comentario “Inicio de sesión correcto”. ' ----- Inicio de sesion correcto. IdUsuarioInicio = CInt(infoBD!ID) NombreUsuarioInicio = CStr(infoBD!IdInicio) ... Justo antes de este bloque de código, agregue el siguiente código de revisión de licencia. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 7. ' ----- No permitir el inicio de sesion si el programa no tiene licencia. Do While (ExaminarLicencia( ).Estatus <> _ EstatusLicencia.LicenciaValida) ' ----- Ask the user what to do. If (MsgBox("Esta aplicacion no tiene una licencia apropiada " & _ "para uso administrativo. Si tiene acceso a " & _ "un archivo de licencia valido, puede verificarlo ahora. " & _ 634 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 634 18/2/10 11:20:08 "Confirme si le gustaria localizar un archivo de licencia valido " & _ "en este momento.", MsgBoxStyle.YesNo Or _ MsgBoxStyle.Question, TituloPrograma) <> _ MsgBoxResult.Yes) Then infoBD.Close() infoBD = Nothing Return End If ' ----- Pedir una licencia actualizada. Call LocalizarLicencia.CambiarLicencia( ) LocalizarLicencia = Nothing Loop Este código le da al usuario un número ilimitado de posibilidades de ubicar un archivo de licencia válido. Una vez que la licencia sea válida, el código se mueve hacia adelante y permite acceso administrativo. Procesamiento del elemento Daily El último conjunto importante de código que habrá de agregarse al proyecto Biblioteca no está relacionado con el licenciamiento, pero es importante: el procesamiento de multas para elementos no pagados. Agregaremos un método común que realizará el procesamiento, y después lo llamaremos cuando se necesite a través de la aplicación. Agregue el nuevo método ProcesamientoDiarioPorCopiaCliente al módulo General. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 8. Public Sub ProcesamientoDiarioPorCopiaCliente( _ ByVal idCopiaCliente As Integer, ByVal hastaFecha As Date) ' ----- Esta rutina hace la mayor parte del trabajo basico de ' procesamiento de multas vencidas. Todas las otras ' rutinas de procesamiento diario llaman a esta rutina. Dim textoSql As String Dim infoBD As SqlClient.SqlDataReader Dim DiasDeMulta As Integer Dim ultimoProceso As Date Dim multasHastaAhora As Decimal On Error GoTo ErrorHandler ' ----- Obtener todos los valores basicos necesarios para ' procesar esta entrada. textoSql = "SELECT PC.FechaVencimiento, PC.FechaProceso, " & _ "PC.Multa, CMT.MultaDiaria FROM CopiaCliente AS PC " & _ "INNER JOIN CopiaArticulo AS IC ON PC.CopiaArticulo = IC.ID " & _ "INNER JOIN ArticuloConNombre AS NI ON IC.IdArticulo = NI.ID " & _ "INNER JOIN CodigoTipoMedio AS CMT ON “ & _ "NI.TipoMedio = CMT.ID " & _ Proyecto | 635 22_PATRICK-CHAPTER_22.indd 635 18/2/10 11:20:08 "WHERE PC.ID = " & idCopiaCliente & _ " AND PC.FechaVencimiento <= " & FechaBD(Today) & _ " AND PC.Regresado = 0 AND PC.Faltante = 0 " & _ "AND IC. Faltante = 0" infoBD = CrearLector(textoSql) If (infoBD.Read = False) Then ' ----- Falta el registro de copia del cliente. Oh bueno. ' tal vez sea porque este articulo no estaba ' vencido, o faltaba, o algo valido como ' que las multas no deben incrementarse. infoBD.Close( ) infoBD = Nothing Return End If ' ----- Si ya hemos procesado este registro para hoy, ' no hacerlo de nuevo. If (IsDBNull(infoBD!FechaProceso) = False) Then If (CDate(infoBD!FechaProceso) >= hastaFecha) Then infoBD.Close( ) infoBD = Nothing Return End If ultimoProceso = CDate(infoBD!FechaProceso) Else ultimoProceso = CDate(infoBD!FechaVencimiento) End If ' ----- Las multas estan vencidas en este registro. Idear cuanto. DiasDeMulta = CInt(DateDiff(DateInterval.Day, _ CDate(infoBD!FechaVencimiento), hastaFecha) - _ DateDiff(DateInterval.Day, CDate(infoBD!FechaVencimiento), _ ultimoProceso) - FineGraceDays) If (DiasDeMulta < 0) Then DiasDeMulta = 0 multasHastaAhora = 0@ If (IsDBNull(infoBD!Multa) = False) Then _ multasHastaAhora = CDec(infoBD!Multa) multasHastaAhora += CDec(infoBD!MultaDiaria) * CDec(DiasDeMulta) infoBD.Close( ) infoBD = Nothing ' ----- Actualizar el registro con la ultima multa y ' la informacion de procesamiento. textoSql = "UPDATE CopiaCliente SET " & _ "FechaProceso = " & FechaBD(hastaFecha) & _ ", Multa = " & Format(multasHastaAhora, "0.00") & _ " WHERE ID = " & IDCopiaCliente EjecutarSQL(textoSql) Return ErrorHandler: GeneralError("ProcesamientoDiarioPorCopiaCliente", Err.GetException( Resume Next End Sub )) 636 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 636 18/2/10 11:20:08 Este código examina un registro CopiaCliente (el registro que marca la revisión de un solo elemento por un cliente) para ver si no se ha pagado, y si es así, qué multas se necesitan agregar al registro. Cada registro incluye un campo FechaProceso. No queremos cobrar el cliente dos veces en el mismo día por un solo artículo sin pagar (no, no queremos), así que usamos FechaProceso para confirmar qué días no se cobran. Hay algunos lugares de la aplicación donde queremos llamar esta rutina de procesamiento sin molestar al usuario. El primero es el formulario RegistrosCliente, el formulario que despliega las multas que un cliente todavía debe. Justo antes de mostrar esa lista, debemos actualizar cada elemento revisado por el cliente para asegurarnos de desplegar la información de multa más reciente. Abra el código fuente del formulario, ubique el manejador de eventos RegistrosCliente_Load y agregue el código siguiente, justo antes de llamar a ActualizarMultasCliente (-1) que aparece a mitad de la rutina. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 9. ' ----- Asegurarse de que cada articulo esta actualizado. For contador = 0 To ArticulosPrestados.Items.Count - 1 nuevaEntrada = CType(ArticulosPrestados.Items(contador), ArticuloDetalleCliente) ProcesamientoDiarioPorCopiaCliente(nuevaEntrada.IDDetalle, Today) Next contador El estado sin pagar de un elemento también debe actualizarse justo antes de que se revise. Abra el código fuente del formulario FormularioPrincipal y ubique el manejador de eventos AccEntrada_Click. Cerca de la mitad del camino a través de este código, encontrará un comentario que inicia con “Manejar artículos faltantes”. Justo antes de tal comentario, inserte el siguiente código. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 10. ' ----- Traer el estatus del articulo actualizado. ProcesamientoDiarioPorCopiaCliente(idCopiaCliente, FechaEntrada.Value) La revisión tiene que actualizar las multas del cliente también, justo antes de permitir que el cliente sepa si hay, en realidad, alguna multa vencida. Muévase al manejador de eventos FormularioPrincipal.AccClienteSalida_Click y agregue las siguientes instrucciones en la parte superior de la rutina. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 11. Dim tablaBD As Data.DataTable Dim filaBD As Data.DataRow Proyecto | 637 22_PATRICK-CHAPTER_22.indd 637 18/2/10 11:20:08 En este mismo método encuentre un comentario que inicia con “Mostrar el cliente si hay multas adeudadas”. Como es usual, está cerca de la mitad de la rutina. Inserte el siguiente código justo antes de ese comentario. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 12. ' ----- Actualizar el registro del cliente. textoSql = "SELECT ID FROM CopiaCliente WHERE Regresado = 0 " & _ "AND Faltante = 0 AND FechaVencimiento < " & FechaBD(Today) & _ " AND (FechaProceso IS NULL OR FechaProceso < " & _ FechaBD(Today) & ") AND Cliente = " & idCliente tablaBD = CrearTablaDatos (textoSql) For Each filaBD In dbTable.Rows ProcesamientoDiarioPorCopiaCliente(CInt(filaBD!ID), Today) Next filaBD tablaBD.Dispose( ) tablaBD = Nothing Además del procesamiento de multas automático, el proyecto Biblioteca también permite a un administrador o bibliotecario realizar procesamiento diario de todos los artículos de cliente a voluntad. Esto ocurre mediante el panel Procesamiento diario en el formulario principal (véase la figura 22-10). Figura 22-10. Procesamiento administrativo diario. Actualmente, el panel no hace mucho, así que cambiemos eso. La primera tarea es actualizar la etiqueta de estado que aparece en la parte superior del panel. Agregue un nuevo método denominado ActualizarUbicacionProceso a la clase FormularioPrincipal del formulario. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 13. 638 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 638 18/2/10 11:20:09 No mostraré su código aquí, pero básicamente revisa el campo de la base de datos CodigoLugar.UltimoProcesamiento para todas las ubicaciones, o para la ubicación seleccionada por el usuario, y actualiza el estado de despliegue de acuerdo con esto. El usuario selecciona una ubicación para procesamiento dentro de la lista desplegable ProcesarUbicacion, pero no hemos agregado todavía ningún código para rellenar esa lista. Encuentre el método TareaProceso en el código fuente del formulario principal, y agregue estas instrucciones a la parte superior de este código. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 14. Dim textoSql As String Dim infoBD As SqlClient.SqlDataReader On Error GoTo ErrorHandler Después agregue estas instrucciones al final del método. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 15. ' ----- Actualizar la lista de ubicaciones. ProcesarUbicacion.Items.Clear( ) ProcesarUbicacion.Items.Add(New ListItemData( _ " Proyecto | 639 22_PATRICK-CHAPTER_22.indd 639 18/2/10 11:20:09 Cada vez que el usuario selecciona una ubicación diferente de la lista, necesitamos actualizar el despliegue del estado. Agregue el siguiente código al manejador de eventos ProcesarUbicacion_SelectedIndexChanged. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 16. ' ----- Actualizar el estatus con base en la ubicacion actual. ActualizarProcesarUbicacion( ) El procesamiento diario ocurre cuando el usuario hace clic en el botón Procesar. Agregue el siguiente código al manejador de eventos AccProcesar_Click. Inserción de fragmento de código Inserte el fragmento de código Cap22, Elemento 17. ' ----- Procesar todos los libros prestados. Dim textoSql As String Dim tablaBD As Data.DataTable Dim filaBD As Data.DataRow Dim idUbicacion As Integer On Error GoTo ErrorHandler Me.Cursor = Cursors.WaitCursor ' ----- Obtener la lista de todos los articulos que probablemente necesiten procesamiento. textoSql = "SELECT PC.ID FROM CopiaCliente AS PC " & _ "INNER JOIN CopiarArticulo AS IC ON PC.CopiaArticulo = IC.ID "& _ "WHERE PC.Regresado = 0 AND PC.Faltante = 0 " & _ "AND IC.Faltante = 0 AND PC.FechaVencimiento < " & FechaBD(Today) & _ " AND (PC.FechaProceso IS NULL OR PC.FechaProceso < " & _ FechaBD(Today) & ")" If (ProcesarUbicacion.SelectedIndex <> -1) Then idUbicacion = CInt(CType(ProcesarUbicacion.SelectedItem, _ DatosListaElementos)) If (idUbicacion <> -1) Then textoSql &= _ " AND IC.Location = " & idUbicacion Else idUbicacion = -1 End If tablaBD = CrearTablaDatos(textoSql) For Each filaBD In tablaBD.Rows ProcesamientoDiarioPorCopiaCliente(CInt(filaBD!ID), Today) Next filaBD tablaBD.Dispose( ) tablaBD = Nothing Me.Cursor = Cursors.Default 640 | Capítulo 22: Otorgue licencia a su aplicación 22_PATRICK-CHAPTER_22.indd 640 18/2/10 11:20:09 MsgBox("Procesamiento completo.", MsgBoxStyle.OkOnly Or _ MsgBoxStyle.Information, TituloPrograma) ' ----- Actualizar la fecha de procesamiento. textoSql = "UPDATE CodigoLugar SET UltimoProcesamiento = " & _ FechaBD(Today) If (idUbicacion <> -1) Then textoSql &= _ " WHERE ID = " & idUbicacion EjecutarSQL(textoSql) ' ----- Actualizar el despliegue de estatus. ProcesarEstatus.Text = " El procesamiento esta actualizado." ProcesarEstatus.ImageIndex = ImagenEstatusBueno Return ErrorHandler: GeneralError("FormularioPrincipal.AccProcesar_Click", Err.GetException( )) Resume Next Pruebe el código, ejecútelo, localice un archivo de licencia válido y pruebe las diferentes características administrativas. Esto marca el final de la colocación de código principal en el proyecto Biblioteca. ¡Felicidades! Pero todavía hay algo mucho que hacer, como puede ver por la presencia de cuatro capítulos más. Ahora no sería el momento de cerrar el libro y dar por terminado el día. Pero sería un buen momento para aprender acerca de ASP.NET, el tema del siguiente capítulo. Proyecto | 641 22_PATRICK-CHAPTER_22.indd 641 18/2/10 11:20:09 Capítulo 23 Desarrollo Web Cuando sir Tim Berners-Lee (recibió el título de caballero en 2004) inventó World Wide Web en 1989, realmente no era muy importante. Como el diseñador principal de HTTP y HTML, ciertamente no fue perezoso. Pero la mayor parte de las tecnologías para estructuración y transporte de páginas Web existía desde años antes, incluso décadas. SGML (la base de HTML) y los sistemas de hipervinculación han existido desde la década de 1960, y la transmisión de datos basada en Internet entre clientes y servidores ya era común entre campus de universidades y algunos negocios. Aún así, aquí estamos en el siglo xxi, y World Wide Web es el centro de tanta tecnología computacional que hace que mi cabeza gire. Gracias, Sr. B-L. Microsoft promueve .NET como el sistema para desarrollar páginas Web y software relacionado. Y realmente es un buen sistema. Conforme nos adentremos en el código, descubrirá que cerca de 90% de lo que hace para escribir aplicaciones Web en Visual Studio es idéntico a lo que hace cuando escribe aplicaciones de escritorio. Es fácil de hacer, y muy divertido, así que tal vez querrá escribir algunos programas. Y eso es lo que haremos en este capítulo. Pero primero, revisemos brevemente lo que pasa en el mundo de la comunicación de World Wide Web. Cómo funciona Internet Antes de .NET, desarrollar aplicaciones para “Web” era difícil y aburrido. Y por buenas razones: World Wide Web no fue diseñado como una plataforma de programación o procesamiento lógico. Se orientó originalmente por completo a enviar archivos de texto con formato especializado de una computadora a otra. No había ningún lenguaje de programación que aprender, ninguna lógica personalizada; sólo texto simple, y tal vez una o dos imágenes gráficas binarias. Los primeros exploradores en realidad sólo eran programas glorificados de copia de archivos. Cuando iniciaba el explorador Mosaic (casi todo lo que había en ese entonces) y pedía una página Web de otra computadora, aquí se muestra lo que podía pasar: 642 23_PATRICK-CHAPTER_23.indd 642 17/2/10 15:33:39 1. El explorador Web determinaba la dirección IP del sistema remoto. 2. El explorador Web se ponía en contacto con el sistema remoto por medio del número de puerto 80 TCP/IP. 3. El sistema remoto aceptaba la conexión. 4. El explorador Web decía: “Oye, estoy buscando un archivo llamado index.html. ¿Me lo podrías enviar?” 5. El sistema remoto decía: “Lo tengo”, y lo mandaba de inmediato. 6. El sistema remoto cerraba la conexión. Mucho de este proceso está oculto de la vista, pero realmente puede ver cómo pasa. Si está interesado, abra el indicador de comandos de Windows y escriba el siguiente comando: telnet www.google.com 80 Esto ejecuta el programa Telnet, un programa de emulación de terminal que permite conectarse a sistemas remotos a través de una interfaz de texto. (Telnet está instalado en Windows XP como opción predeterminada, pero es opcional en Windows Vista. Puede agregarlo a Windows Vista mediante la applet Programas y características del Panel de control.) Telnet suele conectarse al puerto 23 TCP/IP, pero puede especificarse el puerto que desee, como lo hicimos aquí con el puerto WWW predeterminado de 80. Es posible que su pantalla se quede en blanco, o que se quede así, como si estuviera muerta. Si tiene suerte, verá un mensaje “connected”, pero tal vez no. Y está bien. Su sistema está conectado al servidor Web de Google. Escriba el siguiente comando: GET / HTTP/1.0 No olvide los espacios que rodean la primera diagonal. Después de este comando oprima dos veces la tecla Enter. Este comando pide al sistema remoto enviar la página Web predeterminada en la parte superior de la jerarquía Web del servidor. Y porque lo pidió, lo hará. HTTP/1.0 200 OK Cache-Control: private Content-Type: text/html; charset=ISO-8859-1 Set-Cookie: PREF=ID=1c1dd342e463f3f1:TM=1199325226 ↵ :LM=1199325226:S=Pl-4f1fg4yh8Mvw7; ↵ expires=Sat, 02-Jan-2010 01:53:46 GMT; ↵ path=/; domain=.google.com Server: gws Date: Tue, 01 Jan 2008 01:30:00 GMT Connection: Close ...resto del contenido de la pagina Web HTML aqui... Connection to host lost. 23_PATRICK-CHAPTER_23.indd 643 Cómo funciona Internet | 643 17/2/10 15:33:39 Por supuesto, normalmente no ve todo esto. El explorador Web lleva a cabo este diálogo, y amablemente forma la respuesta como una página Web. Esto es en realidad todo lo que hay de World Wide Web. Ha experimentado las características principales: la trasferencia básica de datos a través del puerto TCP/IP. Entonces, ¿dónde entra la programación? Programación de Internet Las páginas estáticas estuvieron bien por un tiempo, pero después Internet se volvió monótono. Finalmente, alguien tuvo una brillante idea: “Tenemos un programa en ejecución en nuestro servidor Web que está respondiendo a clientes, y alimentándolo de páginas pedidas. ¿Qué pasaría si pudiéramos mejorarlo para que, en ciertas páginas, llamara a un programa o una secuencia de comandos que generara el contenido HTML al vuelo y enviara de regreso el contenido al cliente?” Entonces cambiaron el proceso de servidor. Ahora, cuando el cliente pedía una página Web que terminaba con una extensión .cgi, el proceso de servidor Web ejecutaba la secuencia de comandos que generaba el contenido. El sistema también proporcionaba los medios para contenido proporcionado por el cliente para abrirse paso hasta la secuencia de comandos, haciendo posible la personalización de características. De ahí, el paso era corto a la obtención de una solución genérica. En la plataforma de Microsoft, el servicio de información de Internet (IIS, Internet Information Server) dio soporte a complementos a los que podía llamarse con base en la extensión del archivo solicitado. Esto llevó, a las páginas activas de servidor (ASP, Active Server Pages), una solución que permitió a los desarrolladores incrustar secuencias de comandos del lado del servidor (a menudo utilizando VBScript, una variación de Visual Basic) en el contenido HTML, y que hacía que ajustara el contenido antes de enviarse al cliente. Alguien más dijo: “Si podemos escribir secuencia de comandos en el lado del servidor, ¿no podríamos también incluir una ‘secuencia de comandos del lado del cliente’ justo en el contenido HTML que un explorador Web inteligente pudiera procesar?” Al poco tiempo, los desarrolladores del lado del cliente y servidor estaban combatiendo en las calles, pero la batalla no llegó muy lejos, porque todos los programadores estaban cansados. ¿La causa? ¡Programar en secuencia de comandos! Ya sea incrustar secuencias de comandos en HTML (el lado del cliente) o generar HTML desde una secuencia de comandos (el lado del servidor), la programación de secuencia de comandos es difícil, lenta, alta en colesterol “malo” y casi imposible de depurar interactivamente. Algunos programadores de secuencias de comandos Web no habían usado un compilador de lenguaje por años, y estaban al borde del colapso debido a comas fatales inducidos por secuencia de comandos. Se podía compilar lógica del lado del servidor en una DLL y usarlo para procesar páginas Web, pero estaba lejos de ser sencillo, y estas DLL todavía estaban a menudo vinculadas en el contenido HTML por medio de secuencias de comandos cortas. Después llegó .NET y su soporte para desarrollo de aplicación compilada del lado de servidor. Los programadores de secuencias de comandos lanzaron un suspiro colectivo de alivio desde sus camas de hospital; ahora podían usar el poder completo de los lenguajes Visual Studio y .NET para generar contenido HTML. Y este nuevo sistema, ASP.NET, fue diseñado para que se 644 | Capítulo 23: Desarrollo Web 23_PATRICK-CHAPTER_23.indd 644 17/2/10 15:33:39 pudiera diseñar una aplicación Web completa sin ver siquiera una simple etiqueta HTML. La meta de diseño: hacer desarrollo Web casi idéntico al desarrollo de aplicaciones de escritorio. Y Microsoft tuvo mucho éxito. No resolvió el problema de la secuencia de comandos de cliente (¡tal vez pronto!), pero algunas de las características nuevas del lado del servidor incluidas en ASP.NET redujeron en mucho la necesidad de secuencias de comandos personalizadas del lado del cliente. Las páginas que se generan en ASP.NET se denominan Web Forms, y debido a que están tan unidas, algunas veces uso ASP.NET y Web Forms de forma intercambiable. Pero no son exactamente la misma cosa: ASP.NET incluye Web Forms. Características de ASP.NET ASP.NET incluye muchos nuevos avances en tecnología de desarrollo Web. Aquí se muestran algunos de los más famosos: Código compilado Todo el código que escriba para aplicaciones ASP.NET está totalmente compilado en ensamblados estándar DLL de .NET. Cuando el cliente pide un archivo con extensión .aspx, Internet Information Server ubica este archivo (que contiene HTML o una combinación de HTML/ ASP.NET) y la DLL compilada asociada, y los usa juntos para procesar el contenido de la página. Puede precompilar la DLL antes del despliegue, o permitir que ASP.NET lo compile al vuelo la primera vez que se llama al archivo .aspx (aunque esto afecta un poco el rendimiento). Soporte .NET Las aplicaciones ASP.NET pueden acceder a .NET Framework Class Libraries (FCL, bibliotecas de clase de .NET Framework) completas, excepto las que tienen como objetivo específico el desarrollo de escritorio. Cualquiera de las características y clases grandiosas que tiene en aplicaciones .NET de escritorio están en las aplicaciones Web también. Basado en objeto Las etiquetas HTML, como |