So, I created a class to avoid all the work behind the validation of input in a textbox.
The idea is to pass an existing textbox and a type of the desired content and the class will take care of block unacceptable input (for example letters in a numeric textbox), validate the text while the user writes and display a message if the content is invalid.
Exposed Methods
- Create: associates the existing textbox to the class, set the content type, and set appearance properties
- Validate: checks validity of content and display the message
Exposed properties
- TextBoxType: Let|Get custom - content type
- MaxValue: Let|Get double - only valid for numeric types
- MinValue: Let|Get double - only valid for numeric types
- FixedFormat: Let|Get boolean - only valid for numeric types, maintain the format of the number while typing
- ToCase: Let|Get custom - only valid for non-numeric types, change the case of the string while typing
- InvalidValueMessage: Let|Get string - message showed by the Validate function if the content is not vald
- IsValid: Get boolean - content validity by the type expected
- ShowValidityThrough: Let|Get custom - IsValid property can colour the textbox to indicate to the user if the content is valid or not. You can choose to colour backcolor, forecolor or bordercolor
- ValidColor: Let|Get long - the color of the ShowValidityThrough property if the content is valid
- InvalidColor: Let|Get long - the colour of the ShowValidityThrough property if the content is not valid
I would like to have some advice if you can on the design and on the possible errors you can see. Also advises on other possible types are very welcome! Thank you!
Class Name AdvTextBox
Option Explicit
Private WithEvents txt                      As MSForms.TextBox
' properties storage
Private pTextBoxType                        As TextBoxTypes
Private pMaxValue                           As Double
Private pMinValue                           As Double
Private pFixedFormat                        As Boolean
Private pToCase                             As DesiredCase
Private pInvalidValueMessage                As String
Private pIsValid                            As Boolean
Private pShowValidityThrough                As ValidityProperty
Private pValidColor                         As Long
Private pInvalidColor                       As Long
' calculated
Private pAllowedCharacters                  As String
Private pEvaluateMinMax                     As Boolean
Private pAllowEvents                        As Boolean
Private pOutputFormat                       As String
Private pEnlarged                           As Boolean
Private DecimalSeparator                    As String
' constants
Private Const numbers                       As String = "0123456789"
Private Const letters                       As String = "abcdefghijklmnopqrstuvwxyz"
Private Const accented                      As String = "èéàòì"
Private Const numberPunctuation             As String = ",."
Private Const otherPunctuation              As String = " !?=_/|-@€+"
Private Const defaultInvalidColor           As Long = &H5F5BDD
Public Enum TextBoxTypes
    ShortText = 0
    Notes = 1
    Iban = 10
    ItalianVatNumber = 11
    Email = 12
    WholeNumber = 20
    Decimal1Digit = 21
    Decimal2Digit = 22
    Decimal3Digit = 23
    Decimal4Digit = 24
    Decimal5Digit = 25
    Decimal6Digit = 26
End Enum
Public Enum DesiredCase
    Normal = 0
    UpperCase = 1
    LowerCase = 2
    ProperCase = 3
End Enum
Public Enum ValidityProperty
    NoOne = 0
    vBorders = 1
    vBackColor = 2
    vForeColor = 3
End Enum
' class
Private Sub Class_Initialize()
    DecimalSeparator = Application.DecimalSeparator
    pAllowEvents = True
    pFixedFormat = True
    pShowValidityThrough = NoOne
    pToCase = Normal
    pValidColor = -1
    pInvalidColor = -1
End Sub
' let properties
Public Property Let InvalidValueMessage(value As String)
    pInvalidValueMessage = value
End Property
Public Property Let ShowValidityThrough(value As ValidityProperty)
    pShowValidityThrough = value
    ColorTextBox pIsValid
End Property
Public Property Let ValidColor(value As Long)
    pValidColor = value
    ColorTextBox pIsValid
End Property
Public Property Let InvalidColor(value As Long)
    pInvalidColor = value
    ColorTextBox pIsValid
End Property
Public Property Let ToCase(value As DesiredCase)
    pToCase = value
End Property
Public Property Let FixedFormat(value As Boolean)
    pFixedFormat = value
    Select Case pTextBoxType
        Case WholeNumber
            pOutputFormat = "#,##0"
            pAllowedCharacters = numbers
        Case Decimal1Digit
            pOutputFormat = "#,##0.0"
            pAllowedCharacters = numbers & IIf(value, vbNullString, numberPunctuation)
        Case Decimal2Digit
            pOutputFormat = "#,##0.00"
            pAllowedCharacters = numbers & IIf(value, vbNullString, numberPunctuation)
        Case Decimal3Digit
            pOutputFormat = "#,##0.000"
            pAllowedCharacters = numbers & IIf(value, vbNullString, numberPunctuation)
        Case Decimal4Digit
            pOutputFormat = "#,##0.0000"
            pAllowedCharacters = numbers & IIf(value, vbNullString, numberPunctuation)
        Case Decimal5Digit
            pOutputFormat = "#,##0.00000"
            pAllowedCharacters = numbers & IIf(value, vbNullString, numberPunctuation)
        Case Decimal6Digit
            pOutputFormat = "#,##0.000000"
            pAllowedCharacters = numbers & IIf(value, vbNullString, numberPunctuation)
    End Select
End Property
Private Property Let IsValid(value As Boolean)
    pIsValid = value
    ColorTextBox value
End Property
Public Property Let MinValue(value As Double)
    pEvaluateMinMax = True
    pMinValue = value
End Property
Public Property Let MaxValue(value As Double)
    pEvaluateMinMax = True
    pMaxValue = value
End Property
Private Property Let TextBoxType(value As TextBoxTypes)
        
    Dim text        As String
    Dim maxLength   As Long
    
    pTextBoxType = value
    
    Select Case value
        Case ShortText
            maxLength = 40
            pAllowedCharacters = numbers & letters & numberPunctuation & otherPunctuation
        Case Notes
            txt.EnterKeyBehavior = True
            txt.MultiLine = True
            pAllowedCharacters = numbers & letters & numberPunctuation & otherPunctuation & accented & Chr(10) & Chr(13)
        Case Iban
            maxLength = 31
            pAllowedCharacters = numbers & letters
        Case ItalianVatNumber
            maxLength = 11
            pAllowedCharacters = numbers
        Case Email
            pAllowedCharacters = numbers & letters & numberPunctuation & otherPunctuation
        Case WholeNumber
            text = 0
            pOutputFormat = "#,##0"
            pAllowedCharacters = numbers
            txt.ControlTipText = "Press ""-"" to change the sign"
        Case Decimal1Digit
            text = 0
            pOutputFormat = "#,##0.0"
            pAllowedCharacters = numbers & IIf(pFixedFormat, vbNullString, numberPunctuation)
            txt.ControlTipText = "Press ""-"" to change the sign"
        Case Decimal2Digit
            text = 0
            pOutputFormat = "#,##0.00"
            pAllowedCharacters = numbers & IIf(pFixedFormat, vbNullString, numberPunctuation)
            txt.ControlTipText = "Press ""-"" to change the sign"
        Case Decimal3Digit
            text = 0
            pOutputFormat = "#,##0.000"
            pAllowedCharacters = numbers & IIf(pFixedFormat, vbNullString, numberPunctuation)
            txt.ControlTipText = "Press ""-"" to change the sign"
        Case Decimal4Digit
            text = 0
            pOutputFormat = "#,##0.0000"
            pAllowedCharacters = numbers & IIf(pFixedFormat, vbNullString, numberPunctuation)
            txt.ControlTipText = "Press ""-"" to change the sign"
        Case Decimal5Digit
            text = 0
            pOutputFormat = "#,##0.00000"
            pAllowedCharacters = numbers & IIf(pFixedFormat, vbNullString, numberPunctuation)
            txt.ControlTipText = "Press ""-"" to change the sign"
        Case Decimal6Digit
            text = 0
            pOutputFormat = "#,##0.000000"
            pAllowedCharacters = numbers & IIf(pFixedFormat, vbNullString, numberPunctuation)
            txt.ControlTipText = "Press ""-"" to change the sign"
    End Select
    
    If maxLength > 0 Then txt.maxLength = maxLength
    txt.text = text
    
End Property
    
' get properties
Public Property Get InvalidValueMessage() As String
    InvalidValueMessage = pInvalidValueMessage
End Property
Public Property Get ShowValidityThrough() As ValidityProperty
    ShowValidityThrough = pShowValidityThrough
End Property
Public Property Get ToCase() As DesiredCase
    ToCase = pToCase
End Property
Public Property Get FixedFormat() As Boolean
    FixedFormat = pFixedFormat
End Property
    
Public Property Get MaxValue() As Double
    MaxValue = pMaxValue
End Property
Public Property Get MinValue() As Double
    MinValue = pMinValue
End Property
Public Property Get IsValid() As Boolean
    ColorTextBox pIsValid
    IsValid = pIsValid
End Property
Public Property Get ValidColor() As Long
    ValidColor = pValidColor
End Property
Public Property Get InvalidColor() As Long
    InvalidColor = pInvalidColor
End Property
Private Property Get TextBoxType() As TextBoxTypes
    TextBoxType = pTextBoxType
End Property
    
' exposed methods and functions
Public Function Create(ByVal obj As MSForms.TextBox, _
                    ByVal txtType As TextBoxTypes) As AdvTextBox
    
    If pValidColor = -1 Then
        Select Case pShowValidityThrough
            Case NoOne, vBackColor
                pValidColor = obj.BackColor
            Case vBorders
                pValidColor = obj.BorderColor
            Case vForeColor
                pValidColor = obj.ForeColor
        End Select
    End If
    If pInvalidColor = -1 Then
        pInvalidColor = defaultInvalidColor
    End If
    
    Set txt = obj
    TextBoxType = txtType
    
    Set Create = Me
    
End Function
Public Function Validate() As Boolean
    
    ColorTextBox pIsValid
    If (Not pIsValid) And (Not pInvalidValueMessage = vbNullString) Then MsgBox pInvalidValueMessage, vbInformation, "Invalid value"
    Validate = pIsValid
    
End Function
' textbox events
Private Sub txt_Change()
    
    If Not pAllowEvents Then Exit Sub
    pAllowEvents = False
    
    Dim valore          As Variant
    
    valore = txt.text
    
    Select Case pTextBoxType
        Case ShortText
            If Not pToCase = Normal Then valore = StrConv(valore, pToCase)
        Case Notes
            If Not pToCase = Normal Then valore = StrConv(valore, pToCase)
        Case Iban
            IsValid = isValidIBAN(valore)
            valore = UCase(valore)
        Case ItalianVatNumber
            IsValid = IsValidItalianVatNumber(valore)
        Case Email
            IsValid = IsValidEmail(valore)
            valore = LCase(valore)
        Case Else
            Dim selectText  As Boolean
            If pFixedFormat Then
                valore = Replace(Replace(valore, ",", vbNullString), ".", vbNullString)
                If valore = vbNullString Then valore = 0
                valore = CDbl(valore)
                Select Case pTextBoxType
                    Case Decimal1Digit
                        valore = valore / 10
                    Case Decimal2Digit
                        valore = valore / 100
                    Case Decimal3Digit
                        valore = valore / 1000
                    Case Decimal4Digit
                        valore = valore / 10000
                    Case Decimal5Digit
                        valore = valore / 100000
                    Case Decimal6Digit
                        valore = valore / 1000000
                End Select
            Else
                valore = Replace(valore, IIf(DecimalSeparator = ",", ".", ","), IIf(DecimalSeparator = ",", ",", "."))
                If Not IsNumeric(valore) Then
                    valore = 0
                    selectText = True
                End If
            End If
            If pEvaluateMinMax Then
                IsValid = (Not valore < pMinValue) And (Not valore > pMaxValue)
            End If
            If pFixedFormat Then valore = Format(valore, pOutputFormat)
    End Select
    
    txt.text = valore
    If selectText Then
        txt.SelStart = 0
        txt.SelLength = Len(CStr(valore))
    End If
    
    pAllowEvents = True
    
End Sub
Private Sub txt_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
    If KeyAscii = 45 Then
        Select Case pTextBoxType
            Case WholeNumber, Decimal1Digit, Decimal2Digit, Decimal3Digit, Decimal4Digit, Decimal5Digit, Decimal6Digit
                txt.text = CDbl(txt.text) * -1
        End Select
    End If
    If Not KeyAscii = 8 Then
        If InStr(1, pAllowedCharacters, Chr(KeyAscii), vbTextCompare) = 0 Then KeyAscii = 0
    End If
End Sub
' validation routines
Private Sub ColorTextBox(validity As Boolean)
    If (Not pShowValidityThrough = NoOne) And (Not txt Is Nothing) Then
        Select Case pShowValidityThrough
            Case vBackColor
                txt.BackColor = IIf(validity, pValidColor, pInvalidColor)
            Case vBorders
                txt.BorderStyle = fmBorderStyleSingle
                txt.BorderColor = IIf(validity, pValidColor, pInvalidColor)
                txt.Width = txt.Width + IIf(pEnlarged, -0.1, 0.1)
                pEnlarged = Not pEnlarged
            Case vForeColor
                txt.ForeColor = IIf(validity, pValidColor, pInvalidColor)
        End Select
    End If
End Sub
Private Function IsValidItalianVatNumber(ByVal str As String) As Boolean
    
    IsValidItalianVatNumber = False
    
    If Not IsNumeric(str) Then Exit Function
    If Not Len(str) = 11 Then Exit Function
    
    Dim X               As Long
    Dim Y               As Long
    Dim z               As Long
    Dim t               As Long
    Dim i               As Long
    Dim c               As Long
    Dim ch              As Variant
    Dim pari            As Boolean
    
    pari = True
    
    For i = 1 To Len(str) - 1
        pari = Not pari
        ch = CLng(Mid(str, i, 1))
        If pari Then
            Y = Y + (ch * 2)
            If ch > 4 Then z = z + 1
        Else
            X = X + ch
        End If
    Next i
    
    t = (X + Y + z) Mod 10
    c = (10 - t) Mod 10
    
    IsValidItalianVatNumber = (c = CLng(Right(str, 1)))
    
End Function
Private Function isValidIBAN(ByVal Iban As String) As Boolean
   
    ' Written by Davide Tonin
    ' Documentation at https://davidetonin.com/code-snippets/how-to-validate-an-iban-with-vba
   
    isValidIBAN = False
   
    Dim LengthByCountry As Long
    Dim ReorderedIBAN   As String
    Dim NumericIBAN     As String
    Dim ch              As String
    Dim i               As Long
    Const Div           As Integer = 97
    Const SepaCountries As String = "AT20,BE16,BG22,CY28,HR21,DK18,EE20,FI18,FR27,DE22,GI23,GR27,GL18,IE22,IS26,FO18,IT27,LV21,LI21,LT20,LU20,MT31,MC27,NO15,NL18,PL28,PT25,GB22,CZ24,SK24,RO24,SM27,SI19,ES24,SE24,CH21,HU28"
   
    If Iban = vbNullString Then Exit Function
   
    'Check if the first 2 characters are letters
    If IsNumeric(Left(Iban, 1)) Or IsNumeric(Mid(Iban, 2, 1)) Then Exit Function
   
    'Get the expected legth by country
    LengthByCountry = InStr(1, SepaCountries, Left(Iban, 2), vbTextCompare)
    If LengthByCountry > 0 Then LengthByCountry = CInt(Mid(SepaCountries, LengthByCountry + 2, 2))
   
    If Len(Iban) <> LengthByCountry Then Exit Function
   
    'Move first 4 characters to right
    ReorderedIBAN = Right(Iban, Len(Iban) - 4) & Left(Iban, 4)
   
    'Loop through every single character in ReorderedIBAN and, if not numeric, return 10 based number from letter using string to store the returned value in place of number
    For i = 1 To Len(ReorderedIBAN)
        ch = Mid(ReorderedIBAN, i, 1)
        If Not IsNumeric(ch) Then
            NumericIBAN = NumericIBAN & CStr(Asc(UCase(ch)) - 55)
        Else
            NumericIBAN = NumericIBAN & CStr(ch)
        End If
    Next i
   
    ch = vbNullString
   
    'Perform primary school style division, digit by digit. I don't need to store the result, only the remainder
    For i = 1 To Len(NumericIBAN)
        ch = ch & Mid(NumericIBAN, i, 1)
        'If is the last character in NumericIBAN I check if remainder is 1 - Only fired once
        If i = Len(NumericIBAN) Then
            isValidIBAN = ((CLng(ch) Mod Div) = 1)
            Exit Function
        End If
        ch = IIf(CLng(ch) < Div, ch, CLng(ch) Mod Div)
    Next i
   
End Function
Private Function IsValidEmail(ByVal emailAddress As String) As Boolean
    
    IsValidEmail = False
    
    Const emailPattern          As String = "^([a-zA-Z0-9_\-\.]+)@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$"
    
    With CreateObject("VBScript.RegExp")
        .Global = True
        .IgnoreCase = True
        .Pattern = emailPattern
        IsValidEmail = .Test(emailAddress)
    End With
    
End Function
