Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

smart WPF text-box with Input Restriction and Validation

5.00/5 (1 vote)
24 Jan 2016CPOL1 min read 17.4K  
Custom TextBox control for UI input validation

Introduction

EDIT:
-found a couple of small bugs. (will update code, when done checking)
-implemented DP for "Value". (will update code, when done checking)
-known issue: "Value" setter is fired more than once, when text is updated from UI.
-known issue: numbers string format is NOT according to region.
--------------------------------------------------------------------------------------------

My WPF dev. started with inheriting a one-man application, written in "WPF", using win-forms event driven approach, and NOT using any of the WPF provided technologies. Shouldn't even mention the architecture, which was further from MVVM, as we are from colonizing Mars.

One of the most annoying issues I've encountered was validation, restriction and formatting of numerical user input. Given the nature of the application, some fields are positive only, some are integer only, etc.

I needed a solution that will address both data-binding, AND using the UI element directly. Additionally, I wished for both the string value, and the numerical to be readily available. Lastly, I required "fine-tuning" the values by keys/mouse wheel + different text foregrounds.

Below is my solution, which may be easily expanded and modified according to specific needs.
What can it do:

  • restrict input to match Int/Double correct format, disable copy-paste
  • IsPositiveOnly setter ("0" is considered "positive")
  • Normal/Modified/Error text foreground setters
  • display numerics with "thousands" commas (I'm dealing with large numbers, so it really helps)
  • number of (double) fraction digits setter
  • keys Up/Down/P-Up/P-Down/mouse wheel - increase/decrease the value by predefined %

Using the Code

  • Usable as is.
  • Abstract base class + implementations for Int and Double data-types.
  • If binding to the numeric "Value" is desired, a DP should be implemented.

Base Class

C#
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace CustomControls
{
    public abstract class SmartTextBox : TextBox
    {
        public string ParamName { get; set; }
        public bool IsValid { get; protected set; }
        public bool IsPosOnly { get; set; }
        public bool IsSetModifiedFG { get; set; }

        public int FinePercent { get; set; }
        public int CoarsePercent { get; set; }
        public int WheelPercent { get; set; }

        private static readonly BrushConverter _BrushConverter = new BrushConverter();
        private const string _DefaultBgColor = "#3CFFFFFF";
        protected SmartTextBox()
        {
            IsPosOnly = true;
            IsSetModifiedFG = true;
            FinePercent = 1;
            CoarsePercent = 10;
            WheelPercent = 3;

            DataObject.AddCopyingHandler(this, 
            (sender, args) => args.CancelCommand()); //disable copy-paste
            DataObject.AddPastingHandler(this, 
            (sender, args) => args.CancelCommand()); //disable copy-paste
            this.PreviewMouseRightButtonUp += 
            (sender, args) => args.Handled = true;    //disable right-click context menu
            this.PreviewKeyDown += OnPreviewKeyDown;
            this.TextChanged += OnTextChanged;
            this.MouseWheel += OnMouseWheel;

            this.Background = (Brush)_BrushConverter.ConvertFromString(_DefaultBgColor);
        }

        protected abstract void ApplyField();
        protected abstract void CancelField();

        public void SetNormalForeground()
        {
            this.Foreground = Brushes.Black;
        }

        public void SetModifiedForeground()
        {
            this.Foreground = Brushes.RoyalBlue;
        }

        public void SetErrorForeground()
        {
            this.Foreground = Brushes.Red;
        }

        private void OnMouseWheel(object sender, MouseWheelEventArgs mouseWheelEventArgs)
        {
            if (mouseWheelEventArgs.Delta > 0)
                IncrementByPercent(WheelPercent);
            else
                DecrementByPercent(WheelPercent);
        }

        protected virtual void OnTextChanged(object sender, TextChangedEventArgs textChangedEventArgs)
        {
            IsValid = !(this.Text == "" || 
            this.Text == "-" || this.Text == "Not supported");
        }

        protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs k)
        {
            if (k.Key == Key.Enter)
            {
                ApplyField();
            }
            else if (k.Key == Key.Escape)
            {
                CancelField();
            }
            else if (k.Key == Key.Up)
            {
                IncrementByPercent(FinePercent);
            }
            else if (k.Key == Key.Down)
            {
                DecrementByPercent(FinePercent);
            }
            else if (k.Key == Key.PageUp)
            {
                k.Handled = true;
                IncrementByPercent(CoarsePercent);
            }
            else if (k.Key == Key.PageDown)
            {
                k.Handled = true;
                DecrementByPercent(CoarsePercent);
            }
            else if (!IsPosOnly && (k.Key == Key.Subtract || k.Key == Key.OemMinus))
            {
                if (this.Text.StartsWith("-") || this.CaretIndex != 0)
                    k.Handled = true;
            }
            else
            {
                int keyInt = (int)k.Key;
                if ((keyInt >= 34 && keyInt <= 43) || 
                (keyInt >= 74 && keyInt <= 83)) //numerics
                {
                    if (this.CaretIndex == 0 && 
                    (this.Text.StartsWith("-") || this.Text.StartsWith(".")))
                        k.Handled = true;
                }
                else if (!(k.Key == Key.Back || k.Key == Key.Delete || k.Key == Key.Left || 
                k.Key == Key.Right || k.Key == Key.Tab || k.Key == Key.Home || k.Key == Key.End))
                {
                    k.Handled = true;
                }
            }
        }

        protected abstract void IncrementByPercent(int percent);

        protected abstract void DecrementByPercent(int percent);
    }
}

INT Handling Implementation

C#
using System;
using System.Windows.Controls;

namespace CustomControls
{
    public class SmartIntTextBox : SmartTextBox
    {
        public Action<smartinttextbox> ApplyFieldEvent;
        public Action<smartinttextbox> CancelFieldEvent;
        public Action<smartinttextbox> DataChangedEvent;

        private int _Value;
        public int Value
        {
            get { return _Value; }
            protected set
            {
                _Value = value;

                int ci = this.CaretIndex;
                int len = this.Text.Length;
                this.Text = _Value.ToString("N0");
                this.CaretIndex = ci + (this.Text.Length - len);    //account for added commas

                if (DataChangedEvent != null)
                    DataChangedEvent(this);
            }
        }

        protected override void ApplyField()
        {
            if (ApplyFieldEvent != null)
                ApplyFieldEvent(this);
        }

        protected override void CancelField()
        {
            if (CancelFieldEvent != null)
                CancelFieldEvent(this);
        }

        protected override void OnTextChanged(object sender, TextChangedEventArgs textChangedEventArgs)
        {
            base.OnTextChanged(sender, textChangedEventArgs);
            if(!IsValid)
                return;

            if (!int.TryParse(this.Text.Replace(",", ""), out _Value))
            {
                _Value = 0;
                IsValid = false;
                SetErrorForeground();
                return;
            }

            Value = _Value;

            IsValid = true;
            if (IsSetModifiedFG)
                SetModifiedForeground();
        }
        protected override void IncrementByPercent(int percent)
        {
            if (_Value == 0)
            {
                ++Value;
                return;
            }

            double ratio = ((double)percent + 100) / 100;
            Value = _Value > 0 ? (int)Math.Ceiling(_Value * ratio) : (int)Math.Ceiling(_Value / ratio);
        }

        protected override void DecrementByPercent(int percent)
        {
            if (!IsPosOnly && _Value == 0)
            {
                --Value;
                return;
            }

            double ratio = ((double)percent + 100) / 100;
            Value = _Value > 0 ? (int)Math.Floor(_Value / ratio) : (int)Math.Floor(_Value * ratio);
        }
    }
}

Double Handling Implementation

C#
using System;
using System.Windows.Controls;
using System.Windows.Input;

namespace CustomControls
{
    public class SmartDoubleTextBox : SmartTextBox
    {
        public Action<smartdoubletextbox> ApplyFieldEvent;
        public Action<smartdoubletextbox> CancelFieldEvent;
        public Action<smartdoubletextbox> DataChangedEvent;

        private string _Format = "{0:#,##0.##}";
        private int _DecPlaces = 2;
        public int DecPlaces
        {
            get { return _DecPlaces; }
            set
            {
                _DecPlaces = value;
                _Format = "{0:#,##0.";
                for (int ii = 0; ii < _DecPlaces; ++ii)
                    _Format += "#";
                _Format += "}";

                this.Text = string.Format(_Format, _Value);
            }
        }
        
        private double _Value;
        public double Value
        {
            get { return _Value; }
            protected set
            {
                _Value = value;

                int ci = this.CaretIndex;
                int len = this.Text.Length;
                this.Text = string.Format(_Format, _Value);
                this.CaretIndex = ci + (this.Text.Length - len);    //account for added commas

                if (DataChangedEvent != null)
                    DataChangedEvent(this);
            }
        }

        protected override void ApplyField()
        {
            if (ApplyFieldEvent != null)
                ApplyFieldEvent(this);
        }

        protected override void CancelField()
        {
            if (CancelFieldEvent != null)
                CancelFieldEvent(this);
        }

        protected override void OnTextChanged(object sender, TextChangedEventArgs textChangedEventArgs)
        {
            base.OnTextChanged(sender, textChangedEventArgs);
            if (!IsValid)
                return;

            if (!double.TryParse(this.Text.Replace(",", ""), out _Value))
            {
                _Value = 0;
                IsValid = false;
                SetErrorForeground();
                return;
            }

            if (!(this.Text.EndsWith(".") || 
            (this.Text.Contains(".") && this.Text.EndsWith("0"))))
                Value = _Value;

            IsValid = true;
            if (IsSetModifiedFG)
                SetModifiedForeground();
        }

        protected override void OnPreviewKeyDown(object sender, KeyEventArgs k)
        {
            int decIdx = this.Text.IndexOf(".");
            if (k.Key == Key.Decimal || k.Key == Key.OemPeriod)
            {
                if (decIdx != -1)
                    k.Handled = true;
                return;
            }
            if (decIdx >= 0)
            {
                int keyInt = (int)k.Key;
                if (this.CaretIndex > decIdx &&
                    this.Text.Length - decIdx - 1 >= _DecPlaces &&
                    ((keyInt >= 34 && keyInt <= 43) || 
                    (keyInt >= 74 && keyInt <= 83)))
                {
                    k.Handled = true;
                    return;
                }
            }

            base.OnPreviewKeyDown(sender, k);
        }

        protected override void IncrementByPercent(int percent)
        {
            if (Math.Abs(_Value) < 0.00001)
            {
                ++Value;
                return;
            }

            double ratio = ((double)percent + 100) / 100;
            Value = _Value > 0 ? _Value * ratio : _Value / ratio;
        }

        protected override void DecrementByPercent(int percent)
        {
            if (!IsPosOnly && Math.Abs(_Value) < 0.00001)
            {
                --Value;
                return;
            }

            double ratio = ((double)percent + 100) / 100;
            Value = _Value > 0 ? _Value / ratio : _Value * ratio;
        }
    }
}

History

First version, will probably be updated according to new findings and needs. : )

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)