具有智利 RUT 掩码的 WPF 文本控件





5.00/5 (2投票s)
一个 Windows Presentation Foundation TextBox 控件,支持智利税号掩码,并通过模 11 算法进行验证。
引言
在过去的几个月里,我一直在开发一个桌面应用程序,该应用程序需要在各种表单中输入客户和供应商的智利税号(称为 RUT)。由于 RUT 的格式遵循 NN.NNN.NNN-C 的模式,我希望阻止用户输入任何不符合要求的数值,因此我决定创建我的第一个自定义控件,它继承自 TextBox
,可以只接受允许的数值,并自动提供 RUT 掩码。
背景
RUT(Rol Único Tributario,单一税务登记号)是智利使用的唯一税号。这是一个 7 到 8 位数字,外加一个校验位(可以是 0 到 9 的数字,或字母 'K'),该校验位通过模 11 算法计算得出。RUT 通常用一个连字符来分隔 7 到 8 位数字(左侧)和校验位(右侧)。
模 11
模 11 是一种用于检查数字序列中数据完整性的数学算法。该算法返回一个介于 0 到 11 之间的值,称为校验位,用于验证序列。通常,标识号(如 RUT)的最后一个数字就是校验位。
通过模 11 计算校验位的步骤如下:
- 获取不包含校验位的数字序列并将其反转。
18798442 -> 24489781
- 使用以下因子模式将反转后的数字的每一位进行乘法运算:2, 3, 4, 5, 6, 7。如果序列长度超过六位,则重复该模式。
值 2 4 4 8 9 7 8 1 因素 x2 x3 x4 x5 x6 x7 x2 x3 结果 =4 =12 =16 =40 =54 =49 =16 =3 - 将上一步(2)中得到的乘积相加。
4 + 12 + 16 + 40 + 54 +49 + 16 + 3 = 194
- 将 1) 步骤 3 中得到的总和结果除以 11,得到余数。
194 % 11 = 7
- 从 11 中减去步骤 4 的余数。减法的结果即为校验位。
11 - 7 = 4
- 智利 RUT 的额外步骤:如果减法结果是 10,则校验位为 'K';如果结果是 11,则校验位为 0。
创建控件
首先,我创建了一个继承自 TextBox
的新控件,我将其命名为 RutBox
。
public class RutBox : TextBox
{
}
因此,这个控件具有 TextBox
的所有属性。之后,我定义了将在代码中使用的字段,并开始声明一个常量字符串,其中包含用作数字和校验位分隔符的连字符。
private const string ComponentSeparator = "-";
此外,还有一个 string
用于设置稍后与 CultureInfo
类一起使用的区域性名称。
private const string CultureName = "es-CL";
RUT(包括校验位)允许的最小和最大长度。
private const int MaxLengthAllowed = 9;
private const int MinLengthAllowed = 8;
RUT 的模式和忽略大小写的 Regex 选项(考虑字母 'K
')。
private const string Pattern = @"^[0-9]+K?$";
private const RegexOptions RegexPatternOption = RegexOptions.IgnoreCase;
另一方面,我声明了两个只读字段:一个用于获取 CultureInfo
,另一个用于从 RutCulture
获取千位分隔符并用于格式化数字部分。
private readonly CultureInfo RutCulture;
private readonly string GroupSeparator;
最后,我声明了一个字段来显示或隐藏千位分隔符。
private bool showThousandsSeparator;
定义属性
我创建的第一个属性是 Value
,它获取 TextBox
的文本并调用 GetRutWithoutSeparators()
来删除输入中的组分隔符和组件分隔符。如果值符合模式,则设置该值,否则会引发异常(顺便说一句,如果值为 null
,则会更改为空 string
)。
public string Value
{
get
{
return GetRutWithoutSeparators(this.Text);
}
set
{
//Sets empty string if value is null.
value = value ?? string.Empty;
if (!Regex.IsMatch(value, Pattern, RegexPatternOption) && value != string.Empty)
{
throw new ArgumentException("Value is not valid.", "Value");
}
else
{
this.Text = value;
}
}
}
此处定义的是 GetRutWithoutSeparators()
方法,它唯一的参数是一个包含带分隔符的 RUT 的 string
。
private string GetRutWithoutSeparators(string rutWitSeparators)
{
rutWitSeparators = rutWitSeparators.Replace(GroupSeparator, string.Empty).Replace
(ComponentSeparator, string.Empty);
return rutWitSeparators;
}
另一个属性是 IsValid
。如果值在允许的最小和最大长度之间,并且符合 RUT 的模式,则返回 true
,否则返回 false
。为了验证字符串,需要将 RUT 分成两部分:左侧是数字,右侧是校验位;然后使用 GetModulus11CheckDigit()
获取校验位,并将其与从分割 RUT 中获得的校验位进行比较。
public bool IsValid
{
get
{
if (Regex.IsMatch(this.Value, Pattern, RegexPatternOption) &&
this.Value.Length >= MinLengthAllowed && this.Value.Length <= MaxLengthAllowed)
{
long rutWithoutCheckDigit =
long.Parse(this.Value.Substring(0, this.Value.Length - 1));
string checkDigit = this.Value.Substring(this.Value.Length - 1, 1);
return checkDigit ==
this.GetModulus11CheckDigit(rutWithoutCheckDigit) ? true : false;
}
else
{
return false;
}
}
}
GetModulus11CheckDigit()
方法需要一个整数值来应用算法,返回一个代表校验位的 string
,并将 11 替换为 0
,将 10 替换为 'K
'。
private string GetModulus11CheckDigit(long number)
{
long sum = 0;
int multiplier = 2;
//Get each digit of the number.
while (number != 0)
{
//Check if multiplier is between 2 and 7, otherwise reset to 2.
multiplier = multiplier > 7 ? 2 : multiplier;
//Get the last digit of the number, multiply and add it.
sum += (number % 10) * multiplier;
//Remove last number from right to left.
number /= 10;
//And increase multiplier by 1.
multiplier++;
}
sum = 11 - (sum % 11);
//Evaluate the result of the operation to get the check digit.
switch (sum)
{
case 11:
return "0";
case 10:
return "K";
default:
return sum.ToString();
}
}
创建的最后一个属性是 ShowThousandsSeparator
,它提供了显示或隐藏千位分隔符的选项。
public bool ShowThousandsSeparator
{
get
{
return showThousandsSeparator;
}
set
{
showThousandsSeparator = value;
UseMask();
}
}
处理控件的行为
现在是时候考虑控件在以下情况下将如何表现了:
- 当控件获得焦点时显示掩码,当失去焦点时隐藏掩码。
- 用户通过键盘输入值。
- 用户通过粘贴输入值。
首先,我们必须定义构造函数,在其中为粘贴和文本更改事件添加处理程序,默认将 CharacterCasing
属性设置为大写,MaxLength
属性设置为 RUT 的最大允许长度,RutCulture
成员初始化为 "es-CL
" 值,showThousandsSeparator
设置为 true
,并且 Value
属性设置为空字符串。
public RutBox()
{
DataObject.AddPastingHandler(this, this.RutBox_OnPaste);
this.CharacterCasing = CharacterCasing.Upper;
this.MaxLength = MaxLengthAllowed;
this.RutCulture = new CultureInfo(CultureName);
this.GroupSeparator = RutCulture.NumberFormat.NumberGroupSeparator;
this.TextChanged += this.RutBox_TextChanged;
this.showThousandsSeparator = true;
this.Value = string.Empty;
}
对于掩码,我创建了一个名为 UseMask()
的方法,它检查 RutBox
是否获得了焦点。如果 IsFocused
为 false
,它会尝试将格式应用于文本。重要的是在进行更改时取消订阅 TextChanged
事件,或者当我们为 Text
属性分配新值时,将不会调用事件处理程序。
private void UseMask()
{
//It's necessary to unsubscribe TextChanged event handler
//while setting a value for Text property.
this.TextChanged -= this.RutBox_TextChanged;
if (this.IsFocused)
{
//If control is Focused, show chilean RUT without separators.
this.Text = this.Value;
}
else
{
//But if the control isn't focused, show chilean RUT with separators.
if (this.Value.Length > 1)
{
bool isValidNumber = long.TryParse(this.Value.Substring(0, this.Value.Length - 1),
NumberStyles.Any, RutCulture, out long rutWithoutCheckDigit);
if (isValidNumber)
{
//If left component is a valid number,
//the displayed text in the control will correspond to NN.NNN.NNN-C pattern.
string checkDigit = this.Value.Substring(this.Value.Length - 1, 1);
this.Text = string.Join(ComponentSeparator,
string.Format(RutCulture, "{0:N0}", rutWithoutCheckDigit), checkDigit);
//If showThousandsSeparator is false, the text won't display the separator.
this.Text = showThousandsSeparator ? this.Text :
this.Text.Replace(GroupSeparator, string.Empty);
this.SelectionStart = this.Text.Length;
}
else
{
this.Text = string.Empty;
}
}
}
//Don't forget to subscribe again to TextChanged event handler
//after changing Text property.
this.TextChanged += this.RutBox_TextChanged;
}
因此,为了隐藏掩码,我们重写 OnGotFocus()
并调用基类方法和 UseMask()
方法。
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
this.UseMask();
}
为了显示掩码,我们重写 OnLostFocus()
并调用基类方法以及 UseMask()
方法。
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
this.UseMask();
}
另一方面,我们必须在文本更改时调用 UseMask()
。这是为了应对在代码中手动更改值的情况。
private void RutBox_TextChanged(object sender, EventArgs e)
{
this.UseMask();
}
对于来自键盘的用户输入,我们重写 OnPreviewTextInput()
并调用相关的基类方法(再次),并获取输入的字符。使用该字符,我们将验证它是否是数字、字母 'K
' 或控制字符,并涵盖其他情况。
protected override void OnPreviewTextInput(TextCompositionEventArgs e)
{
base.OnPreviewTextInput(e);
char characterFromText = Convert.ToChar(e.Text);
//Cancels the character if isn't a number, letter 'K' or a control character.
if (!char.IsDigit(characterFromText) &&
!char.Equals(char.ToUpper(characterFromText), 'K') && !char.IsControl(characterFromText))
{
e.Handled = true;
}
//Cancels the character if caret is not positioned at the end of text and is a letter 'K'.
else if (this.SelectionStart != this.Text.Length &&
char.Equals(char.ToUpper(characterFromText), 'K'))
{
e.Handled = true;
}
//Cancels the character if caret is positioned at the end of text
//and this contains 'K', and the key pressed is a number or letter 'K'.
else if (this.SelectionStart == this.Text.Length &&
this.Text.ToUpper().Contains("K") && (char.IsDigit(characterFromText) ||
char.Equals(char.ToUpper(characterFromText), 'K')))
{
e.Handled = true;
}
}
最后,如果用户将值粘贴到 RutBox
,我们首先检查它是否是 string
,然后验证该值是否符合 RUT 模式。如果有效,则将该值粘贴到 RutBox
,否则则不会粘贴。
private void RutBox_OnPaste(object sender, DataObjectPastingEventArgs e)
{
bool isText = e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true);
if (isText)
{
string rut = e.SourceDataObject.GetData(DataFormats.UnicodeText) as string;
e.CancelCommand();
rut = GetRutWithoutSeparators(rut);
if (Regex.IsMatch(rut, Pattern, RegexPatternOption))
{
this.Text = rut;
this.SelectionStart = this.Text.Length;
}
}
}
Using the Code
RutBox
的行为与其他 WPF 控件一样。您可以将其拖放到窗口上并设置您想要修改的属性。
属性
以下属性是控件特有的属性,可以被使用:
属性名称 | 类型 | 类别 | 描述 |
---|---|---|---|
IsValid | bool | Data | 使用模 11 指示该值是否为有效的智利 RUT。 |
ShowThousandsSeparator | bool | 外观 | 指示当控件失去焦点时是否显示千位分隔符。 |
值 | 字符串 | Data | 不带点或连字符的智利 RUT 的值。 |
历史
- 2020 年 2 月 9 日:版本 1.0