Archivo de la Categoría '.NET'

TextBox mejorado en .NET.

Cuando desarrollamos en .NET es habitual utilizar controles personalizados que mejoran los preexistentes. Habitualmente podremos hacer uso de librerías públicas que podemos obtener en internet (liberadas o de pago). Otras veces podremos crearnos nosotros mismos nuestras propias librerías de controles. Yo suelo hacer esto último cuando no se trata de algo demasiado complejo, ya que termino antes que buscando componentes de terceros y peleándome después con sus correspondientes licencias. Entre otras cosas porque cualquier tipo de licencia “share alike” no me sirve.

Uno de los controles que me ahorran mucho trabajo en la creación de formularios es este que presento hoy y que en su día llamé TextBoxFocused (en adelante TBF). Se trata de un TextBox que cambia de color al recibir y perder el foco (de ahí su nombre -por cierto, sé que debería ser FocusedTextBox, pero quería mantener lo de TextBox al inicio-) y permite controlar a priori y a posteriori qué se puede introducir en él.

Un formulario con algunos TBF se vería así:

Como se puede apreciar, sin tener que añadir código alguno al formulario, el TBF aparecerá sombreado cuando tenga el foco. Además cuando se esté introduciendo un texto se mostrará en rojo, y cuando ya esté validado quedará en verde, tal como se aprecia en las siguientes imágenes:

Nada realmente espectacular, pero queda bonito. Sin embargo en donde el TBF se muestra útil es en el control del contenido introducido. Este control se realiza de una doble manera: a priori y mediante una propiedad del control se establece qué tipo de introducciones se admitirán, a posteriori -si se ha seleccionado- se controla que lo que se ha introducido realmente fuera lo que se debía introducir, impidiendo abandonar el TBF en caso contrario.

Para ello, en la ventana de propiedades del control nos aparecen dos nuevas creadas para la ocasión, tal como se muestra a continuación:

En las propiedades de cosecha propia siempre añado el prefijo “am” para localizarlas todas juntas. En este caso amAllow permite fijar qué introducciones se permitirán (cualquier cosa, nada de nada, sólo enteros positivos, dobles negativos, etc.) y amPostVerification decide si se aplicará verificación posterior o no.

Esto de amPostVerification tiene un sentido claro. Y es que podemos decirle al control que admita números dobles negativos y así sólo permitirá introducir números, el signo negativo y el signo decimal. Pero es evidente que con esos caracteres se pueden crear expresiones que no se correspondan con un número doble negativo (p.ej. -30-3.8..-25). Con este control a posteriori verificamos que efectivamente lo sea.

A nivel de código el control a posteriori se podría haber implementado a base de cástings y control de errores, pero no ha sido la opción que he escogido. Queda al antojo de cada cual modificar el código como le plazca.

Para su implementación en una aplicación, basta con añadir una clase TextBoxFocused.vb, copiar todo el código que se adjunta a continuación, compilar la aplicación y a partir de entonces tendremos el control disponible para añadirlo en cualquier formulario. O también se puede añadir a una librería de controles personalizados (que es lo que hago yo) y mantenerlo un poquito más organizado.

Código completo (de libre uso como todo lo que aparece publicado en este blog):

'--------------------------------------------------------------------
' Author:             Albert Mata (www.albertmata.net)
' Last time modified: 2008-11-13
' Description:        Acts like a standard TextBox but with some 
'                     better features like changing color when gets 
'                     or losts focus and controlling allowed 
'                     introductions.
'--------------------------------------------------------------------
Imports System.Drawing

Public Class TextBoxFocused
    Inherits System.Windows.Forms.TextBox

#Region "Constants"

    '----------------------------------------------------------------
    ' Constants for colors.
    '----------------------------------------------------------------
    Private COLOR_FOCUSED As Color = Color.FromArgb(255, 240, 157)
    Private COLOR_NON_FOCUSED As Color = Color.White
    Private COLOR_VALIDATED As Color = Color.Green
    Private COLOR_NON_VALIDATED As Color = Color.Red

    '----------------------------------------------------------------
    ' Constants for special keys.
    '----------------------------------------------------------------
    Private ASC_BACKSPACE As Integer = 8
    Private ASC_SUPPRESS As Integer = 127
    Private ASC_DASH As Integer = 45
    Private ASC_COMMA As Integer = 44

#End Region

#Region "Enumerations"

    '----------------------------------------------------------------
    ' Enumerations.
    '----------------------------------------------------------------
    Public Enum Introduction
        NothingAtAll
        PositiveInteger
        NegativeInteger
        PositiveDouble
        NegativeDouble
        OnlyLetter
        OnlyLetterOrDigit
        Password
        Everything
        EverythingNonCasing
    End Enum

#End Region

#Region "Attributes&Properties"

    '----------------------------------------------------------------
    ' Attributes.
    '----------------------------------------------------------------
    Private aAllow As Introduction = Introduction.Everything
    Private aPostVerification As Boolean = True

    '----------------------------------------------------------------
    ' Public properties to be shown in design time.
    '----------------------------------------------------------------
    Public Property amAllow() As Introduction
        Get
            Return Me.aAllow
        End Get
        Set(ByVal value As Introduction)
            Me.aAllow = value
        End Set
    End Property
    Public Property amPostVerification() As Boolean
        Get
            Return Me.aPostVerification
        End Get
        Set(ByVal value As Boolean)
            Me.aPostVerification = value
        End Set
    End Property

#End Region

#Region "GraphicalControls"

    '----------------------------------------------------------------
    ' Event Me.GotFocus.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_GotFocus(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.GotFocus
        MyBase.BackColor = COLOR_FOCUSED
        MyBase.ForeColor = COLOR_NON_VALIDATED
    End Sub

    '----------------------------------------------------------------
    ' Event Me.LostFocus.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_LostFocus(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.LostFocus
        If Me.CheckText() Or Not Me.aPostVerification Then
            MyBase.BackColor = COLOR_NON_FOCUSED
            MyBase.ForeColor = COLOR_VALIDATED
        Else
            Me.Focus()
        End If
    End Sub

    '----------------------------------------------------------------
    ' Event Me.KeyPress.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_KeyPress(ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyPressEventArgs) _
    Handles Me.KeyPress
        Select Case Me.aAllow
            Case Introduction.NothingAtAll
                'Allowing no character.
                e.Handled = True
                If AscW(e.KeyChar) = ASC_BACKSPACE Then
                    MyBase.Text = ""
                End If
            Case Introduction.PositiveInteger
                'Allowing just digits.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                End If
            Case Introduction.NegativeInteger
                'Allowing just digits and '-' character.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_DASH Then
                    e.Handled = True
                End If
            Case Introduction.PositiveDouble
                'Allowing just digits and ',' character.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_COMMA Then
                    e.Handled = True
                End If
            Case Introduction.NegativeDouble
                'Allowing just digits and '-' and ',' characters.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_DASH _
                And AscW(e.KeyChar) <> ASC_COMMA Then
                    e.Handled = True
                End If
            Case Introduction.OnlyLetter
                'Allowing only letters.
                If Not Char.IsLetter(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                Else
                    e.KeyChar = Char.ToUpper(e.KeyChar)
                End If
            Case Introduction.OnlyLetterOrDigit
                'Allowing only letters and digits and upper casing 
                'letters.
                If Not Char.IsLetterOrDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                Else
                    e.KeyChar = Char.ToUpper(e.KeyChar)
                End If
            Case Introduction.Password
                'Allowing only letters and digits but not upper 
                'casing letters.
                If Not Char.IsLetterOrDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                End If
            Case Introduction.Everything
                'Allowing everything upper casing letters.
                e.KeyChar = Char.ToUpper(e.KeyChar)
            Case Introduction.EverythingNonCasing
                'Allowing everything not upper casing letters (so 
                'doing nothing).
        End Select
    End Sub

#End Region

#Region "PrivateMethods"

    '----------------------------------------------------------------
    ' Checks introduced text according to Me.amAllow property and 
    ' returns True only when introduced text respects desired 
    ' Me.amAllow property.
    '----------------------------------------------------------------
    Private Function CheckText() As Boolean
        Select Case Me.aAllow
            Case Introduction.NothingAtAll
                'Checking no character has been introduced.
                Return Me.Text = ""
            Case Introduction.PositiveInteger
                'Checking a right positive integer has been 
                'introduced.
                Return Me.IsPositiveInteger(Me.Text) Or Me.Text = ""
            Case Introduction.NegativeInteger
                'Checking a right negative integer has been 
                'introduced.
                Return Me.IsNegativeInteger(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.PositiveDouble
                'Checking a right positive double has been 
                'introduced.
                Return Me.IsPositiveDouble(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.NegativeDouble
                'Checking a right negative double has been 
                'introduced.
                Return Me.IsNegativeDouble(Me.Text) _
                    Or Me.IsPositiveDouble(Me.Text) _
                    Or Me.IsNegativeInteger(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.OnlyLetter
                'Checking only letters have been introduced.
                Return Me.HasOnlyLetters(Me.Text) Or Me.Text = ""
            Case Introduction.OnlyLetterOrDigit, _
                 Introduction.Password
                'Allowing only letters and digits have been 
                'introduced.
                Return Me.HasOnlyLettersOrDigits(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.Everything, _
                 Introduction.EverythingNonCasing
                'Allowing everything.
                Return True
        End Select
    End Function

    '----------------------------------------------------------------
    ' Checks a right positive integer has been introduced.
    '----------------------------------------------------------------
    Private Function IsPositiveInteger(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

    '----------------------------------------------------------------
    ' Checks a right negative integer has been introduced.
    '----------------------------------------------------------------
    Private Function IsNegativeInteger(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_DASH)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - first character is a dash character,
        '  - length must be at least 2 characters.
        Return R And S.LastIndexOf(ChrW(ASC_DASH)) = 0 _
                 And S.Length >= 2
    End Function

    '----------------------------------------------------------------
    ' Checks a right positive double has been introduced.
    '----------------------------------------------------------------
    Private Function IsPositiveDouble(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_COMMA)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - there's one comma character and it's not the first one,
        '  - there's exactly only one comma character,
        '  - the comma character it's not the last one.
        Return R And S.IndexOf(ChrW(ASC_COMMA)) > 0 _
                 And S.IndexOf(ChrW(ASC_COMMA)) = _
                     S.LastIndexOf(ChrW(ASC_COMMA)) _
                 And S.LastIndexOf(ChrW(ASC_COMMA)) < S.Length - 1
    End Function

    '----------------------------------------------------------------
    ' Checks a right negative double has been introduced.
    '----------------------------------------------------------------
    Private Function IsNegativeDouble(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_DASH) _
                                    OrElse C = ChrW(ASC_COMMA)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - first character is a dash character,
        '  - length must be at least 4 characters,
        '  - there's one comma character and it's not the first or 
        '    second one (-0,0),
        '  - there's exactly only one comma character,
        '  - the comma character it's not the last one.
        Return R And S.LastIndexOf(ChrW(ASC_DASH)) = 0 _
                 And S.Length >= 4 _
                 And S.IndexOf(ChrW(ASC_COMMA)) > 1 _
                 And S.IndexOf(ChrW(ASC_COMMA)) = _
                     S.LastIndexOf(ChrW(ASC_COMMA)) _
                 And S.LastIndexOf(ChrW(ASC_COMMA)) < S.Length - 1
    End Function

    '----------------------------------------------------------------
    ' Checks only letters have been introduced.
    '----------------------------------------------------------------
    Private Function HasOnlyLetters(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsLetter(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

    '----------------------------------------------------------------
    ' Checks only letters and digits have been introduced.
    '----------------------------------------------------------------
    Private Function HasOnlyLettersOrDigits(ByVal S As String) _
    As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsLetterOrDigit(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

#End Region

End Class

Decidir clase en tiempo de ejecución en .NET.

En ocasiones puede resultarnos imprescindible no determinar la clase de la que un objeto va a ser instancia hasta que estemos en tiempo de ejecución. Para ello podemos valernos de la clase Activator y de su método CreateInstance, que nos permiten pasarle una clase para que nos devuelva un objeto de esa clase.

Podemos por ejemplo crearnos una función sencilla como esta…

'----------------------------------------------------------------
' Creates an instance of specified class and returns that object.
'----------------------------------------------------------------
Public Function CreateInstance(ByVal T As Type) As Object
    Return Activator.CreateInstance(T)
End Function

…que podríamos tener encapsulada como método de alguna clase que nos permitiera hacer determinadas acciones con objetos, esto ya a gusto de cada cual.

Una vez tenemos esta función podemos llamarla de varias maneras. La primera, si tenemos otro objeto de la misma clase de la que ahora queremos obtener una instancia:

Dim OBJ As Object

'Option 1. We already have an object of that class.
Dim C1 As New Class1()
OBJ = Me.CreateInstance(C1.GetType())

La segunda, si no tenemos ningún objeto pero conocemos el nombre de la clase en cuestión:

Dim OBJ As Object

'Option 2. We haven't any object of that class.
OBJ = Me.CreateInstance(Type.GetType("AlbertMata.Class2"))

'But be careful because this would fail:
'OBJ = Me.CreateInstance(Type.GetType("Class2"))

Como aparece en el código, conviene remarcar que el nombre de la clase debe incorporar el espacio de nombres, de lo contrario provocará una excepción.

En ambos casos, si una vez creado el objeto -que si nos fijamos lo habíamos declarado como Object y lo habíamos instanciado a través de un método que también devolvía un Object- consultamos su tipo exacto…

'Checking exact type for object.
Debug.Print(OBJ.GetType.ToString)

…obtendremos…

AlbertMata.Class2

Particularmente este sistema me ha venido bien para alguna travesura que quería hacer con formularios, pero creo que en más ocasiones podrá serme útil.

Distancia de Levenshtein en VisualBasic.NET.

Hace un rato se me ha planteado un problema que quien más quien menos habrá tenido en alguna ocasión. Necesitaba poder comparar dos cadenas y obtener algún indicador de hasta qué punto podría tratarse de la misma cadena escrita de maneras ligeramente diferentes. He estado dándole vueltas sobre cómo podría jugar con los distintos métodos de la clase String para conseguir algún valor. Me he planteado utilizar análisis de frecuencias, generar números ponderados en función de carácter y posición dentro de la cadena… Pero nada me convencía, así que he insistido un poco más en buscar si ya alguien había desarrollado alguna función parecida y para mi mayúscula sorpresa me he dado de bruces con la Distancia de Levenshtein.

Resulta que esto que andaba buscando es exactamente esta distancia. Y tal como reza la Wikipedia… “se llama Distancia de Levenshtein o distancia de edición al número mínimo de operaciones requeridas para transformar una cadena de caracteres en otra. Se entiende por operación, bien una inserción, eliminación o la substitución de un carácter. Esta distancia recibe ese nombre en honor al científico ruso Vladimir Levenshtein, quien se ocupara de esta distancia en 1965. Es útil en programas que determinan cuán similares son dos cadenas de caracteres, como es el caso de los correctores de ortografía.”

Y para mayor regocijo, en la propia página de la Wikipedia aparece el algoritmo en pseudocódigo de la función:

int LevenshteinDistance(char str1[1..lenStr1], char str2[1..lenStr2])
   // d is a table with lenStr1+1 rows and lenStr2+1 columns
   declare int d[0..lenStr1, 0..lenStr2]
   // i and j are used to iterate over str1 and str2
   declare int i, j, cost
 
   for i from 0 to lenStr1
       d[i, 0] := i
   for j from 0 to lenStr2
       d[0, j] := j
 
   for i from 1 to lenStr1
       for j from 1 to lenStr2
           if str1[i] = str2[j] then cost := 0
                                else cost := 1
           d[i, j] := minimum(
                                d[i-1, j] + 1,     // deletion
                                d[i, j-1] + 1,     // insertion
                                d[i-1, j-1] + cost   // substitution
                            )
 
   return d[lenStr1, lenStr2]

Incluso viene la implementación en algunos lenguajes de programación (C++, Java, Perl, Python, Ruby, Delphi y ColdFusion). Lástima que no viniera también en VisualBasic.NET que es el lenguaje en el que yo desarrollo… pero bueno, como codificar un algoritmo que ya nos viene dado en pseudocódigo tampoco es tarea infernal, a ello me he dedicado:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20081212
' Description: Calculates Levenshtein distance between two different 
'              strings.
'--------------------------------------------------------------------
Public Function LevenshteinDistance(ByVal STR1 As String, _
ByVal STR2 As String) As Integer
    'Variables to iterate (i, j) and get cost for any needed 
    'operation.
    Dim i As Integer
    Dim j As Integer
    Dim Cost As Integer

    'Creating array with string's lengths as bounds.
    Dim D(STR1.Length, STR2.Length) As Integer

    'Initializing array's values.
    For i = 0 To STR1.Length
        D(i, 0) = i
    Next
    For j = 0 To STR2.Length
        D(0, j) = j
    Next

    'Calculating Levenshtein distance.
    For i = 1 To STR1.Length
        For j = 1 To STR2.Length
            If STR1(i - 1) = STR2(j - 1) Then
                Cost = 0
            Else
                Cost = 1
            End If
            'First compared element: deletion.
            'Second compared element: insertion.
            'Third compared element: substitution.
            D(i, j) = Math.Min(Math.Min(D(i - 1, j) + 1, _
                                        D(i, j - 1) + 1), _
                                        D(i - 1, j - 1) + Cost)
        Next
    Next

    'Returning result.
    Return D(STR1.Length, STR2.Length)
End Function

Creo que está bien. Y el resultado de algunas pruebas así parece demostrarlo:

Debug.Print(Me.LevenshteinDistance("Albert Mata", "Albert Mata"))
0
Debug.Print(Me.LevenshteinDistance("Albert Mata", "Alber Matas"))
2
Debug.Print(Me.LevenshteinDistance("Albert Mata", "Mr. Albert Mata"))
4

No es ni mucho menos un sistema infalible, pero nos puede resultar muy útil para estimar si dos cadenas algo distintas pueden ser (o pretender ser) en realidad la misma. No obstante hay que utilizarla con cabeza, ya que por ejemplo si comparamos…

Albert Mata
Antoni Pons

…obtendremos una distancia de 9. Y si comparamos…

Berberechos y enlatados del norte, S.A.
Berberechos y enlatd. norte, SA

…obtendremos también una distancia de 9. Sin embargo en el primer caso las personas son claramente distintas y en el segundo caso el cliente parece ser el mismo. Para corregir esto se podrían buscar varias alternativas, pero yo propongo algo tan sencillo como ponderar la distancia entre la longitud de alguna de las cadenas:

Dim S1 As String = "Albert Mata"
Dim S2 As String = "Antoni Pons"
Dim LD As Integer = Me.LevenshteinDistance(S1, S2)
Dim Similar As Double = (S1.Length - LD) / S1.Length

Esto nos da un coeficiente de similitud del 0,181818181818182 (escaso, escaso). En cambio esto otro…

Dim S1 As String = "Berberechos y enlatados del norte, S.A."
Dim S2 As String = "Berberechos y enlatd. norte, SA"
Dim LD As Integer = Me.LevenshteinDistance(S1, S2)
Dim Similar As Double = (S1.Length - LD) / S1.Length

…nos da un coeficiente de similitud del 0,769230769230769 (¡mucho mejor!). Se tratará en cada caso de establecer dónde ponemos el límite.

Report in .NET using Crystal Reports and MySQL database.

This is just the first of some English posts that I’ll publish by translating most popular posts in this blog. Original version (in Spanish) is here.

First of all, I must asume that creating reports is one of that things I like worst in programming. But it’s quite obvious that few serious applications don’t need them, and the one I’m developing now isn’t an exception to this rule. So I’ve been creating some reports recently and I’ve discovered a new way to do it. And that’s what I explain in this post.

As I’ve said sometimes before in previous posts, I develop with VisualBasic.NET and MySQL database. And I use Crystal Reports to create reports, as this tool is integrated in VisualStudio.NET. Since now, I used to use an ODBC connection configured in each PC to connect to MySQL server. But I didn’t like this system much, because actually I’m not working just with one database but with some with different names. They have the same structure, tables and data, but just one is the good one, as the rest are just for developing purposes. It’s really easy to use one or other connection string to make the application connect with one or other database, but with reports it isn’t so easy as they take data using that ODBC connection (and it just can connect with one database).

But now I’ve discovered how to create reports with just a DataTable and an XML schema, needing nothing else. Actually it’s possible to use a DataSet instead of a DataTable as well. So I’m going to explain it with an easy example and some images.

I’ll work with two tables in my MySQL database where I’ll keep information about bills. First table is the one with information about headers and has this data:

+---------+------------+-------------------------+
| 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 |
+---------+------------+-------------------------+

Second table is the one with information about positions and has this rows:

+---------+---------+------------------------+---------+---------+
| 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 |
+---------+---------+------------------------+---------+---------+

It’s something really simple and not normalized, but will be enough for this example, as we’re going to create a report that will be the inovice for purchase number 4 (the one with customer SOFTWARE ALBERTMATA.NET). Obviously, we’ll need information about both tables but I just want to work with one DataTable, so first of all I’m going to create a MySQL view with this sentence:

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
);

So, from now on the report will be created using this zbl_bill2print view. Let’s go with the .NET part.

Step 1. Creating XML file containing table/view structure.

Along this post we’ll work with these three things:

1) a Windows form (frmMain) where we’ll have the report viewer object.
2) a class (clsReportCreator) we’re going to create right now.
3) a report (rptBill) that will be the invoice we want to print.

So let’s start creating clsReportCreator class. It’ll have only one attribute (the name of the table or view), one constructor method, one method to load DataTable object and one last method to generate the XML file. Here is the full code for this class:

'--------------------------------------------------------------------
' 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

And we also create frmMain form, which only code by the moment will be this:

'--------------------------------------------------------------------
' 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

Right now we have a first application. If we execute it we’ll get C:\zbl_bill2print.xml file with the structure of zbl_bill2print view. So we run it and get that file.

Step 2. Creating report and loading data source.

First, we add a report to our project and give it a name like rptBill.rpt. We create it choosing empty report option, so desestimating any templates.

Now we go to Fields explorer menu and right-click the first option (Database fields). In new contextual menu we click on Database assistant option.

After this we get the Available data source menu, where we choose Create new connection and after that ADO.NET option.

Making this, we’ll see a new form where we’ll be asked about File’s path. In this point we have to find XML file we’ve created before (in my example C:\zbl_bill2print.xml) and then press Finish. We have NewDataSet option including our just added zbl_bill2print in Available data source menu now.

So we select it and press button to move it to Selected tables menu. Done this, it’s time to click on Accept.

With all this stuff we’ve gotten that zbl_bill2print structure available in Fields explorer menu with all its fields, as shown in image below:

Step 3. Designing report.

Nothing special to say here. Just adding fields from Fields explorer menu, inserting text objects where needed, sums, text formats, images and so on…

I’ve just created a very simple design like this:

Step 4. Last actions to get the invoice.

Finally we’re going to create the bill. To do that, we add a CrystalReportViewer object in frmMain form. I call it crvBill. After that it’s necessary to modify frmMain source code to make it look like this:

'--------------------------------------------------------------------
' 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

It’s important to note that the line where the XML file is created is commented now, as we just need to create this file once to use it to create the source data, but from now on we don’t need to generate it every time.

What we’re mainly doing in this code is:

1) creating a report object same kind we’ve designed in step 3,
2) getting a DataTable with data we want to show (in this example and according to the way we’ve defined MySQL view, we want to show invoice number 4),
3) setting this DataTable as the report’s source data,
4) asking CrystalReportViewer to show this report.

We execute the application again and get desired invoice:

Of course there should be quite more information, images and legal texts in a real invoice, but this is just an easy example of how to do the report itself.

So we’ve seen how to create a report in VisualBasic.NET just using an XML file. Of course there are plenty of things to improve, as optimizing how database connection is done, or avoiding WHERE condition directly in MySQL view and so on… but what I was looking for with this example was just a very minimum guide to show the process.

PS. Some menu and option names can be different as I develope in VisualStudio Spanish version and I’ve just translated them as I’ve thought they could appear in English version. Sorry about that!

Update.

There is a second part for this post explaining how to pass parameters from form to report, but it’s still only in Spanish.

ReDim Preserve para cambiar más de una dimensión en .NET.

Recientemente en grupos de .NET salió el tema de cómo se pueden redimensionar las dos dimensiones de una matriz de dos dimensiones sin perder los valores que ya se tienen almacenados en dicha matriz.

Si fuera una matriz de una dimensión no habría ningún problema, ya que la opción Preserve nos permite hacer precisamente eso:

Dim myArray(3) As Int32
myArray(0) = 2
myArray(1) = 4
myArray(2) = 6
myArray(3) = 8
ReDim Preserve myArray(5)
myArray(4) = 10
myArray(5) = 12

Así, este código no da ningún problema. Y tampoco lo da este otro:

Dim myArray(3, 0) As Int32
myArray(0, 0) = 2
myArray(1, 0) = 4
myArray(2, 0) = 6
myArray(3, 0) = 8
ReDim Preserve myArray(3, 1)
myArray(0, 1) = 10
myArray(1, 1) = 12
myArray(2, 1) = 14
myArray(3, 1) = 1

Ya que aunque es una matriz de dos dimensiones, sólo estamos redimensionando la dimensión situada más a la derecha. En cambio si intentamos hacer esto que sigue:

Dim myArray(3, 0) As Int32
myArray(0, 0) = 2
ReDim Preserve myArray(4, 1)
myArray(4, 1) = 10

Nos dará una excepción de tipo ArrayTypeMismatchException y nos dirá que…

‘ReDim’ sólo puede cambiar la dimensión situada más a la derecha

…porque un ReDim Preserve en una matriz de dos dimensiones sólo puede actuar sobre la última dimensión.

Para solventar esto podemos utilizar la siguiente función:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20081118
' Description: Simulates a ReDim Preserve action on 2-dimensions
'              arrays, allowing to change not only the last dimension
'              but both.
'--------------------------------------------------------------------
Public Function ReDimPreserve(ByVal M As Array, _
ByVal NewLimit0 As Integer, ByVal NewLimit1 As Integer) As Array
    If NewLimit0 >= M.GetUpperBound(0) _
    And NewLimit1 >= M.GetUpperBound(1) Then
        Dim NewArray(NewLimit0, NewLimit1) As [Int32]
        For i As Integer = 0 To M.GetUpperBound(0)
            For j As Integer = 0 To M.GetUpperBound(1)
                NewArray.SetValue(M.GetValue(i, j), i, j)
            Next
        Next
        Return NewArray
    Else
        Return M
    End If
End Function

De tal manera que ahora ante el siguiente código, donde creamos una matriz inicialmente de dimensiones (2,3) para luego redimensionarla a (4,5) y por tanto cambiando las dos dimensiones de la matriz, sin perder los valores que ya teníamos almacenados…

Debug.Print("Creating array with dimensions [2,3]")
Dim myArray(2, 3) As [Int32]
Debug.Print("Storing value 12 in position [0,2]")
myArray.SetValue(12, 0, 2)
Debug.Print("Storing value 15 in position [2,3]")
myArray.SetValue(15, 2, 3)
Debug.Print("Upper bound for first dimension = " _
            & (myArray.GetUpperBound(0)))
Debug.Print("Upper bound for second dimension = " _
            & (myArray.GetUpperBound(1)))
Debug.Print("Value in position [0,2] = " _
            & myArray.GetValue(0, 2).ToString)
Debug.Print("Value in position [2,3] = " _
            & myArray.GetValue(2, 3).ToString)
Try
    myArray.SetValue(24, 3, 4)
    Debug.Print("I can store values in position [3,4]")
Catch ex As Exception
    Debug.Print("I can't store values in position [3,4]")
End Try

Debug.Print("Changing array dimensions to [4,5]")
myArray = DirectCast(Me.ReDimPreserve(myArray, 4, 5), Integer(,))
Debug.Print("Upper bound for first dimension = " _
            & (myArray.GetUpperBound(0)))
Debug.Print("Upper bound for second dimension = " _
            & (myArray.GetUpperBound(1)))
Debug.Print("Value in position [0,2] = " _
            & myArray.GetValue(0, 2).ToString)
Debug.Print("Value in position [2,3] = " _
            & myArray.GetValue(2, 3).ToString)
Try
    myArray.SetValue(24, 3, 4)
    Debug.Print("I can store values in position [3,4]")
Catch ex As Exception
    Debug.Print("I can't store values in position [3,4]")
End Try

…obtenemos esta salida en la Ventana Inmediato:

Creating array with dimensions [2,3]
Storing value 12 in position [0,2]
Storing value 15 in position [2,3]
Upper bound for first dimension = 2
Upper bound for second dimension = 3
Value in position [0,2] = 12
Value in position [2,3] = 15
*** 'System.IndexOutOfRangeException' EXCEPTION ***
I can't store values in position [3,4]
Changing array dimensions to [4,5]
Upper bound for first dimension = 4
Upper bound for second dimension = 5
Value in position [0,2] = 12
Value in position [2,3] = 15
I can store values in position [3,4]