65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2020 年 2 月 9 日

CPOL

6分钟阅读

viewsIcon

6992

downloadIcon

174

一个 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 计算校验位的步骤如下:

  1. 获取不包含校验位的数字序列并将其反转。
    18798442 -> 24489781
  2. 使用以下因子模式将反转后的数字的每一位进行乘法运算: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
  3. 将上一步(2)中得到的乘积相加。
    4 + 12 + 16 + 40 + 54 +49 + 16 + 3  = 194
  4. 将 1) 步骤 3 中得到的总和结果除以 11,得到余数。
    194 % 11 = 7
  5. 从 11 中减去步骤 4 的余数。减法的结果即为校验位。
    11 - 7 = 4
  6. 智利 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();
    }
}

处理控件的行为

现在是时候考虑控件在以下情况下将如何表现了:

  1. 当控件获得焦点时显示掩码,当失去焦点时隐藏掩码。
  2. 用户通过键盘输入值。
  3. 用户通过粘贴输入值。

首先,我们必须定义构造函数,在其中为粘贴和文本更改事件添加处理程序,默认将 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 是否获得了焦点。如果 IsFocusedfalse,它会尝试将格式应用于文本。重要的是在进行更改时取消订阅 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
© . All rights reserved.