How to add WinForms TextBox Features (Numbers, Currencies, IPv4 & a Maximum Character Limiter)

129 Views Asked by At

I need a better TextBox to suit my needs (several purposes). For instance:

  • Ability to filter characters as numbers;
  • Ability to work with currencies (add decimals and currency designators);
  • Ability to limit the number of characters allowed in the TextBox;
  • Automatically add decimal places;
  • More logical (missing) features useful for the Control.

I believe everyone (at some point) shares the same opinion: WinForms TextBox features are somehow limited, specially when using a TextBox in order to work with Numbers, Currencies or IP Addresses (IPv4 in this Situation).

Thanks in advance.

1

There are 1 best solutions below

0
On

Answering my own question while providing my humble code:

Info (Last Update: 2022.04.02)


About

  • This is the Default TextBox with Extra Features.

Remarks

  • I've been working on this control recently and I'm adding more features on the go. The control should be working fine, even though it may require some further development.
  • I'll try to keep it updated as soon as possible until perfection is achieved.
    See "Known Bugs" bellow.

Features:

  • Filter / Format / Validate the Text Input (Text, Numeric, Currency or IP Address).
  • Set Currency Designator Symbol.
  • Set Currency Designator as a Symbol or Abbreviated Designator Name (i.e: EUR).
  • Set the Currency Designator Symbol Location. i.e: Left: Before the Value. Right: After the Value.
  • Set Values as Decimal.
  • Set Decimal Zeros Automatically when Entering a Whole Number.
  • Limit Maximum Character Input

Bug Fixes

  • Prevented Clipboard Data to be Set to the Control.
  • Default TextBox Initial (Default) Value was Impossible to be Set (for Numeric and Currency Text Inputs).
  • Character Limiter Function was Missing.

Known Bugs

  • There is an issue with Text (number of chars) Limiter while using Decimals. TextBox Prevents user Text Input Proper Behaviour.
  • Sometimes not Accepting Paste. SHIFT + INSERT, on the other hand is allowed.

Code


using System;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

namespace RG_Custom_Controls.Controls
{
    public class RGTextBoxI : TextBox
    {
        #region <Constructor>
        public RGTextBoxI()
        {
            // -> Set Default Configuration.
            ForeColor = Color.Gainsboro;
            BackColor = Color.FromArgb(255, 36, 36, 52);
            BorderStyle = BorderStyle.FixedSingle;            
        }
        #endregion


        #region <Fields>
        private const int WM_PASTE = 0x0302;            // Used to Validate Clipboar Data.
        private string numbers = "0123456789.";
        private string allowedChars => numbers;
        private string decimalFormat = string.Empty;
        #endregion

        #region <Custom Properties> : (Char Limiter)
        private bool charsLimited = false;
        [Category("1. Custom Properties"), DisplayName("1. Chars Limited")]
        [Description("Toggle Character Input Limit.")]
        [Browsable(true)]
        public bool CharsLimited
        {
            get { return charsLimited; }
            set { charsLimited = value; }
        }

        private int maximumChars = 32;
        [Category("1. Custom Properties"), DisplayName("2. Maximum Chars")]
        [Description("Limit the Maximum Number of Chars Allowed.")]
        [Browsable(true)]
        public int MaximumChars
        {
            get { return maximumChars; }
            set { maximumChars = value; }
        }
        #endregion

        #region <Custom Properties> : (Input Mode)
        /// <summary> TextBox Text Iput Mode (Normal, Numeric, Currency). </summary>
        public enum TextBoxInputType { Default, Numeric, Currency, IPV4 }
        private TextBoxInputType inputType = TextBoxInputType.Default;
        [Category("1. Custom Properties"), DisplayName("1. Input Mode")]
        [Description("Select Control Mode (Normal, Numeric or Currency).")]
        [Bindable(true)] /* Required for Enum Types */
        [Browsable(true)]
        public TextBoxInputType TextBoxType
        {
            get { return inputType; }
            set
            {
                inputType = value;
                
                Text_SetDefaultValue();
                Text_Align();

                Invalidate();
            }
        }
        #endregion

        #region <Custom Properties> : (Decimals)
        private bool useDecimals;
        [Category("1. Custom Properties"), DisplayName("2. Use Decimals")]
        [Description("Select wether to use Whole Number or a Decimal Number.")]
        [Browsable(true)]
        public bool UseDecimals
        {
            get { return useDecimals; }
            set { useDecimals = value; }
        }

        private int decimalPlaces = 2;
        [Category("1. Custom Properties"), DisplayName("3. Decimal Places")]
        [Description("Select wether to use Whole Number or a Decimal Number.")]
        [Browsable(true)]
        public int DecimalPlaces
        {
            get { return decimalPlaces; }
            set
            {
                if (value > 0 & value < 3)
                {
                    decimalPlaces = value;

                    // Aet Decimal Format
                    switch (decimalPlaces)
                    {
                        case 1: decimalFormat = "0.0"; break;
                        case 2: decimalFormat = "0.00"; break;
                    }
                }
            }
        }
        #endregion       

        #region <Custom Properties> : (Curency Designator)
        private string currencyDesignator = "€";
        [Category("1. Custom Properties"), DisplayName("4. Currency Designator")]
        [Description("Set Currency Symbol or Designator.\n\n i.e: €, Eur, Euros")]
        [Browsable(true)]
        public string CurrencyDesignator
        {
            get { return currencyDesignator; }
            set { currencyDesignator = value; }
        }

        public enum DesignatorAlignment { Left, Right }
        private DesignatorAlignment designatorAlignment = DesignatorAlignment.Right;
        [Category("1. Custom Properties"), DisplayName("5. Designator Location")]
        [Description("Select Currency Designator Location")]
        [Bindable(true)] /* Required for Enum Types */
        [Browsable(true)]
        public DesignatorAlignment DesignatorLocation
        {
            get { return designatorAlignment; }
            set { designatorAlignment = value; }
        }
        #endregion

        private bool IsLimitingChars(int textLength)
        {
            bool val = false;

            if (charsLimited)
            {
                switch (inputType)
                {
                    case TextBoxInputType.Default: val = Text.Length.Equals(maximumChars); break;
                    case TextBoxInputType.Numeric:
                    case TextBoxInputType.Currency:

                        if (useDecimals)
                        {
                            // Note: '+1' Refers the '.' that Separates the Decimals
                            val = Text.Length.Equals(maximumChars + decimalPlaces + 1);
                        }

                        else { val = Text.Length.Equals(maximumChars); }
                        break;
                }
                // case TextBoxInputType.IPV4: break;
            }

            return val;
        }

        private void SetDecimalValue()
        {
            Text_RemoveWhiteSpaces();
            Text_SetDecimalValue();
            Text_AddCurrencyDesignator();
        }

        #region <Overriden Events>
        /// <summary> Occurs Before the Control Stops Being the Active Control. </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void OnValidating(CancelEventArgs e)
        {
            base.OnValidating(e);

            switch (inputType)
            {
                // ...

                case TextBoxInputType.IPV4:
                    // Validate the IPv4 Address
                    if (!HasValidIPAddress(Text)) { Text_SetDefaultValue(); }
                    break;
            }
        }

        /// <summary> Occurs when a Keyboard Key is Pressed. </summary>
        /// <param name="e"></param>
        protected override void OnKeyPress(KeyPressEventArgs e)
        {
            base.OnKeyPress(e);

            if (!e.KeyChar.Equals((char)Keys.Back))
            {
                // Limit Number of Characters
                switch (inputType)
                {
                    // case TextBoxInputType.Default: e.Handled = IsLimitingChars(Text.Length); break;
                    case TextBoxInputType.Numeric:
                    case TextBoxInputType.Currency:
                        e.Handled = !HasValidNumericChar(e.KeyChar) ^ IsLimitingChars(Text.Length);
                        if (e.KeyChar.Equals('.') & NrCharOccurrences('.') >= 1) { e.Handled = true; }
                        break;
                        // ...
                }
            }
        }

        /// <summary> Occurs when the Control Becomes the Active Control. </summary>
        /// <param name="e"></param>
        protected override void OnEnter(EventArgs e)
        {
            base.OnEnter(e);
            
            switch (inputType)
            {
                // ...

                case TextBoxInputType.Currency:
                    Text_RemoveWhiteSpaces();
                    Text_RemoveCurrencyDesignator();
                    break;

                // ...
            }

            // Select the Text
            SelectAll();
        }

        /// <summary> Occurs when the Control Stops Being the Active Control. </summary>
        /// <param name="e"></param>
        protected override void OnLeave(EventArgs e)
        {
            base.OnLeave(e);

            switch (inputType)
            {
                // ...

                case TextBoxInputType.Currency:
                    SetDecimalValue();
                    break;

                // ...
            }
        }

        #endregion


        #region <Methods> : (Validate Clipboard Data : On Paste)
        protected override void WndProc(ref Message m)
        {
            /*
             * Remarks: Handling Clipboard Data (Validate Data on Paste).
             * Adapted Code from: 'Thorarin'.
             * Source: https://stackoverflow.com/questions/15987712/handle-a-paste-event-in-c-sharp
             */

            // 1. Handle All Other Messages Normally.
            if (m.Msg != WM_PASTE) { base.WndProc(ref m); }

            // 2. Handle Clipboard Data (On Paste).
            else
            {
                if (Clipboard.ContainsText())
                {
                    string val = Clipboard.GetText();

                    if (HasValidClipboardContent(val)) { Text = val; }

                    // Note(s):
                    // Text Validation for Each Input Type, Occurs under Control Leave Event.

                    // Clipboard.Clear(); --> You can use this if you Wish to Clear the Clipboard after Pasting the Value
                }
            }
        }
        #endregion
        // 65666
        #region <Methods>
        /// <summary> Determines if the Clipboard Content Value is Valid. </summary>
        /// <param name="val"></param>
        /// <returns> True if Clipboard Content Matches the TextBox Input Requirements. </returns>
        private bool HasValidClipboardContent(string val)
        {
            bool isValid = false;

            switch (inputType)
            {
                case TextBoxInputType.Default: isValid = !IsLimitingChars(val.Length); break;
                case TextBoxInputType.Numeric:
                case TextBoxInputType.Currency:

                    isValid = !IsLimitingChars(val.Length) && IsNumericString(val);
                    break;

                case TextBoxInputType.IPV4:
                    isValid = HasValidIPAddress(val);
                    break;
            }

            return isValid;
        }

        /// <summary> Determines if Specified Char Paramter is a Valid Numeric Character. </summary>
        /// <param name="char"></param>
        /// <returns> true if Received Char is a Number. </returns>
        private bool HasValidNumericChar(char @char)
        {
            return allowedChars.Contains(@char) | @char.Equals((char)Keys.Back);
        }

        /// <summary> Determines if Received String Parameter is a Number. </summary>
        /// <param name="value"></param>
        /// <returns> True if Received String Parameter is a Number. </returns>
        private bool IsNumericString(string value)
        {
            bool isNumeric = true;

            for (int i = 0; i < value.Length; i++)
            {
                char c = value[i];

                if (!HasValidNumericChar(c))
                {
                    isNumeric = false;
                    break;
                }
            }

            return isNumeric;
        }

        /// <summary> Determines if Specified Parameter String Contains a Valid IPv4 Address. </summary>
        /// <returns> True if the IPv4 Address is Valid. </returns>
        private bool HasValidIPAddress(string value)
        {
            // Remarks:
            // Code based on Yiannis Leoussis Approach.
            // Using a 'for' Loop instead of 'foreach'.
            // Link: https://stackoverflow.com/questions/11412956/what-is-the-best-way-of-validating-an-ip-address

            bool isValid = true;

            if (string.IsNullOrWhiteSpace(Text)) { isValid = false; }

            //  Split string by ".", check that array length is 4
            string[] arrOctets = Text.Split('.');

            if (arrOctets.Length != 4) { isValid = false; }

            // Check Each Sub-String (Ensure that it Parses to byte)
            byte obyte = 0;

            for (int i = 0; i < arrOctets.Length; i++)
            {
                string strOctet = arrOctets[i];

                if (!byte.TryParse(strOctet, out obyte)) { isValid = false; }
            }

            // Set Default TextBox Text if IP is Invalid:
            if (!isValid) { Text_SetDefaultValue(); }

            return isValid;
        }

        /// <summary> Calculates the Nr. of Occurrences for the Specified Char Parameter. </summary>
        /// <param name="char"></param>
        /// <returns> The Number of the Received Char Parameter Occurrences Found in the TextBox Text. </returns>
        private int NrCharOccurrences(char @char)
        {
            return Text.Split(@char).Length - 1;
        }

        /// <summary> Adds the Currency Symbol to the End of the TextBox Text. </summary>
        private void Text_AddCurrencyDesignator()
        {
            // Add this to Control Event: Control_Leave

            if (inputType.Equals(TextBoxInputType.Currency))
            {
                if (!string.IsNullOrEmpty(Text) & !string.IsNullOrWhiteSpace(Text))
                {
                    TextAlign = HorizontalAlignment.Right;

                    switch (designatorAlignment)
                    {
                        case DesignatorAlignment.Left:
                            if (!Text.StartsWith(currencyDesignator))
                            {
                                Text = $"{currencyDesignator} {Text}";
                            }
                            break;

                        case DesignatorAlignment.Right:
                            if (!Text.EndsWith(currencyDesignator))
                            {
                                Text = $"{Text} {currencyDesignator}";
                            }
                            break;
                    }
                }
            }

            Text_Align();
        }

        /// <summary> Remove the Currency Symbol to the End of the TextBox Text. </summary>
        private void Text_RemoveCurrencyDesignator()
        {
            if (inputType.Equals(TextBoxInputType.Currency))
            {
                Text = Text.Replace(currencyDesignator, string.Empty);
            }
        }

        /// <summary> Remove White Spaces from TextBox Text. </summary>
        private void Text_RemoveWhiteSpaces()
        {
            if (inputType.Equals(TextBoxInputType.Currency) ^ inputType.Equals(TextBoxInputType.Numeric))
            {
                Text = Text.Replace(" ", string.Empty);
            }
        }

        /// <summary> Align TextBox Text. </summary>
        private void Text_Align()
        {
            switch (inputType)
            {
                case TextBoxInputType.Default: TextAlign = HorizontalAlignment.Left; break;
                case TextBoxInputType.Numeric:
                case TextBoxInputType.Currency: TextAlign = HorizontalAlignment.Right; break;
                case TextBoxInputType.IPV4: TextAlign = HorizontalAlignment.Center; break;
            }
        }        

        /// <summary> Sets the Text Value as a Decimal Value by Inserting Missing Zeros. </summary>
        private void Text_SetDecimalValue()
        {
            if (useDecimals)
            {
                decimal decVal = -1;
                string val = string.Empty;

                // Success:
                // [Reference]: if (decimal.TryParse(Text, out decVal)) { val = decVal.ToString("0.00"); }
                if (decimal.TryParse(Text, out decVal)) { val = decVal.ToString(decimalFormat); }

                // else { /* FAIL */ }

                // Set the Decimal Value as Text
                Text = val;
            }
        }

        /// <summary> Sets the Default Text Value to Each Input Type. </summary>
        private void Text_SetDefaultValue()
        {
            switch (inputType)
            {
                case TextBoxInputType.Default: Text = string.Empty; break;
                case TextBoxInputType.Numeric: 
                    if (string.IsNullOrEmpty(Text)) { Text = "0"; }
                    else 
                    { 
                        if (IsNumericString(Text)) { Text = Text; }
                    }
                    break;

                case TextBoxInputType.Currency:
                    if (string.IsNullOrEmpty(Text)) { Text = "0"; }
                    else
                    {
                        if (IsNumericString(Text)) 
                        {
                            Text_SetDecimalValue();
                            Text_AddCurrencyDesignator();
                            Text = Text; 
                        }
                    }
                     
                    break;
                case TextBoxInputType.IPV4: Text = "0.0.0.0"; break;
            }
        }
        #endregion
    }
}

Further References: