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

Numeric TextBox for Universal Windows App (UWP)

3.77/5 (4 votes)
21 Nov 2016CPOL2 min read 33.4K   412  
Numbers only textbox for Universal Windows App with options to allow negative numbers and set custom numeric pattern.

Image 1

Introduction

Class thats enhance TextBox with option to allow numbers only, limit posetive only and use "." (dot) as decimal character to jump to its fractional part (if any).

Background

Numbers only TextBox can be achieved using various techniques, most common by proper handling Key Events, however Microsoft advise against use for Key Events to handle text in UWP becouse of it's various input methods (keyboard, touch pad, pen, gamming control, remote control of tv...etc) and there is no proper way to know witch "text" particular key, or combination of keys, will generate.

Microsoft strongly advises to do it thru validation process (display ballons and warning errors) and let user to correct it.

I write mostly financial applications that use lot of currency fields and they particualary need to handle numeric data at same time display properly formated numbers according to user culture while editing/inserting data so user can easily be at pair with.

For me, input number (eg: 41234456.4556) and latter formating it, is much more dificult to write becouse it makes me constantly check if what i'm typing is correct; but if i can see formated number while I'm writing it (eg: 41,234,456.4556) thats makes lot of job easier.

There was also need to avoid negative numberes in certain fields of application, while best way to avoid such errors is thru validation, it can be completly handled in way to avoid such errors.

Image 2

Using the code

To use class, create new instance of ClsNumTextTagIn with default parameters in Tag property and create 3 necessary events of TextBox control:

  • SelectionChanged: refresh original data inside class when text selection or pointer position changes;
  • TextChanged: refresh original data inside class after being handdled in TextChanging event;
  • TextChanging: process text.
XML
<TextBox InputScope="Number" SelectionChanged="TextBox_SelectionChanged" TextChanged="TextBox_TextChanged" TextChanging="TextBox_TextChanging">
	<TextBox.Tag>
		<local:ClsNumTextTagIn />
	</TextBox.Tag>
</TextBox>
C#
	private void TextBox_SelectionChanged(object sender, RoutedEventArgs e)
        {
            TextBox tb = (TextBox)sender;
            if (tb.Tag != null)
            {
                ((ClsNumTextTagIn)tb.Tag).Refresh(tb);
            }
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            TextBox tb = (TextBox)sender;
            if (tb.Tag != null)
            {
                ((ClsNumTextTagIn)tb.Tag).Refresh(tb);
            }
        }

        private void TextBox_TextChanging(TextBox sender, TextBoxTextChangingEventArgs args)
        {
            TextBox tb = (TextBox)sender;
            if (tb.Tag != null)
            {
                ((ClsNumTextTagIn)tb.Tag).NumericText(tb);
            }
        }
or you can customize settings even further by setting properies:
 
Tag: use paramenter to pass some object in Tag here as alternative to TextBox.Tag parameter that is being used by this class.
MyPattern: allows to specify numeric pattern as string;
eg: MyPattern="N4" tip: integer only: MyPattern = "N0". 
IsNumOnly: true = text should always be numeric ignoring non numeric, false = accepts non numeric text;
eg: IsNumOnly="False".
IsDotSepa: true = "." counts as decimal separator (regardless of culture) and it will go for fractional part (if any).
eg: IsDotSepa ="True".
CanNegNum: true = text box can have negative numbers, false = don't allow negative numbers;
eg: CanNegNum="True"
LstRemStr: list of any other strings you wish remove (useful to accept pasted text);
eg:  <local:ClsNumTextTagIn.LstRemStr>
             <x:String>/</x:String>
             <x:String>,</x:String>
      </local:ClsNumTextTagIn.LstRemStr>
XML
<TextBox InputScope="Number" SelectionChanged="TextBox_SelectionChanged" TextChanged="TextBox_TextChanged" TextChanging="TextBox_TextChanging">
     <TextBox.Tag>
          <local:ClsNumTextTagIn MyPattern="N4" IsNumOnly="False" IsDotSepa="True" CanNegNum="False">
               <local:ClsNumTextTagIn.LstRemStr>
                    <x:String>/</x:String>
                    <x:String>,</x:String>
               </local:ClsNumTextTagIn.LstRemStr>
          </local:ClsNumTextTagIn>
     </TextBox.Tag>
</TextBox>
 
create ClsNumTextTagIn class in your project:
C#
public class ClsNumTextTagIn
    {
        private class cTxtBoxProperty
        {
            public int iBeg { get; set; }
            public int iSel { get; set; }
            public int iLen { get; set; }
            public string sLef { get; set; }
            public string sRig { get; set; }
            public string sSel { get; set; }
            public string sTxt { get; set; }
            public cTxtBoxProperty(Windows.UI.Xaml.Controls.TextBox sender)
            {
                iBeg = sender.SelectionStart;
                iSel = sender.SelectionLength;
                iLen = sender.Text.Length;
                sLef = sender.Text.Substring(0, sender.SelectionStart);
                sRig = sender.Text.Substring(sender.SelectionStart + sender.SelectionLength);
                sSel = sender.SelectedText;
                sTxt = sender.Text;
            }
        }

        private cTxtBoxProperty org { get; set; }
        public string MyPattern { get; set; }
        public bool IsNumOnly { get; set; }
        public bool IsDotSepa { get; set; }
        public bool CanNegNum { get; set; }
        public System.Collections.Generic.List<string> LstRemStr { get; set; }
        public object Tag { get; set; } //use this Tag

        public ClsNumTextTagIn()
        {
            //org = new cTxtBoxProperty(sender);
            MyPattern = "N2";
            IsNumOnly = true;
            IsDotSepa = true;
            CanNegNum = true;
            LstRemStr = new System.Collections.Generic.List<string>();
            //Tag = anyTag; //use this Tag
        }
        /// <summary>
        /// Routine to test numeric if current TextChanging is numeric and display accordinly to current culture.
        /// 
        ///<param name="sender" />Target TextBox
        ///<param name="pattern" />Currently only Numeric Pattern is supported eg: "N2", "N6"...etc. N stands for number.
        ///<param name="numbersOnly" />true = allow numbers only; false = leave text if its not numeric decimal.
        ///<param name="dotAsDecimalSeparator" />true = "." counts as decimal separator (regardless of culture) and it will go for decimal part of pattern (if any).
        ///<param name="allowNegativeNumbers" />true = can have negative numbers, false = prevents negative numbers.
        ///<param name="lstRemoveStrings" />List of any other strings you wish remove eg: "(", " / ", ")", " - ", " + "...etc, if none just pass "null" parameter.
        ///<param name="anyTag" />Use paramenter to pass some object in Tag, if none just pass "null" parameter.
        public ClsNumTextTagIn(Windows.UI.Xaml.Controls.TextBox sender, string pattern, bool numbersOnly, bool dotAsDecimalSeparator, bool allowNegativeNumbers, System.Collections.Generic.List<string> lstRemoveStrings, System.Object anyTag)
        {
            org = new cTxtBoxProperty(sender);
            MyPattern = pattern;
            IsNumOnly = numbersOnly;
            IsDotSepa = dotAsDecimalSeparator;
            CanNegNum = allowNegativeNumbers;
            LstRemStr = lstRemoveStrings;
            Tag = anyTag; //use this Tag
        }
        public void Refresh(Windows.UI.Xaml.Controls.TextBox sender)
        {
            org = new cTxtBoxProperty(sender);
        }
        public void NumericText(Windows.UI.Xaml.Controls.TextBox sender)
        { //logics
            if (org == null)
            {
                org = new cTxtBoxProperty(sender);
            }

            System.Globalization.CultureInfo ci = System.Globalization.CultureInfo.CurrentUICulture;
            cTxtBoxProperty edi = new cTxtBoxProperty(sender);
            string sPtn = MyPattern;
            string sAdd = string.Empty;
            string sSub = string.Empty;
            int iChg = edi.iLen - org.iBeg - org.sRig.Length;

            if (iChg >= 0)
            {
                sAdd = edi.sTxt.Substring(org.iBeg, iChg);
                sSub = org.sSel;
            }
            else
            { //backspace?
                if (org.iLen >= edi.iLen)
                {
                    if (org.iBeg - (org.iLen - edi.iLen) >= 0)
                    {
                        sSub = org.sTxt.Substring(org.iBeg - (org.iLen - edi.iLen), (org.iLen - edi.iLen));
                    }
                }
            }

            int iBgx = edi.iBeg;
            int iSlx = 0;
            string sFnl = org.sTxt;

            int iNeg = sFnl.IndexOf(ci.NumberFormat.NegativeSign);
            int iDot = sFnl.IndexOf(ci.NumberFormat.NumberDecimalSeparator);

            if (sAdd == ci.NumberFormat.NegativeSign)
            {
                if (iNeg == -1)
                { //add
                    if (CanNegNum)
                    {//accepts negative numbers
                        //if (ci.TextInfo.IsRightToLeft)
                        //{
                        //    sFnl = ci.NumberFormat.NegativeSign + sFnl; //sFnl = sFnl + ci.NumberFormat.NegativeSign;
                        //    iBgx = edi.iBeg - ci.NumberFormat.NegativeSign.Length;
                        //}
                        //else
                        //{
                            sFnl = ci.NumberFormat.NegativeSign + sFnl;
                            iBgx = edi.iBeg;
                        //}
                    }
                    else
                    {//does not accepts negative numbers


                        //if (ci.TextInfo.IsRightToLeft)
                        //{
                            iBgx = edi.iBeg - ci.NumberFormat.NegativeSign.Length;
                        //}
                        //else
                        //{
                        //    iBgx = edi.iBeg;
                        //}

                        
                    }
                }
                else
                {//remove
                    sFnl = sFnl.Remove(iNeg, ci.NumberFormat.NegativeSign.Length);
                    if (iNeg >= iBgx)
                    {
                        iBgx = edi.iBeg - ci.NumberFormat.NegativeSign.Length;
                    }
                    else
                    {
                        //if (ci.TextInfo.IsRightToLeft)
                        //{
                        //    iBgx = edi.iBeg - ci.NumberFormat.NegativeSign.Length;
                        //}
                        //else
                        //{
                            iBgx = edi.iBeg - ci.NumberFormat.NegativeSign.Length - ci.NumberFormat.NegativeSign.Length;
                        //}
                    }
                }
            } //end NegativeSign
            else if (sAdd == ci.NumberFormat.NumberDecimalSeparator)
            { //NumberDecimalSeparator
                if (iDot == -1)
                { //add
                    sFnl = edi.sTxt;
                }
                else
                { //go to point
                    sFnl = org.sTxt;
                    iBgx = iDot + ci.NumberFormat.NumberDecimalSeparator.Length;
                }
            }
            else if (sAdd == ".")
            { //dotAsDecimalSeparator
                if (IsDotSepa)
                {
                    if (iDot == -1)
                    { //add
                        sFnl = edi.sTxt;
                    }
                    else
                    { //go to point
                        sFnl = org.sTxt;
                        iBgx = iDot + ci.NumberFormat.NumberDecimalSeparator.Length;
                    }
                }
                else
                {
                    sFnl = edi.sTxt; //override
                }
            }
            else if (sSub == ci.NumberFormat.NumberGroupSeparator & org.iSel == 0)
            { //backspace and GroupSeparator
                if (edi.iBeg == 0)
                {
                    sFnl = edi.sTxt;
                }
                else
                {
                    sFnl = edi.sLef.Replace(ci.NumberFormat.NumberGroupSeparator, string.Empty);
                    int iRem = edi.sLef.Length - sFnl.Length;
                    sFnl = sFnl.Substring(0, edi.sLef.Length - iRem - 1) + edi.sRig;
                    iBgx = iBgx - iRem - 1;
                }
            }//end backspace and GroupSeparator
            else
            {
                sFnl = edi.sTxt;
            }

            if (iBgx > sFnl.Length)
            {
                iBgx = sFnl.Length;
            }
            string sLfx = sFnl.Substring(0, iBgx);
            string sRgx = sFnl.Substring(iBgx);

            sRgx = sRgx.Replace(ci.NumberFormat.NumberGroupSeparator, string.Empty);
            sFnl = sFnl.Replace(ci.NumberFormat.NumberGroupSeparator, string.Empty);

            if (LstRemStr != null)
            {
                foreach (string sRem in LstRemStr)
                {
                    sRgx = sRgx.Replace(sRem, string.Empty); //automatically trimmed
                    sFnl = sFnl.Replace(sRem, string.Empty); //automatically trimmed
                }
            }

            //s += "Rgx: " + sRgx.ToString() + Environment.NewLine;
            //s += "Fnl: " + sFnl.ToString() + Environment.NewLine;

            if (!CanNegNum)
            {//remove negative sign
                sRgx = sRgx.Replace(ci.NumberFormat.NegativeSign, string.Empty);
                sFnl = sFnl.Replace(ci.NumberFormat.NegativeSign, string.Empty);
            }

            decimal dFnl = decimal.Zero;
            decimal dRgx = decimal.Zero;
            if (decimal.TryParse(sFnl, out dFnl))
            {

                int iDtx = sFnl.IndexOf(ci.NumberFormat.NumberDecimalSeparator);
                int iDif = sFnl.Length - dFnl.ToString().Length;

                if (iDtx == -1)
                {//DecimalSeparator does not exist
                    sFnl = dFnl.ToString(sPtn);
                    int iTmp = sFnl.IndexOf(ci.NumberFormat.NumberDecimalSeparator);
                    if (iTmp == -1)
                    {//DecimalSeparator not found after apply pattern
                        if (sRgx == string.Empty)
                        {
                            iBgx = sFnl.Length;
                        }
                        else
                        {
                            if (sRgx.StartsWith(decimal.Zero.ToString()))
                            {
                                sRgx = decimal.One.ToString() + sRgx;
                                if (decimal.TryParse(sRgx, out dRgx))
                                {
                                    sRgx = dRgx.ToString(sPtn);
                                    if (sRgx.StartsWith(decimal.One.ToString() + ci.NumberFormat.NumberDecimalSeparator))
                                    {
                                        iBgx = sFnl.Length - sRgx.Length + (decimal.One.ToString() + ci.NumberFormat.NumberDecimalSeparator).Length;
                                    }
                                    else
                                    {
                                        iBgx = sFnl.Length - sRgx.Length + decimal.One.ToString().Length;
                                    }
                                }
                            }
                            else
                            {
                                if (decimal.TryParse(sRgx, out dRgx))
                                {
                                    sRgx = dRgx.ToString(sPtn);
                                    iBgx = sFnl.Length - sRgx.Length;
                                }
                            }
                        }
                    }
                    else
                    {//DecimalSeparator found after apply pattern
                        if (sRgx == string.Empty)
                        {
                            iBgx = iTmp;
                        }
                        else
                        {
                            if (sRgx.StartsWith(decimal.Zero.ToString()))
                            {
                                sRgx = decimal.One.ToString() + sRgx;
                                if (decimal.TryParse(sRgx, out dRgx))
                                {
                                    sRgx = dRgx.ToString(sPtn);
                                    if (sRgx.StartsWith(decimal.One.ToString() + ci.NumberFormat.NumberDecimalSeparator))
                                    {
                                        iBgx = sFnl.Length - sRgx.Length + (decimal.One.ToString() + ci.NumberFormat.NumberDecimalSeparator).Length;
                                    }
                                    else
                                    {
                                        iBgx = sFnl.Length - sRgx.Length + decimal.One.ToString().Length;
                                    }
                                }
                            }
                            else
                            {
                                if (decimal.TryParse(sRgx, out dRgx))
                                {
                                    sRgx = dRgx.ToString(sPtn);
                                    iBgx = sFnl.Length - sRgx.Length;
                                }
                            }
                        }
                    }
                    if (iBgx < 0)
                    {
                        iBgx = 0;
                    }
                }
                else
                {//DecimalSeparator does exist
                    if (sFnl.Length - sRgx.Length <= iDtx)
                    {//before DecimalSeparator
                        sFnl = dFnl.ToString(sPtn);
                        if (sRgx.StartsWith(decimal.Zero.ToString()))
                        {
                            sRgx = decimal.One.ToString() + sRgx;
                            if (decimal.TryParse(sRgx, out dRgx))
                            {
                                sRgx = dRgx.ToString(sPtn);
                                if (sRgx.StartsWith(decimal.One.ToString() + ci.NumberFormat.NumberDecimalSeparator))
                                {
                                    iBgx = sFnl.Length - sRgx.Length + (decimal.One.ToString() + ci.NumberFormat.NumberDecimalSeparator).Length;
                                }
                                else
                                {
                                    iBgx = sFnl.Length - sRgx.Length + decimal.One.ToString().Length;
                                }
                            }
                        }
                        else if (sRgx.StartsWith(ci.NumberFormat.NumberDecimalSeparator))
                        {
                            if (decimal.TryParse(sRgx, out dRgx))
                            {
                                sRgx = dRgx.ToString(sPtn);
                                iBgx = sFnl.Length - sRgx.Length + ci.NumberFormat.NumberDecimalSeparator.Length;
                            }
                        }
                        else
                        {
                            if (decimal.TryParse(sRgx, out dRgx))
                            {
                                sRgx = dRgx.ToString(sPtn);
                                iBgx = sFnl.Length - sRgx.Length;
                            }
                        }

                        if (iBgx < 0)
                        {
                            iBgx = 0;
                        }
                    }
                    else
                    {//after DecimalSeparator
                        sFnl = dFnl.ToString(sPtn);
                        int iTmp = sFnl.IndexOf(ci.NumberFormat.NumberDecimalSeparator); //override
                        if (iTmp == -1)
                        {// pattern does not accept DecimalSeparator
                            iBgx = sFnl.Length;
                        }
                        else
                        {
                            if (sSub != string.Empty & sAdd == string.Empty)
                            {// if backspace (after decimal separator)
                                if (iTmp + 1 < iBgx)
                                {
                                    iBgx = iBgx - 1;
                                }
                                else
                                {
                                    iBgx = iTmp;
                                }
                            }

                            if (sFnl.Length > iBgx)
                            {
                                if (iBgx > iTmp)
                                {
                                    iSlx = 1;
                                }
                            }
                        }
                    }
                }
                sender.Text = sFnl;
                sender.SelectionStart = iBgx;
                sender.SelectionLength = iSlx;
            }//end of if decimal
            else
            {//not decimal
                if (IsNumOnly)
                { //IsNumOnly =true
                    decimal dOrg = decimal.Zero;
                    if (sFnl != string.Empty && decimal.TryParse(org.sTxt, out dOrg))
                    {
                        sender.Text = org.sTxt;
                        sender.SelectionStart = org.iBeg;
                        sender.SelectionLength = org.iSel;
                    }
                    else
                    {
                        decimal dAdd = decimal.Zero;
                        decimal.TryParse(sAdd, out dAdd);
                        sFnl = dAdd.ToString(sPtn);
                        int iTmp = sFnl.IndexOf(ci.NumberFormat.NumberDecimalSeparator);

                        if (iTmp == -1)
                        {
                            iBgx = sFnl.Length;
                        }
                        else
                        {
                            iBgx = iTmp;
                        }
                        sender.Text = sFnl;
                        sender.SelectionStart = iBgx;
                        sender.SelectionLength = 0;
                    }
                }
                else
                { //numbersOnly false
                    sender.Text = edi.sTxt;
                    sender.SelectionStart = edi.iBeg;
                    sender.SelectionLength = edi.iSel;
                }
            }
        }
    }

 

Points of Interest

The NumericText routine can be improved to include parameters for Minimum and Maximum numbers allowed.

It may annoy some users the current way of handling text after decimal point.

It was not tested with languages that have Right To Left pattern!

Please help identify any bug, or improvement suggestion.

Note: if you are testing attached sample application in diferent plataform than ARM please change debug to x86 or x64 accordinly.

Thank you.

History

Version 1.01 - added sample application and  screenshoot; included image to show different world cultures.

Verision 1.00 - original.

License

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