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





