Archivo de la Categoría 'Herramientas'

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

Formato no disponible en Snapshot de Access.

Hace un tiempo, cuando me dedicaba fundamentalmente a programar aplicaciones en Access, me encontré con un problema que irremediablemente todos mis clientes sufrían tarde o temprano. Es un problema que se refiere a las versiones XP y 2003 (ignoro si alguna más) y que ocurre cuando se quiere generar un informe en formato Snapshot. Este formato es muy útil para exportar informes en archivos .snp que se pueden visualizar desde prácticamente cualquier ordenador (y si no existe un visualizador gratuito de Microsoft).

Bien, el problema increíblemente reside en que al instalar Microsoft Access en español, la instalación crea una clave en el registro de Windows (regedit.exe) con la descripción en castellano:

snp,,1,Formato Snapshot (*.snp),0

Pero cuando el propio Access busca el valor para esa clave espera encontrarlo en inglés:

snp,,1,Snapshot Format (*.snp),0

Y al no encontrarlo muestra un mensaje diciendo que el formato en cuestión no está disponible. O sea, una chapuza monumental de los señores de Microsoft, sí.

Para solucionar esto simplemente hay que cambiar la entrada correspondiente en el registro de Windows para cambiar el primer valor por el segundo. Ojito, todos sabemos que puede ser crítico cambiar cosas en dicho registro, así que cada cual sabrá lo que hace (pero este cambio es bastante inofensivo, eso sí ;-) ). En cualquier caso la entrada a modificar es la siguiente para Access XP:

HKEY_LOCAL_MACHINE\_
     SOFTWARE\Microsoft\Office\10.0\Access\Report Formats

Y esta otra para Access 2003:

HKEY_LOCAL_MACHINE\_
     SOFTWARE\Microsoft\Office\11.0\Access\Report Formats

Y la entrada concreta a la que hay que cambiarle el valor es Snapshot Format.

A mis clientes solía enviarles un archivo .bat para facilitarles la modificación, que consistía simplemente en la instrucción:

REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\
Office\11.0\Access\Report Formats" /v "Snapshot Format" /d "snp,
,1,Snapshot Format(*.snp),0" /f

Todo junto en la misma línea y sin espacios en los saltos de línea, ¿ok? Ah, y en donde pone 11.0 cambiarlo por 10.0 cuando la versión de Access sea la XP.

Por último añadir que en mi caso el uso del formato Snapshot era como paso intermedio para la creación de archivos PDF directamente desde Access sin necesidad de tener instalada ninguna impresora PDF, utilizando para ello la magnífica herramienta de Lebans.

Actualización: Iván aporta amablemente en los comentarios de esta misma entrada que en su caso para que pasara a funcionarle tuvo que cambiar el valor en castellano por este otro:

Formats\snp,,1,Snapshot Format (*.snp),0

Es decir, añadiéndole el Formats\ delante. Así que si estáis intentando resolver este problema y con las indicaciones del post no se os resuelve, no dejéis de probar también esta alternativa.

Copias de seguridad en MySQL con mysqldump.

En los pocos días de vida que tiene este blog es posible que haya mencionado ya que en el proyecto en el que estoy trabajando actualmente estamos utilizando una base de datos MySQL. Sí, lo sé, unas cuantas veces llevo ya… pero es que realmente va muy bien, estamos muy contentos con sus prestaciones hasta la fecha. Se está mostrando rápida y muy fiable… y todo ello con una licencia GPL, no lo olvidemos.

Bueno, el caso es que como es evidente, cuando trabajamos con cualquier sistema tenemos que tener muy bien previsto un método decente de copias de seguridad. En el caso de MySQL tenemos varias alternativas, pero la que a mí más me ha convencido ha sido la herramienta mysqldump. Es realmente sencilla de utilizar y tremendamente funcional. Su funcionamiento exacto con las decenas de opciones que admite se puede encontrar en el propio manual de referencia de MySQL. No requiere instalación alguna, simple y llanamente que dispongamos del archivo mysqldump.exe que conseguiremos sin problemas en la página oficial de MySQL y que si hemos instalado el servidor MySQL probablemente tendremos ya en nuestro equipo.

La herramienta mysqldump nos genera archivos con extensión .sql que incluyen -según hayamos configurado- las instrucciones necesarias para restaurar una base de datos entera -o todas las que tengamos en el servidor MySQL-, desde su creación hasta la adición de los datos pasando por la creación de las tablas. Realmente completito. Para una restauración sólo habría que hacer algo así en una línea de comandos (localhost o servidor según corresponda):

mysql -u root -p -h localhost < C:\archivo_backup.sql

No obstante lo que quiero explicar hoy es cómo he automatizado estas copias de seguridad que hago con mysqldump a través de un archivo batch. Expongo código y después lo comento.

@echo off

set path_mysqldump="C:\Program Files\MySQL\MySQL Server 5.0\bin"
set path_backups="\\99.24.13.29\BBDD\Backups"
set user=root
set password=mi_password
set host=99.24.13.26

if %time:~0,2% GEQ 10 goto :DespuesDeLas10

:AntesDeLas10
%path_mysqldump%\mysqldump --user=%user% --password=%password% 
      -h %host% --databases db_offers --single-transaction 
      > %path_backups%\backup_%date:~6,4%%date:~3,2%%date:~0,2%
        _0%time:~1,1%%time:~3,2%.sql
goto :Salir

:DespuesDeLas10
%path_mysqldump%\mysqldump --user=%user% --password=%password%
      -h %host% --databases db_offers --single-transaction 
      > %path_backups%\backup_%date:~6,4%%date:~3,2%%date:~0,2%
        _%time:~0,2%%time:~3,2%.sql
:Salir

Antes que nada, comentar que las líneas que comienzan por %path… aparecen aquí cortadas por temas de espacio (en píxeles, no en bytes ;-) ) en el blog, pero en realidad deben formar una sola línea que termina con .sql. Debe haber un espacio entre %password% y -h y después otro entre transaction y > %path_, pero en cambio la última linea va sin espacio (%date:~0,2%_0%time va todo seguido, ¿ok?).

Paso a explicarlo. Las primeras cinco líneas después del archiconocido @echo off son asignaciones de valores a las variables que después utilizaremos. Es la parte de configuración del archivo batch.

1) path_mysqldump contiene la ruta del directorio donde se encuentra el archivo mysqldump.exe.

2) path_backups contiene la ruta del directorio donde se almacenará la copia de seguridad, en este ejemplo he puesto una carpeta en un servidor.

3) user es el usuario con el que se ejecutará mysqldump. Parece una perogrullada, pero hay que tener en cuenta que debe disponer de permisos suficientes, aunque no es necesario que sea el usuario root (de hecho mucho mejor si no lo es, ya que así no quedará en un archivo batch visualizable el password del usuario root).

4) password es… sí, eso.

5) host es la máquina donde se encuentra el servidor MySQL que queremos backupear. Si es nuestro propio ordenador escribiremos localhost. A destacar que el servidor MySQL debe estar corriendo en el momento de lanzar el mysqldump.

Lo que sigue es ya la ejecución. En mi caso las copias de seguridad las quiero en formato backup_YYYYMMDD_HHMM con la fecha y la hora de esta manera, ya que así una simple ordenación alfabética de los archivos lleva a cabo también una ordenación por fechas. Para lograr eso hay que jugar con los date:~6,4 y similares y además hay que programar dos ramas en función de que sean antes o después de las 10 de la mañana, ya que hasta entonces necesito concatenar un 0 y un time:~1,1 para la hora con dos dígitos y a partir de las 10 me vale con time:~0,2. Podemos prescindir de hacer estas dos alternativas si vamos a lanzar siempre el batch a la misma hora y será más tardía que esas 10am de la madrugada. En cualquier caso para discernir entre si seguir un camino u otro utilizamos el…

if %time:~0,2% GEQ 10 goto :DespuesDeLas10

…donde GEQ equivale a lo que en lenguaje de programación normal y corriente diríamos >=.

Por último comentar que con esto obtenemos un archivo .bat que podemos ejecutar manualmente o programar para que se ejecute periódicamente a nuestro antojo. Por ejemplo con una simple tarea programada de Windows si no nos queremos complicar demasiado la vida.

Sobrevivir a los cáràctërês extraños.

Pues aquí está lo que comentaba en el post anterior: una pequeña y simple aplicación que sirve para traducir masivamente caracteres a sus correspondientes entidades seguras. Digo pequeña y simple porque realmente lo es: no está especialmente optimizada ni tiene muchas opciones que digamos. Apenas un textbox para introducir el texto a… mmmh “codificar”, y un botón para hacerlo. Nada más. Se me ocurre que se podría mejorar haciendo que sólo cambie un espacio por &#160; cuando haya más de uno consecutivo con el fin de no inundar de &#160; un texto normal convirtiéndolo así en ilegible. También se podría ofrecer al usuario de la aplicación la opción de parametrizar qué entidades quiere reemplazar y cuáles no, ya que yo por ejemplo sí que reemplazo < y > porque en realidad no utilizo la aplicación para páginas web, sino para los fragmentos de código de este propio blog. Pero está claro que alguien que utilice una aplicación así para páginas web no querrá reemplazar esos dos caracteres ni tampoco la barra /.

En fin, sólo sugiero posibles modificaciones que le puede hacer al programita quien lo desee.

Aquí dejo el link para descargarse el ejecutable correspondiente. No requiere instalación alguna de mínimo que es el programita. Eso sí, como está desarrollado en .NET requerirá el correspondiente Framework. Mucha gente lo tiene ya en su ordenador sin siquiera saberlo, y si no se puede descargar gratuitamente desde la página de Microsoft. No es más que una especie de máquina virtual más o menos análoga a la MVJ de Java.

El programa está basado sólo en un formulario y una clase auxiliar. He aquí el código del formulario principal:

'----------------------------------------------------------
' Author: Albert Mata (www.albertmata.net)
' Date: 20080601
'----------------------------------------------------------

'----------------------------------------------------------
' Main class to replace all problematic characters in a
' text to make it XML-valid.
'----------------------------------------------------------
Public Class frmXML

    '------------------------------------------------------
    ' Attributes.
    '------------------------------------------------------
    Private arrEntities As New ArrayList() 'of clsEntity

    '------------------------------------------------------
    ' Loads all desired entities translations.
    '------------------------------------------------------
    Private Sub frmXML_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load
        Me.LoadEntities()
    End Sub

    '------------------------------------------------------
    ' Main method to make the replacements.
    '------------------------------------------------------
    Private Sub cmdMain_Click(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles cmdMain.Click
        Me.txtMain.Text = FullReplace(Me.txtMain.Text)
    End Sub

    '------------------------------------------------------
    ' Iterates all problematic entities and replaces them
    ' in the original text.
    '------------------------------------------------------
    Private Function FullReplace(ByVal FullString As String) _
    As String
        Dim Temp As String = FullString
        Dim IT As IEnumerator = arrEntities.GetEnumerator()
        While IT.MoveNext
            Dim Current As clsEntity = IT.Current
            Temp = Replace(Temp, Current.Bad, Current.Safe)
        End While
        Return Temp
    End Function

    '------------------------------------------------------
    ' Loads all entities to be replaced. Make here all the
    ' changes you like!
    '------------------------------------------------------
    Private Sub LoadEntities()
        'Avoiding "&#;" characters are replaced twice.
        arrEntities.Add(New clsEntity("&", "TEMP00001"))
        arrEntities.Add(New clsEntity("#", "TEMP00002"))
        arrEntities.Add(New clsEntity(";", "TEMP00003"))
        arrEntities.Add(New clsEntity("!", "&#33;"))
        arrEntities.Add(New clsEntity("""", "&#34;"))
        'HE ELIMINADO DECENAS DE ENTIDADES AQUÍ POR NO HACER
        'UN POST KILOMÉTRICO...
        'Avoiding "&#;" characters are replaced twice.
        arrEntities.Add(New clsEntity("TEMP00001", "&#38;"))
        arrEntities.Add(New clsEntity("TEMP00002", "&#35;"))
        arrEntities.Add(New clsEntity("TEMP00003", "&#59;"))
    End Sub

End Class

Y aquí la clase auxiliar que he utilizado:

'----------------------------------------------------------
' Author: Albert Mata (www.albertmata.net)
' Date: 20080601
'----------------------------------------------------------

'----------------------------------------------------------
' Auxiliar class representing an entity (its problematic
' character and the safe one).
'----------------------------------------------------------
Public Class clsEntity

    '------------------------------------------------------
    ' Attributes.
    '------------------------------------------------------
    Public Bad As String
    Public Safe As String

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

End Class

Con esto se puede reconstruir la aplicación sin problemas, pero de todos modos si alguien está interesado en que le envíe los archivos fuente no tiene más que pedirlo por correo electrónico (mi dirección aparece en la página Acerca de mí…) y con gusto se los remitiré.

Cáràctërês extraños en XML.

Todo aquel que en algún momento haya programado (o incluso simplemente diseñado) alguna página web se habrá encontrado con los típicos problemas al poner tildes y caracteres extraños que algunos navegadores interpretan bien y otros, oh sorpresa, no lo hacen. A mí al menos me ha pasado. Y ya no digamos si en lugar de un documento HTML (o ASP o PHP) se trata de uno XML. En ese caso ya no hay duda posible: un documento XML no nos aceptará sin quejarse un carácter acentuado ni fuera de lo común. Total, que a menudo nos vemos obligados a recurrir a lo que se conoce como entidades, o lo que es lo mismo, en lugar de utilizar los caracteres de la primera columna utilizar los de la segunda:

  <     &#60;
  @     &#64;
  á     &#225;
  ñ     &#241;

También puede resultar útil si queremos poner más de un espacio consecutivo, ya que por defecto los navegadores igualan esto:

Albert     Mata

A esto otro:

Albert Mata

Para evitarlo podemos sustituir el espacio por su propia entidad (&#160;) tantas veces como deseemos y entonces se nos respetarán los espacios (de hecho así es como lo he hecho en el ejemplo anterior).

Total, que de vez en cuando me encuentro una vez más buceando en internet en busca de una tabla de entidades para buscar cuál era el código de una en concreto. Así que para ya no tener que hacerlo nunca más me he creado una propia donde aparecen las más habituales. La enlazo a continuación. ¡Si buscando como tantas veces antes he buscado yo das con ella, tienes mi permiso para guardarte una copia! ;-)

Tabla de entidades en HTML y XML.

Esta tabla puede servir como recurso puntual, pero está claro que andar sustituyendo caracteres por códigos en un documento completo puede ser de lo más engorroso. Queda pues pendiente para próximo post una pequeña aplicación que nos lo haga de manera automática…

Simulando DLookup fuera de Access (III).

En los dos posts anteriores he mostrado maneras de simular un DLookup en una base de datos MySQL mediante stored procedures y stored functions (primera aproximación y versión mejorada). No obstante, ha quedado claro que hacerlo así aunque puede resultar útil tiene sus limitaciones. Es por ello que normalmente prefiero utilizar un DLookup llevado a cabo desde la parte de la aplicación. En mi caso esto equivale a realizarlo en VisualBasic.NET. Y sin más preámbulos veamos el código de la función:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080530
' Description: Function to simulate Microsoft Access DLookup. 
'--------------------------------------------------------------------
Public Function DLookup(ByVal Field As String, _
                        ByVal Table As String, _
               Optional ByVal Condition As String = "TRUE") _
                        As Object
    Try
        'Creating SQL string.
        Dim SQL As String
        SQL = "SELECT " & Field _
            & " FROM " & Table _
            & " WHERE " & Condition
        'Filling dataset with desired value. 
        Dim DS As New DataSet
        Dim DA As New MySqlDataAdapter(SQL, CONNECTION_STRING)
        DA.Fill(DS, "anyname")
        'Returning value or Null.
        If Not IsDBNull(DS.Tables("anyname").Rows(0).Item(0)) Then
            Return DS.Tables("anyname").Rows(0).Item(0)
        Else
            Dim X As Object = Convert.DBNull
            Return X
        End If
    Catch
        'If error happens, returning Null.
        Dim X As Object = Convert.DBNull
        Return X
    End Try
End Function

Cabe aclarar que CONNECTION_STRING será una constante definida en algún punto de la aplicación que recogerá la cadena de conexión para nuestra base de datos. En mi caso es algo como:

Database = nombre_bbdd; _
Data Source = localhost; _
User ID = root; _
Password = mi_password

Relacionado con cadenas de conexión, recomendado darse un paseo por ConnectionStrings.com.

Yo estoy trabajando con una base de datos MySQL, por eso el DataAdapter es en realidad un MySqlDataAdapter, pero el funcionamiento sería análogo con cualquier otra base de datos y utilizando un objeto DataAdapter convencional. Lo único es que si se trabaja con MySQL es necesario previamente haber agregado la referencia correspondiente al proyecto, y para mayor comodidad en temas de nomenclatura importar el espacio de nombres correspondiente en la parte superior de la clase:

Imports MySql.Data.MySqlClient

Respecto a la función, poco que explicar. Pasarle el campo y la tabla deseados y de manera opcional una condición en formato SQL sin el WHERE. Si no se desea condición se pueden informar sólo dos argumentos, en esta ocasión no es necesario enviar una cadena vacía ni nada por el estilo. Además, el valor que nos devuelve la función es de tipo Object, así que no tendremos ningún problema con el tipo de dato devuelto, nos admitirá cualquier tipo de campo que quiera que sea el campo en cuestión de la tabla en cuestión. En caso de producirse algún error (nombre de campo o tabla mal escritos, conexión a base de datos fallida…) o si no se halla ningún registro, la función nos devolverá un valor nulo, con lo cual desde código podremos recogerlo sin problemas y actuar en consecuencia. :-)

Simulando DLookup fuera de Access (II).

En el anterior post expuse una manera de realizar un DLookup en MySQL mediante stored procedures y stored functions y comenté que para el siguiente post me reservaba un modo de hacer lo mismo desde .NET. No obstante me permito intercalar otra manera de realizar otra vez el DLookup desde MySQL algo más sencilla que la primera aunque para recuperar el valor necesitaremos igualmente una doble consulta. Veamos cómo queda el código en esta ocasión:

#--------------------------------------------------------------------
# Author:      Albert Mata (www.albertmata.net)
# Date:        20080527
# Description: MySQL procedure to simulate Microsoft Access 
#              function DLookup. 
#--------------------------------------------------------------------

#--------------------------------------------------------------------
# Returns DLookup value using fourth parameter.
#--------------------------------------------------------------------
DROP PROCEDURE IF EXISTS DLookup;
DELIMITER //
CREATE PROCEDURE DLookup(IN campo VARCHAR(7),
                         IN tabla VARCHAR(14),
                         IN condicion VARCHAR(250),
                         OUT valor VARCHAR(250))
BEGIN

DROP TABLE IF EXISTS tbl_dlookupaux;
CREATE TABLE tbl_dlookupaux (tbl_res VARCHAR(250));
SET @query = CONCAT('INSERT INTO tbl_dlookupaux SELECT ',
                    campo, ' FROM ', tabla, ' WHERE ',
                    IF(ISNULL(condicion),'TRUE',condicion),
                    ' LIMIT 1');

PREPARE running FROM @query;
EXECUTE running;
DEALLOCATE PREPARE running;
SELECT tbl_res INTO valor FROM tbl_dlookupaux;
DROP TABLE IF EXISTS tbl_dlookupaux;

END
//
DELIMITER ;

Esta vez utilizo únicamente una stored procedure, pero lo hago tomando parámetros tanto de entrada (campo, tabla y condicion) como de salida (valor). Con esto lo que hago es almacenar el resultado en el parámetro valor en lugar de almacenarlo provisionalmente en una tabla para luego recuperarlo de allí (bueno, internamente la stored procedure sí almacena en una tabla temporal, pero en tanto que la misma stored procedure la borra este proceso es transparente al usuario).

Recuerdo que partíamos de una tabla cty_nicecities con esta estructura:

+--------+-----------+---------+
| cty_id | cty_nam   | cty_cou |
+--------+-----------+---------+
|      1 | Paris     | France  | 
|      2 | Rome      | Italy   | 
|      3 | Frankfurt | Germany | 
|      4 | Dortmund  | Germany | 
|      5 | Milan     | Italy   | 
+--------+-----------+---------+

La llamada debe ahora realizarse así:

CALL DLookup ('cty_nam', 'cty_nicecities', 'cty_id = 4', @my_city);
SELECT @my_city;

Almacenamos pues el valor en una variable (@my_city) y después lo rescatamos:

+----------+
| @my_city |
+----------+
| Dortmund |
+----------+

Tal vez este método es más limpio que el anterior. De todos modos para la próxima entrega, esta vez sí, postearé el DLookup mediante código en .NET, que será bastante más potente al incorporar gestión de errores y no tener limitaciones fruto de tener que especificar el tipo de datos concreto que esperamos obtener. No obstante un DLookup en MySQL se ejecutará íntegramente en el servidor, mientras que uno en .NET tendrá una parte ejecutándose en la máquina cliente, así que dependerá del gusto de cada cual escoger uno u otro. Yo me inclino por la versión en .NET…

Simulando DLookup fuera de Access (I).

Para todos aquellos que empezamos en bases de datos con Access y que programamos aplicaciones con VBA, las funciones DCount, DMax y similares nos resultan de lo más útiles para obtener valores puntuales de una base de datos sin tener que recuperar explícitamente un recordset. La más útil de todas probablemente sea DLookup. La sintaxis resumida de DLookup es la siguiente (para una más detallada explicación se puede consultar la propia ayuda de Access):

DLookup (nombre_campo, nombre_tabla, condicion_opcional)

Y nos devuelve el valor del campo correspondiente de la tabla indicada, en el primer registro que cumpla la condición opcionalmente pasada como parámetro.

Pues bien, como ya he comentado en posts anteriores, la base de datos con la que actualmente estoy trabajando es MySQL, y por desgracia esta función tan útil no existe de manera predeterminada en MySQL. No obstante, ello no quiere decir que no podamos utilizar algo similar, sí que podemos, aunque vamos a tener que programar un poco antes. He aquí el código:

#--------------------------------------------------------------------
# Author:      Albert Mata (www.albertmata.net)
# Date:        20080527
# Description: MySQL procedure and function to simulate Microsoft
#              Access function DLookup. 
#--------------------------------------------------------------------

#--------------------------------------------------------------------
# Creates an auxiliar table to store the value.
#--------------------------------------------------------------------
DROP PROCEDURE IF EXISTS PreDLookupAux;
DELIMITER //
CREATE PROCEDURE PreDLookup(campo VARCHAR(7), 
                            tabla VARCHAR(14), 
                            condicion VARCHAR(250))
BEGIN

DROP TABLE IF EXISTS tbl_dlookupaux;
CREATE TABLE tbl_dlookupaux (tbl_res VARCHAR(250));
SET @query = CONCAT('INSERT INTO tbl_dlookupaux SELECT ', 
                    campo, ' FROM ', tabla, ' WHERE ', 
                    IF(ISNULL(condicion),'TRUE',condicion), 
                    ' LIMIT 1');
PREPARE running FROM @que