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.
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.
<TextBox InputScope="Number" SelectionChanged="TextBox_SelectionChanged" TextChanged="TextBox_TextChanged" TextChanging="TextBox_TextChanging">
<TextBox.Tag>
<local:ClsNumTextTagIn />
</TextBox.Tag>
</TextBox>
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>
<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:
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; }
public ClsNumTextTagIn()
{
MyPattern = "N2";
IsNumOnly = true;
IsDotSepa = true;
CanNegNum = true;
LstRemStr = new System.Collections.Generic.List<string>();
}
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;
}
public void Refresh(Windows.UI.Xaml.Controls.TextBox sender)
{
org = new cTxtBoxProperty(sender);
}
public void NumericText(Windows.UI.Xaml.Controls.TextBox sender)
{
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
{
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)
{
if (CanNegNum)
{
sFnl = ci.NumberFormat.NegativeSign + sFnl;
iBgx = edi.iBeg;
}
else
{
iBgx = edi.iBeg - ci.NumberFormat.NegativeSign.Length;
}
}
else
{
sFnl = sFnl.Remove(iNeg, ci.NumberFormat.NegativeSign.Length);
if (iNeg >= iBgx)
{
iBgx = edi.iBeg - ci.NumberFormat.NegativeSign.Length;
}
else
{
iBgx = edi.iBeg - ci.NumberFormat.NegativeSign.Length - ci.NumberFormat.NegativeSign.Length;
}
}
}
else if (sAdd == ci.NumberFormat.NumberDecimalSeparator)
{
if (iDot == -1)
{
sFnl = edi.sTxt;
}
else
{
sFnl = org.sTxt;
iBgx = iDot + ci.NumberFormat.NumberDecimalSeparator.Length;
}
}
else if (sAdd == ".")
{
if (IsDotSepa)
{
if (iDot == -1)
{
sFnl = edi.sTxt;
}
else
{
sFnl = org.sTxt;
iBgx = iDot + ci.NumberFormat.NumberDecimalSeparator.Length;
}
}
else
{
sFnl = edi.sTxt;
}
}
else if (sSub == ci.NumberFormat.NumberGroupSeparator & org.iSel == 0)
{
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;
}
}
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);
sFnl = sFnl.Replace(sRem, string.Empty);
}
}
if (!CanNegNum)
{
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)
{
sFnl = dFnl.ToString(sPtn);
int iTmp = sFnl.IndexOf(ci.NumberFormat.NumberDecimalSeparator);
if (iTmp == -1)
{
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
{
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
{
if (sFnl.Length - sRgx.Length <= iDtx)
{
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
{
sFnl = dFnl.ToString(sPtn);
int iTmp = sFnl.IndexOf(ci.NumberFormat.NumberDecimalSeparator);
if (iTmp == -1)
{
iBgx = sFnl.Length;
}
else
{
if (sSub != string.Empty & sAdd == string.Empty)
{
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;
}
else
{
if (IsNumOnly)
{
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
{
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.