Archivo Mensual de Julio, 2008

Informe en .NET con Crystal Reports y base de datos MySQL.

Vaya por delante que el tema de la creación de informes (o reports) siempre ha sido una de las partes de la programación que me ha dado más pereza. Pero está claro que pocas aplicaciones se salvan de requerirlos en mayor o menor grado, y la que actualmente tengo entre manos no es, en absoluto, una excepción. Así pues, estos últimos días he tenido que preparar un nuevo report y de paso he aprovechado para investigar un poco y descubrir una nueva manera de realizarlos. Paso a explicarme.

Como ya he comentado en ocasiones, mi entorno de desarrollo es VisualBasic.NET y una base de datos MySQL. Y los informes los hago con el propio Crystal Reports que incorpora el VisualStudio.NET. El tema está en que hasta ahora los hacía a través de una conexión ODBC que tenía que instalar en cada máquina apuntando hacia el servidor MySQL, pero este sistema no me gustaba demasiado. Y no me gustaba porque en realidad de bases de datos tengo distintas con distintos nombres pero con las mismas estructuras de tablas y datos parecidos, ya que una es la productiva (la real, la buena) y las otras son para desarrollo, para que los usuarios hagan pruebas sin que afecte a los datos reales, etc. Controlar la cadena de conexión en la aplicación para que ataque a una u otra base de datos no me supone el más mínimo problema, pero los informes cogen sus datos a través de una conexión ODBC concreta y ésta en cada máquina ataca a una única base de datos. Así que no me convence este sistema.

Y ahora he descubierto cómo puede crear informes a través de un DataTable y de una estructura en un archivo XML, sin necesidad de nada más. De hecho en lugar de un DataTable se puede utilizar perfectamente un DataSet y funciona sin ningún tipo de problemas ni apenas modificaciones (dejo en manos del lector probarlo si le interesa). Voy a pasar a explicar mediante un ejemplo y algunas imágenes cómo realizarlo desde cero.

Partiré de dos tables en mi base de datos MySQL en las que guardaré información de facturas. La primera tabla es de cabeceras de esas facturas y tiene estos datos:

+---------+------------+-------------------------+
| blh_num | blh_dat    | blh_cus                 |
+---------+------------+-------------------------+
|       1 | 2008-07-30 | CERAMICAS PEPE, S.A.    |
|       2 | 2008-07-30 | TALLERES GOMEZ, S.L.    |
|       3 | 2008-07-31 | DEPORTES DAMIAN, S.L.   |
|       4 | 2008-07-31 | SOFTWARE ALBERTMATA.NET |
+---------+------------+-------------------------+

La segunda tabla son las posiciones de cada factura y tiene estos otros datos:

+---------+---------+------------------------+---------+---------+
| blp_num | blp_pos | blp_art                | blp_pri | blp_qty |
+---------+---------+------------------------+---------+---------+
|       1 |       1 | RATON LOGITECH         |   15.95 |       1 |
|       2 |       1 | MONITOR LG 19 PULGADAS |   210.5 |       1 |
|       3 |       1 | ROUTER DLINK           |      56 |       1 |
|       4 |       1 | RATON LOGITECH         |   15.95 |       2 |
|       4 |       2 | TECLADO LOGITECH       |   12.95 |       1 |
|       4 |       3 | RECEPTOR GPS ZAPPA     |   59.95 |       1 |
|       4 |       4 | PAQUETE 500 FOLIOS     |     3.7 |       4 |
+---------+---------+------------------------+---------+---------+

Es algo muy sencillo y poco normalizado, pero nos servirá para el ejemplo. Concretamente vamos a crear un informe que será nada más que una impresión de una factura. Como voy a trabajar con un simple DataTable pero mi informe requerirá datos de dos tablas, me crearé primero una vista en MySQL con la siguiente instrucción:

CREATE VIEW zbl_bill2print AS 
(
SELECT
    blh_num AS BILL_NUMBER,
    blh_dat AS BILL_DATE,
    blh_cus AS BILL_CUSTOMER,
    blp_pos AS LINE_NUMBER,
    blp_art AS LINE_ARTICLE,
    blp_pri AS LINE_UNITPRICE,
    blp_qty AS LINE_UNITS,
    blp_pri * blp_qty AS LINE_TOTALPRICE
FROM
    blh_billheader LEFT JOIN blp_billposits ON blh_num = blp_num
WHERE
    blh_num = 4
);

Así, mi informe a partir de ahora se realizará sobre esta vista zbl_bill2print. Vamos pues a empezar con la parte que incumbe a .NET.

Paso 1. Creación del archivo XML con la estructura de la tabla/vista.

Para todo el ejemplo jugaremos con:

1) un formulario frmMain donde tendremos el objeto visualizador de informes.
2) una clase clsReportCreator que crearemos a continuación.
3) un informe rptBill que representará la factura que queremos imprimir.

Comenzamos pues creando la clase clsReportCreator, que constará de un único atributo (el nombre de la tabla o vista), un método constructor, un método para cargar el DataTable correspondiente y un último método para generar el archivo XML. El código completo de esta clase es el siguiente:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080731
' Needs:       MySQL.Data reference.
' Description: Class to create a report using just an XML file. 
'--------------------------------------------------------------------
Imports MySql.Data.MySqlClient

Public Class clsReportCreator

    '----------------------------------------------------------------
    ' Attributes.
    '----------------------------------------------------------------
    Private TableOrView As String

    '----------------------------------------------------------------
    ' Constructor method.
    '----------------------------------------------------------------
    Public Sub New(ByVal TableOrView As String)
        Me.TableOrView = TableOrView
    End Sub

    '----------------------------------------------------------------
    ' Returns DataTable corresponding to TableOrView.
    '----------------------------------------------------------------
    Public Function GetDataTable() As DataTable
        Dim DA As MySqlDataAdapter
        Dim DS As New DataSet
        Dim DT As DataTable
        Dim ConnectionString As String
        Dim SQL As String

        'Setting connection string to connect to MySQL database.
        ConnectionString = "Database = blog; " _
                         & "Data Source = localhost; " _
                         & "User ID = root; " _
                         & "Password = mypassword"

        'Setting SQL string.
        SQL = "SELECT * FROM " & Me.TableOrView

        'Getting data and filling DataSet and DataTable.
        DA = New MySqlDataAdapter(SQL, ConnectionString)
        DA.Fill(DS, Me.TableOrView)
        DT = DS.Tables(Me.TableOrView)

        'Returning DataTable.
        Return DT
    End Function

    '----------------------------------------------------------------
    ' Creates XML file in desired path.
    '----------------------------------------------------------------
    Public Sub CreateXMLFile(ByVal FilePath As String)
        Dim DT As DataTable

        'Creating DataTable.
        DT = Me.GetDataTable()

        'Writting XML file in desired path.
        DT.WriteXmlSchema(FilePath & Me.TableOrView & ".xml")
    End Sub

End Class

Y creamos también el formulario frmMain cuyo único código por el momento (después cambiará un poco) será el que sigue:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080731
' Description: Form to show how to create a report using just an XML
'              file. 
'--------------------------------------------------------------------
Public Class frmMain

    '----------------------------------------------------------------
    ' As a first step, creates XML file.
    '----------------------------------------------------------------
    Private Sub frmMain_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load
       'Creating XML file.
        Dim RC As New clsReportCreator("zbl_bill2print")
        RC.CreateXMLFile("C:\")
    End Sub

End Class

Con esto tenemos una primera aplicación que al ejecutarla nos creará el archivo C:\zbl_bill2print.xml con la estructura de la vista zbl_bill2print. Lo probamos pues y obtenemos un archivo como éste.

Paso 2. Creación del informe e inserción del origen de datos.

Añadimos al proyecto un informe al que llamaremos rptBill.rpt y que crearemos seleccionando la alternativa Como informe en blanco y por tanto desestimando plantillas.

A continuación en el menú Explorador de campos seleccionamos la primera opción Campos de base de datos y en su menú contextual hacemos click en la opción Asistente de base de datos.

En el menú que se despliega (Orígenes de datos disponibles) seleccionamos Crear nueva conexión y a continuación la opción ADO.NET.

Con esto se nos abrira un nuevo formulario en el que nos solicitará la Ruta del archivo. Debemos aquí ir a buscar el archivo XML que hemos creado anteriormente (en mi caso el C:\zbl_bill2print.xml) y acto seguido pulsar en Finalizar. Con ello, en el menú anterior (Orígenes de datos disponibles) se nos mostrará ya la opción NewDataSet incluyendo el zbl_bill2print que acabamos de añadir.

Lo marcamos y le damos al botón para trasladarlo al menú de Tablas seleccionadas y pulsamos en Aceptar.

Con esto hemos conseguido que en el menú Explorador de campos nos aparezca ya la estructura de zbl_bill2print con sus campos, tal como se muestra a continuación:

Paso 3. Diseño del informe.

No tiene ningún misterio. Se trata de añadir los campos desde el menú Explorador de campos donde corresponda, insertarle los objetos de texto que nos parezcan adecuados, los totales cuando sea preciso, dar formato a los textos, incorporar imágenes y demás florituras a nuestro antojo…

Yo me inclino por un diseño sobrio como éste:

Paso 4. Últimos pasos para obtener la factura.

Por último vamos ya a crear la factura. Para ello en el formulario frmMain añadiremos un objeto de tipo CrystalReportViewer al que llamaremos por ejemplo crvBill. Y modificamos el código de frmMain para dejarlo como sigue:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080731
' Description: Form to show how to create a report using just an XML
'              file. 
'--------------------------------------------------------------------
Imports CrystalDecisions.CrystalReports.Engine

Public Class frmMain

    '----------------------------------------------------------------
    ' Creates XML file (just once) and creates and loads a report.
    '----------------------------------------------------------------
    Private Sub frmMain_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load
        'Creating XML file.
        Dim RC As New clsReportCreator("zbl_bill2print")
        'RC.CreateXMLFile("C:\")

        'Creating report.
        Dim RD As ReportDocument = New rptBill()

        'Setting data source for report.
        Dim DT As DataTable = RC.GetDataTable()
        RD.SetDataSource(DT)

        'Setting data source for possible subreports.
        For Each SR As ReportDocument In RD.Subreports
            If SR.Database.Tables.Count > 0 Then
                SR.SetDataSource(DT)
            End If
        Next

        'Setting recently created report must be shown in viewer.
        Me.crvBill.ReportSource = RD
    End Sub

End Class

Nótese que ahora ya he comentado la línea en que creamos el archivo XML, puesto que sólo necesitamos crearlo una única vez para luego poder generar el origen de datos, pero a partir de aquí no necesitaremos andar creándolo cada vez.

En este código lo que estamos haciendo fundamentalmente es crear un objeto del tipo informe que hemos diseñado en el paso 3, obtener un DataTable con los datos que queremos mostrar (en este caso y tal como tenemos definida la vista de MySQL, queremos mostrar la factura número 4), establecer que el origen de datos del informe será este DataTable y finalmente solicitarle al CrystalReportViewer que nos muestre este informe.

Ejecutamos nuevamente la aplicación y obtenemos la factura que queríamos:

Por supuesto en una factura auténtica faltarían datos fiscales, logotipos, impuestos, condiciones de pago y demás, pero lo que aquí pretendía era únicamente mostrar cómo llevar a cabo el informe en sí mismo.

Con esto queda visto cómo simplemente utilizando un archivo XML podemos crear un informe en VisualBasic.NET. Por supuesto habría que mejorar muchas cosas, por ejemplo optimizar cómo se realiza la conexión con la base de datos (particularmente tengo un clase para llevar a cabo esa serie de cuestiones), también evitar poner la condición WHERE directamente en la vista de MySQL y sí por ejemplo cuando recuperamos el DataTable… en fin, unas cuantas cosas. Pero lo que buscaba con este ejemplo era hacer algo muy minimalista para que quedara claro el funcionamiento.

Actualización.

A raíz de uno de los comentarios he añadido una pequeña segunda parte a este post, en la que se explica cómo pasar parámetros desde el formulario hasta el informe.

PcCity no cuida su página web (ni sus clientes).

Estos días estoy mirando opciones para comprarme un receptor de señal GPS que pueda conectar a mi nuevo Pocket PC mediante Bluetooth. Buscando por internet encontré este Ideus TR50 PRO en PcCity a un precio que me pareció razonable. De modo que contemplando seriamente la posibilidad de adquirirlo, utilicé la opción Consulta stock en tienda y comprobé que en el PcCity donde a menudo suelo comprar (el de Barcelona L’Illa) tenían stock, tal como se puede comprobar en la siguiente imagen (click para ampliar).

Así pues, y convencido como estaba de comprarme el gadget en cuestión, conduje los 70 kilómetros (35 de ida y otros tantos de vuelta) para hacerme con él, con la desagradable sorpresa de que al llegar a la tienda el receptor GPS no estaba expuesto en ningún lado. Pero no es que alguien se hubiera llevado la última unidad, no, es simplemente que ni los vendedores conocían ese modelo. No les sonaba nada. Me acerqué al servicio de atención al cliente por si podían mirarme en su base de datos si lo tenían o no, por si estaba en algún rincón del almacén o algo así, no sé. El caso es que hicieron la consulta y, efectivamente, me dijeron que les quedaba 1 unidad. Pero rápidamente me sacaron de mi dicha: realmente no tenían esa unidad, era solo un descuadre, ese Ideus TR50 PRO no estaba en la tienda. Mi gozo en un pozo.

Esta tarde he vuelto a verificar la página web y como podéis ver (la imagen de arriba es de ahora mismo) siguen diciendo que tienen stock disponible en su tienda PcCity de L’Illa (Barcelona). A estas alturas me parece especialmente grave. Hasta ayer podían no ser conscientes de que tenían un descuadre en el stock, pero ayer tuvieron ya la evidencia. No creo que sea tan complicado cambiarlo para que la página web lo refleje bien y ningún otro incauto como yo acuda expresamente al centro a comprarlo. Salvo que sea precisamente lo que se persigue, claro.

Es realmente preocupante que en los días que estamos todavía haya empresas que parecen ver internet como un mal que hay que sufrir y con el que hay que intentar llevarse bien en lugar de como una grandísima oportunidad de negocio. Y me baso no solo en esta unidad del stock que todavía no está corregida (haré el seguimiento a ver cuantos días necesitan para hacerlo), sino en que antes de escribir este post he intentado simplemente contactar con ellos para comentarles la jugada y que lo solucionaran, pero me he encontrado que en la página Ayúdanos a mejorar (accediendo desde la barra inferior en cualquier página de su sitio) aparecen las opciones Sugerencia y Reclamación tanto para particulares como para empresas, pero las cuatro opciones están redirigidas a páginas que muestran un error 404 (The page cannot be found), como se puede comprobar en las cuatro imágenes a continuación.

Brillante servicio de atención al cliente, sí señor. Realmente en PcCity tienen un gran interés en conocer las opiniones y quejas de sus clientes, queda claro.

En fin, que el resultado de todo ello es que he perdido 70 kilómetros y un par de horas por una mala gestión de PcCity, y que me he quedado sin el receptor GPS que quería comprar.

Nota final: Todos los empleados de PcCity con los que traté, genial. Gente amable, te intentan ayudar, muy atentos. Lástima que la empresa no parezca tener la misma actitud con sus clientes.

Actualización a 29 de julio de 2008: Han pasado ya 3 días. En la web sigue diciendo que el centro de Barcelona L’Illa tiene stock del producto. Los cuatro enlaces para sugerencias y quejas siguen sin funcionar. Imagino que se estarán congratulando de la de días que hace que no les llega ninguna queja.

Actualización a 31 de julio de 2008: Han pasado 5 días y… ¡sí, en la web ya no dice que el centro de Barcelona L’Illa tenga stock del producto! Más vale tarde que nunca. Eso sí, los cuatro enlaces para sugerencias y quejas siguen sin funcionar (a saber cuánto tiempo hará). ¿En serio no le sorprenderá al que normalmente recibe las quejas/sugerencias que haga tantos días que no le llega nada?

Actualización a 25 de agosto de 2008: ¡Lo arreglaron! Finalmente alguien se daría cuenta que ningún cliente se quejaba ni sugería nada y echarían un ojo. Lo cierto es que no sé qué día exacto fue. Durante mis vacaciones lo consulté y seguían apareciendo los errores 404 (pero vía PDA no podía postear). No obstante, bien está lo que bien acaba. :-)

Ambrosio o cómo crear un servicio de Windows obediente en .NET.

Últimamente he estado programando un servicio de Windows para que el sistema realice de manera periódica y en segundo plano unas determinadas tareas. En mi caso concreto se trata de replicar unas determinadas tablas desde una base de datos Oracle hacia otra MySQL con una determinada periodicidad en función de cada tabla (unos datos son más críticos y necesitan actualizarse cada hora, otros menos variables los actualizamos una vez al día durante las horas valle de actividad). El caso es que con VisualBasic.NET me ha resultado bastante sencillo crearlo y después instalarlo, así que paso a explicar un ejemplo de cómo hacer un sencillo servicio de Windows.

Paso 1. Creamos el proyecto.

Para empezar simplemente iniciamos VisualStudio.NET (en mi caso 2005) y seleccionamos la opción para crear un nuevo proyecto. Ahí seleccionamos Servicio de Windows, le damos un nombre agradable (a poder ser más que WindowsService1, por ejemplo ServicioAlertas) y aceptamos. Nos aparecerá directamente la clase Service1.vb que también podemos renombrar a algo más intuitivo como clsAlert.vb.

Paso 2. Agregamos un instalador.

En la vista diseño de la clase que se nos ha creado hacemos clic con el botón derecho del ratón y seleccionamos la opción Agregar instalador. Con esto se nos creará la clase ProjectInstaller.vb que contiene dos controles: ServiceInstaller1 y ServiceProcessInstaller1. En mi caso renombro estos dos controles simplemente para eliminarles el 1 final (maniático que es uno). Del código de ProjectInstaller.vb nos olvidaremos por completo, pero vamos a hacer algún ajuste en las propiedades de estos dos controles.

Paso 3. Configuramos el instalador.

En el control ServiceInstaller modificamos las propiedades DisplayName y ServiceName para dejarlas ambas en ServicioAlertas. Modificamos también la propiedad StartType para dejarla en Automatic. Esto último sirve para que después cuando instalemos el servicio, éste quede puesto para que arranque automáticamente al iniciarse el sistema operativo. Si no se desea este comportamiento, ajustar esta propiedad según convenga. Es decir:

DisplayName: ServicioAlertas
ServiceName: ServicioAlertas
StartType:   Automatic

En el control ServiceProcessInstaller modificamos la propiedad Account para dejarla en LocalSystem. Esto indica qué tipo de cuenta se utilizará para ejecutar el servicio. Podemos dejarla en User, pero después al instalar el servicio pedirá cuenta de usuario y contraseña, así que yo recomiendo pasarla a LocalSystem. Así pues:

Account:     LocalSystem

Con la clase ProjectInstaller.vb hemos terminado ya por completo, ahora nos centraremos en clsAlert.vb.

Paso 4. Programamos el servicio.

Accedemos al código de la clase clsAlert.vb y observamos que por defecto ya se nos han generado dos métodos: OnStart y OnStop. Está claro para lo que sirven, ¿no? Eso es, para configurar qué hacer y cómo para arrancar y detener el servicio. Personalmente no utilizo el OnStop para nada, así que no hablaré de él, pero por supuesto el que esté interesado puede buscar más información en la propia ayuda del IDE o en internet. Me voy a centrar en el OnStart y voy a ponerle un temporizador para que cada 5 segundos me escriba en un log la hora actual. Sí, lo sé, es un servicio de alertas mediocre, pero servirá como ejemplo de código sencillo con el que programar un servicio y hacer uso, además, de un temporizador (cosa terriblemente útil en ocasiones).

Muestro el código íntegro que dejo pues en la clase clsAlert.vb:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080723
' Description: Class to show how to create a Windows service and how
'              to work with a timer. 
'--------------------------------------------------------------------
Public Class clsAlert

    '----------------------------------------------------------------
    ' Attributes.
    '----------------------------------------------------------------
    Private DBTimer As System.Timers.Timer

    '----------------------------------------------------------------
    ' Starts service.
    '----------------------------------------------------------------
    Protected Overrides Sub OnStart(ByVal args() As String)
        'Creating timer with interval = 5000 milisec = 5 seconds.
        DBTimer = New System.Timers.Timer(5000)
        DBTimer.Enabled = True
        AddHandler DBTimer.Elapsed, AddressOf Me.ShowAlert
    End Sub

    '----------------------------------------------------------------
    ' Stops service.
    '----------------------------------------------------------------
    Protected Overrides Sub OnStop()
        'Void, as I'm not using this method.
    End Sub

    '----------------------------------------------------------------
    ' Main process executed every time DBTimer gives a signal.
    '----------------------------------------------------------------
    Private Sub ShowAlert(ByVal source As Object, _
    ByVal e As System.Timers.ElapsedEventArgs)
        DBTimer.Enabled = False
        Dim LogFile As New System.IO.StreamWriter("C:\log.txt", True)
        LogFile.WriteLine("Alerta www.albertmata.net - " & Date.Now)
        LogFile.Close()
        DBTimer.Enabled = True
    End Sub

End Class

Con esto tenemos terminado nuestro servicio de Windows. Ahora sólo nos falta instalarlo.

Paso 5. Generamos el ejecutable.

Generamos el ejecutable de nuestro servicio (Generar o Volver a generar). A partir de aquí no necesitamos más el IDE, de modo que podemos cerrarlo. A partir de ahora sólo necesitamos el .exe que se nos acaba de generar y un par de archivos .bat que vamos a crear a continuación (podríamos no generarlos y escribir las instrucciones directamente en línea de comandos, pero a mí me resulta más cómodo hacerlo en archivos batch). El archivo .exe (que encontramos en la carpeta bin/Release de nuestro proyecto ServicioAlertas) lo copiaremos en una ruta más corta (C:\). Y los dos archivos .bat que necesitamos son los siguientes:

Instalador.bat

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallUtil 
                                 "C:\ServicioAlertas.exe"
pause

Desinstalador.bat

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallUtil /U
                                 "C:\ServicioAlertas.exe"
pause

En ambos las dos primeras líneas deben ser una única línea continuada (el ancho del blog no lo permite) y la ruta de la aplicación InstallUtil puede variar ligeramente en función de la versión del Framework que estemos utilizando. Basta con verificarlo con el Explorador de Windows y modificarla según convenga.

Para lo que sirven está claro: uno instala el servicio y el otro lo desinstala.

Paso 6. Instalamos el servicio.

Simplemente ejecutamos el archivo Instalador.bat y se nos abrirá una ventana de línea de comando con el proceso de la instalación terminando exitosamente. Ahora nos vamos a Panel de control - Herramientas administrativas - Servicios y encontraremos nuestro servicio ServicioAlertas. Lo iniciamos y observaremos al cabo de pocos segundos que en la ruta C:\ se nos ha creado un archivo log.txt. Si dejamos el servicio un rato funcionando y después lo detenemos veremos que este archivo ha quedado más o menos así:

Alerta www.albertmata.net - 23/07/2008 21:12:18
Alerta www.albertmata.net - 23/07/2008 21:12:23
Alerta www.albertmata.net - 23/07/2008 21:12:28
Alerta www.albertmata.net - 23/07/2008 21:12:33
Alerta www.albertmata.net - 23/07/2008 21:12:38

Paso 7. Desinstalamos el servicio.

Si en algún momento queremos desinstalar el servicio (si es tan poco útil como ServicioAlertas seguro que querremos) solo debemos ejecutar Desinstalador.bat y el servicio quedará desinstalado (tendremos que refrescar la lista de servicios para ver que efectivamente así es).

Con esto queda visto el tema de los servicios de Windows. En internet se puede encontrar mucha otra información al respecto (p.ej. ésta en CodeGuru) en la que nos alertan de no utilizar MsgBox y similares en un servicio porque suelen dar problemillas. ;-)

Calendario en JavaScript traído del baúl de los recuerdos.

Ordenando estos días viejos archivos que tenía en discos duros varios, he topado con algunas cosas que había desarrollado hace ya tiempo y que en algunos casos ni recordaba. Una de las que me ha parecido más interesante es este calendario para página web creado con JavaScript que presento hoy. Recuerdo que lo creé para recoger una fecha con la que acotar una consulta. Quería evitar problemas con los formatos de entrada de fechas (día/mes/año, mes/día/año, etc) y además me apetecía crear algo más estético que un simple cuadro de texto. Por eso surgió este calendario. De modo que todo el código es de hace unos cuantos años, cuando era un programador 100% amateur, así que no se aceptan críticas despiadadas sobre su corrección… ;-) Ahora simplemente le he echado un vistazo por encima y le he puesto algún comentario y alguna tabulación, nada más.

No tiene mucho misterio explicar cómo funciona, solo hay que crear el calendario en el punto de la página web que se desee con una llamada así:

<script>drawCalendar('date01');</script>

Y después tener algún cuadro de texto que recoja la fecha que se selecciona en el calendario. Este cuadro lo podemos hacer visible o invisible a nuestro antojo, pero tenemos que tener la precaución de asignarle un identificador igual al parámetro que hemos pasado como argumento a la función drawCalendar:

<input type="textbox" id="date01" value=" "/>

Con esto el calendario queda ya plenamente operativo. Aquí va un ejemplo:

Y aquí os dejo el código JavaScript completo del archivo calendar.js:

/*-------------------------------------------------------------------
 Author:      Albert Mata (www.albertmata.net)
 Date:        20080718
 Description: Old JavaScript calendar I programmed a long time ago. 
-------------------------------------------------------------------*/

/*-------------------------------------------------------------------
 Attributes.
-------------------------------------------------------------------*/

var monthShown;
var yearShown;
var textboxId;
var firstMonthWeekDay;

/*-------------------------------------------------------------------
 Draws calendar giving identifiers to each button.
-------------------------------------------------------------------*/
function drawCalendar(txtId) {
    var currentDate;

    textboxId = txtId;

    /* Getting current month and year. */
    currentDate = new Date();
    monthShown = currentDate.getMonth();
    yearShown = currentDate.getFullYear();

    /* Opening HTML table. */
    document.write('<table width="126" bgcolor="#FFFFFF"' 
                 + 'align="center" cellspacing="0" cellpadding="0"'
                 + 'border="0">');
    document.write('<tr><td width="6"><img src="left.gif"' 
                 + 'style="cursor: hand;" onClick="SafeRefill(1);"/>'
                 + '</td><td width="60"><input type="button"' 
                 + 'style="width: 60px; background-color: #FFFFFF;'
                 + 'font-size: 10px; text-align: center;' 
                 + 'font-family: Arial, Helvetica, sans-serif;' 
                 + 'border: 0px;" id="mth"' 
                 + 'value="'+monthName(monthShown)+'"/></td>'
                 + '<td width="6"><img src="right.gif"' 
                 + 'style="cursor: hand;" onClick="SafeRefill(2);"/>'
                 + '</td><td width="10"></td><td width="6">'
                 + '<img src="left.gif" style="cursor: hand;"' 
                 + 'onClick="SafeRefill(3);"/></td>'
                 + '<td width="32"><input type="button"' 
                 + 'style="width: 32px; background-color: #FFFFFF;'
                 + 'font-size: 10px; text-align: center;' 
                 + 'font-family: Arial, Helvetica, sans-serif;' 
                 + 'border: 0px;" id="yea" value="'+yearShown+'"/>'
                 + '</td><td width="6"><img src="right.gif"' 
                 + 'style="cursor: hand;" onClick="SafeRefill(4);"/>'
                 + '</td></tr>');
    document.write('<tr><td width="126" colspan="7">');
   
    /* Adding buttons. */
    for (i = 0; i < 42; i++) {
        document.write('<input type="button" style="width: 18px;' 
                     + 'background-color: #FFFFFF; font-size: 10px;'
                     + 'text-align: center;' 
                     + 'font-family: Arial, Helvetica, sans-serif;'
                     + 'border: 0px;" id="day' + i + '" value=" "'
                     + 'onMouseOver="Hover('+i+',1);"' 
                     + 'onMouseOut="Hover('+i+',0);"' 
                     + 'onClick="ShowDate(' + i + ');"/>');
        if (((i+1) % 7) == 0) {
            document.write('<br/>');
        }
    }

    /* Closing HTML table. */
    document.write('</td></tr>');
    document.write('</table>');

    Refill();
}

/*-------------------------------------------------------------------
 Prevents possible wrong values for month and refills calendar.
-------------------------------------------------------------------*/
function SafeRefill(action) {
    /* Increasing or decreasing month and year. */
    if (action == 1) { monthShown-- }
    if (action == 2) { monthShown++ }
    if (action == 3) { yearShown-- }
    if (action == 4) { yearShown++ }
   
    /* Moving from January to December. */
    if (monthShown == -1) {
        monthShown = 11;
        yearShown--;
    }
   
    /* Moving from December to January. */
    if (monthShown == 12) {
        monthShown = 0;
        yearShown++;
    }
   
    Refill();
}

/*-------------------------------------------------------------------
 Redraws calendar when month or year has changed, using identifiers
 previously given to all buttons.
-------------------------------------------------------------------*/
function Refill() {
    var firstMonthDay;
    var lastMonthDay;
    var lastMonthWeekDay;

    /* Getting first month day and weekday. */
    firstMonthDay = new Date(yearShown,monthShown,1);
    firstMonthWeekDay = firstMonthDay.getDay();
    if (firstMonthWeekDay == 0) {
        firstMonthWeekDay = 7;
    } 

    /* Getting last month day and weekday. */
    lastMonthDay = new Date(yearShown,monthShown,
                            monthDays(monthShown,yearShown));
    lastMonthWeekDay = lastMonthDay.getDay();
    if (lastMonthWeekDay == 0) {
        lastMonthWeekDay = 7;
    } 

    /* Clearing all buttons. */
    for (i = 0; i < 42; i++) {
        document.getElementById('day'+i).value = " ";
        document.getElementById('day'+i).style.cursor = 'default';
    }

    /* Giving new values to buttons. */
    for (i = 1; i <= monthDays(monthShown,yearShown); i++) {
        document.getElementById('day'+(i+firstMonthWeekDay-2))
                                              .value = i;
        document.getElementById('day'+(i+firstMonthWeekDay-2))
                                              .style.cursor = 'hand';
    }

    /* Giving new values to month and year buttons. */
    document.getElementById('mth').value = monthName(monthShown);
    document.getElementById('yea').value = yearShown;
}

/*-------------------------------------------------------------------
 Function to calculate a month's number of days depending on it's a 
 leap year or not. It's a leap year when it can be divided by 4, but
 it's not when it can be divided by 100 as well. And it's a leap
 year again when it can be divided by 100 and by 400.
-------------------------------------------------------------------*/
function monthDays (mm, yyyy) {
    var februaryDays;
    var daysNumber;

    februaryDays = 28;

    /* Deciding number of days for February. */
    if ((yyyy % 4) == 0) {
        if ((yyyy % 100) == 0) {
            if ((yyyy % 400) == 0) { 
                februaryDays = 29;
            } 
        } else { 
            februaryDays = 29;
        }
    } 

    daysNumber = new Array (31, februaryDays, 31, 30, 31, 30, 
                            31, 31, 30, 31, 30, 31);

    /* Returning number of days for selected month and year. */
    return daysNumber[mm];
}

/*-------------------------------------------------------------------
 Returns month name.
-------------------------------------------------------------------*/
function monthName (mm) {
   var monthNames;

   monthNames = new Array('enero','febrero','marzo','abril',
                          'mayo','junio','julio','agosto',
                          'septiembre','octubre','noviembre',
                          'diciembre');

   return monthNames[mm];
}

/*-------------------------------------------------------------------
 Customizes mouseOver effects.
-------------------------------------------------------------------*/
function Hover(dd,x) {
    if (document.getElementById('day'+dd).value != " ") {
        if (x == 1) {
            document.getElementById('day'+dd).style.background = 
                                                       '#CCCCFF' 
        } else { 
            document.getElementById('day'+dd).style.background = 
                                                       '#FFFFFF' 
        }
    }
}

/*-------------------------------------------------------------------
 Returns selected data to textbox.
-------------------------------------------------------------------*/
function ShowDate(dd) {
    var selDay;
    var selMonth;

    /* Formatting day. */
    if ((dd - firstMonthWeekDay + 2) < 10) {
        selDay = '0' + (dd - firstMonthWeekDay + 2);
    } else {
        selDay = (dd - firstMonthWeekDay + 2);
    }

    /* Formatting month. */
    if ((1+monthShown) < 10) {
        selMonth = '0' + (1 + monthShown);
    } else {
        selMonth = (1 + monthShown);
    }

    /* Returning date to textbox. */
    if (document.getElementById('day'+dd).value != ' ' ) { 
        /* Customize this line to change output format!!! */
        document.getElementById(textboxId).value = selDay + '/' 
                                                 + selMonth + '/' 
                                                 + yearShown; 
    }
}

Para que funcione bonito es preciso que en el mismo directorio donde esté ubicado el archivo calendar.js se encuentren también las imágenes left.gif y right.gif que se pueden descargar desde esta misma página haciendo click derecho en las propias imágenes (los triangulitos azules que aparecen en el calendario para desplazarse entre meses y/o años).

Por último, ni que decir tiene que este calendario es muy mejorable y ampliable en funciones… ¡pero no seré yo quien lo haga! Simplemente lo dejo aquí por si a alguien puede servirle o puede aportarle alguna idea. ;-)

Breve momento de reflexión en .NET.

Cuando programamos una aplicación con POO es habitual que para una determinada entidad nos encontremos trabajando los mismos datos en tres niveles: el objeto en sí mismo, una tabla de la base de datos y un formulario. El objeto tendrá una serie de propiedades o atributos. En la tabla de la base de datos guardaremos estas propiedades para poder recuperarlas en otro momento. En el formulario le mostraremos la información del objeto al usuario o bien recogeremos la información que el usuario introduzca para crear el objeto.

Así pues podemos tener una secuencia tal que así:
formulario - objeto - tabla

Hoy voy a centrarme en hablar de la primera parte de la secuencia: formulario - objeto. Imaginemos un caso típico en el que a través de un formulario creamos o mostramos un objeto (p.ej. todos los datos de cabecera de una factura). Para cada propiedad del objeto tendremos en el formulario un control (p.ej. un textbox). Dispondremos además de algún botoncito con el que volcaremos la información del formulario en el objeto para informarle todas sus propiedades. O sea, estaremos utilizando códigos parecidos a…

OBJ.Propiedad1 = Me.txtPropiedad1.Text
OBJ.Propiedad2 = Me.txtPropiedad2.Text
OBJ.Propiedad3 = Me.txtPropiedad3.Text

…para informar el objeto a partir del formulario, y parecidos a…

Me.txtPropiedad1.Text = OBJ.Propiedad1
Me.txtPropiedad2.Text = OBJ.Propiedad2
Me.txtPropiedad3.Text = OBJ.Propiedad3

…para rellenar el formulario a partir del objeto.

Esto puede convertirse en indeseable si el objeto no tiene sólo 3 propiedades sino 50. Es por ello que me decidí a investigar cómo podría realizarse esto de manera automática y terminé topando con… la reflexión.

Según Calling Dr.Marteens, “la reflexión es uno de los pilares de .NET. Esta característica permite almacenar y obtener información en tiempo de ejecución sobre casi cualquier objeto o tipo presente en un módulo. Es gracias a esto que es posible implementar técnicas fundamentales como la recolección de basura o la serialización en distintos formatos. Y aunque es cierto que la mayoría de los entornos de programación modernos proporcionan algún tipo de RTTI (runtime time information, el pariente pobre de la reflexión), nunca antes se había visto un uso tan extenso y generalizado de este recurso como en .NET.”

Es decir, en mi caso la reflexión me permite acceder a las propiedades de un objeto y trabajar con ellas de manera optimizada. Veamos cómo. Para mi ejemplo he creado una clase clsPerson que recogerá los objetos con los que trabajaremos y que tiene el siguiente código:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080710
' Description: Class to show how to work with Reflection. 
'--------------------------------------------------------------------
Public Class clsPerson

    '----------------------------------------------------------------
    ' Attributes.
    '----------------------------------------------------------------
    Private _Name As String = ""
    Private _Age As Integer = 0
    Private _Married As Boolean = False

    '----------------------------------------------------------------
    ' Constructor method.
    '----------------------------------------------------------------
    Public Sub New()
        'Void constructor.
    End Sub

    '----------------------------------------------------------------
    ' Constructor method.
    '----------------------------------------------------------------
    Public Sub New(ByVal Name As String, ByVal Age As Integer, _
    ByVal Married As Boolean)
        Me.Name = Name
        Me.Age = Age
        Me.Married = Married
    End Sub

    '----------------------------------------------------------------
    ' Property for Age attribute.
    '----------------------------------------------------------------
    Public Property Age() As Integer
        Get
            Return Me._Age
        End Get
        Set(ByVal value As Integer)
            Me._Age = value
        End Set
    End Property

    '----------------------------------------------------------------
    ' Property for Name attribute.
    '----------------------------------------------------------------
    Public Property Name() As String
        Get
            Return Me._Name
        End Get
        Set(ByVal value As String)
            Me._Name = value
        End Set
    End Property

    '----------------------------------------------------------------
    ' Property for Married attribute.
    '----------------------------------------------------------------
    Public Property Married() As Boolean
        Get
            Return Me._Married
        End Get
        Set(ByVal value As Boolean)
            Me._Married = value
        End Set
    End Property

End Class

Es fundamental que existan las propiedades establecidas como tal. Aunque normalmente se obtiene un resultado parecido poniendo los atributos como públicos en lugar de privados y no creando las propiedades, en este caso es exigible hacerlo así. De todos modos lo deseable sería hacerlo así en todos los casos, ya que lo anterior es una muy mala práctica… pero me reconozco el primero en tirar de ella en ocasiones, sobretodo en fase de desarrollo.

A continuación he creado un formulario en donde hay tres controles para recoger las tres propiedades del objeto clsPerson: txtAge, txtName y chkMarried. También incorpora dos botones: cmdC2O para crear un objeto con los datos del formulario (Container To Object) y cmdO2C para llenar el formulario con los datos de un objeto (Object To Container). El código del formulario es pues el siguiente:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080710
' Description: Form to demonstrate how to work with Reflection. 
'--------------------------------------------------------------------
Public Class frmMain

    '----------------------------------------------------------------
    ' Loads object properties with form controls information.
    '----------------------------------------------------------------
    Private Sub cmdC2O_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles cmdC2O.Click
        Dim Person As New clsPerson()
        Dim OC As New clsObjectContainer()
        OC.ContainerToObject(Person, Me, "123")
        MsgBox("Valor de Name para el objeto: " & Person.Name)
        MsgBox("Valor de Age para el objeto: " & Person.Age)
        MsgBox("Valor de Married para el objeto: " & Person.Married)
    End Sub

    '----------------------------------------------------------------
    ' Fills form controls with object properties.
    '----------------------------------------------------------------
    Private Sub cmdO2C_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles cmdO2C.Click
        Dim Person As New clsPerson("Albert", 29, True)
        Dim OC As New clsObjectContainer()
        OC.ObjectToContainer(Person, Me, "123")
    End Sub

End Class

Y finalmente nos queda la clase más importante y que aparece ya utilizada en el código de los botones del formulario: la clase clsObjectContainer. Una clase que utilizando la reflexión nos permitirá pasar de formularios a objetos y viceversa con relativa facilidad. El código de esta clase es el siguiente:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080710
' Description: Class to show how to work with Reflection. 
'--------------------------------------------------------------------
Imports System.Reflection

Public Class clsObjectContainer

    '----------------------------------------------------------------
    ' Loads object with information from container. Only container's 
    ' controls where tag is equal to KEY will be considered.
    '----------------------------------------------------------------
    Public Sub ContainerToObject(ByVal OBJ As Object, _
    ByVal CNT As Object, ByVal KEY As String)
        Dim C As Control
        Dim T As Type = OBJ.GetType
        Dim P As PropertyInfo
        Dim index As Object() = Nothing

        'Iterating all controls in container.
        For Each C In CNT.Controls
            'Taking just controls with Tag = KEY.
            If C.Tag = KEY Then
                'Getting property with same name than control.
                P = T.GetProperty(Mid(C.Name, 4))
                'Deciding what to do according to control type.
                Select Case Mid(C.Name, 1, 3)
                    Case "txt"
                        'Idem according to property type.
                        Select Case P.PropertyType.ToString
                            Case GetType(Integer).ToString
                                P.SetValue(OBJ, CInt(C.Text), index)
                            Case GetType(Long).ToString
                                P.SetValue(OBJ, CLng(C.Text), index)
                            Case GetType(String).ToString
                                P.SetValue(OBJ, C.Text, index)
                        End Select
                    Case "chk"
                        'Casting control to CheckBox.
                        Dim CHK As CheckBox
                        CHK = CType(C, CheckBox)
                        P.SetValue(OBJ, CHK.Checked, index)
                End Select
            End If
        Next
    End Sub

    '-------------------------------------