Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

A WPF Text Control with Chilean RUT Mask

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
10 Feb 2020CPOL6 min read 5.6K   106   4   2
A Windows Presentation Foundation TextBox control with chilean tax ID mask, and its validation through modulus 11 algorithm.
Given that RUT format responds to pattern NN.NNN.NNN-C, and I wished to prevent the user from putting any value that didn't correspond; I decided to create my first custom control from a TextBox to accept only the allowed values, besides giving the RUT mask automatically. This post shows how to create the control, define properties, handle behavior of the control, and finally tell us how to use the code.

Introduction

Since the past few months, I'm working on a desktop app that requires, in various forms, the chilean tax ID (known as RUT) of customers and suppliers as a user input. Given that RUT format responds to pattern NN.NNN.NNN-C, and I wished to prevent the user from putting any value that didn't correspond; I decided to create my first custom control from a TextBox to accept only the allowed values, besides giving the RUT mask automatically.

Background

RUT (Rol Único Tributario) is the unique number utilized as tax ID in Chile. This is a 7 to 8 digits number, plus an extra digit (can be a number from 0 to 9, or letter 'K'), that corresponds to check digit, which is obtained through modulus 11. RUT is usually written with an hyphen that splits the 7 to 8 length number (left side), from check digit (right side).

Modulus 11

Modulus 11 is a mathematical algorithm for check data integrity in a sequence of numbers. This algorithm returns a value between 0 and 11, which is called check digit, and it is used to validate the sequence. Usually, the last digit of a sequence in an identification number (like RUT) is the check digit.

The steps to calculate the check digit through modulus 11 are the following:

  1. Get the sequence of numbers without the check digit and reverse it.
    18798442 -> 24489781
  2. Multiply each digit of the reversed number using the following factor pattern: 2, 3, 4, 5, 6, 7. If the sequence is longer than six digits, repeat the pattern.
    Value 2 4 4 8 9 7 8 1
    Factor x2 x3 x4 x5 x6 x7 x2 x3
    Result =4 =12 =16 =40 =54 =49 =16 =3
  3. Sum the products obtained in the last step (2).
    4 + 12 + 16 + 40 + 54 +49 + 16 + 3  = 194
  4. Get the division remainder between 1) the result of the sum in step 3, and 2) 11.
    194 % 11 = 7
  5. To 11, subtract the remainder of step 4. The result of subtraction is the check digit.
    11 - 7 = 4
  6. Extra step for chilean RUT: If the result of subtraction is 10, the check digit value is 'K', and if is 11, the value is 0.

Creating the Control

First, I created a new control that inherits from TextBox, which I decided to call RutBox:

C#
public class RutBox : TextBox
{

}

So, this control has the same properties of TextBox. After that, I defined the fields I'd use in the code, starting to declare a constant string containing the hyphen used as separator between number and check digit:

C#
private const string ComponentSeparator = "-";

Also, a string to set the culture name to use later with the CultureInfo class:

C#
private const string CultureName = "es-CL";

The minimum and maximum length allowed for RUT (including the check digit):

C#
private const int MaxLengthAllowed = 9;

private const int MinLengthAllowed = 8;

The pattern of RUT and Regex option of ignore case (considering the letter 'K'):

C#
private const string Pattern = @"^[0-9]+K?$";

private const RegexOptions RegexPatternOption = RegexOptions.IgnoreCase;

On the other hand, I've declared two readonly fields: one to get the CultureInfo, and another to get the thousands separator from RutCulture and use it to format the number component.

C#
private readonly CultureInfo RutCulture;

private readonly string GroupSeparator;

Finally, I declared a field to display or hide the thousands separator:

C#
private bool showThousandsSeparator;

Defining Properties

The first property I've created was Value, which gets the text of TextBox and calls GetRutWithoutSeparators() to remove the group separator and component separator from the input. Also sets the value only if it matches with the pattern, otherwise returns an exception (by the way, if the value is null, it changes to an empty string).

C#
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;
        }
    }
}

Defined here is the GetRutWithoutSeparators() method, whose only parameter is a string that contains RUT with separators.

C#
private string GetRutWithoutSeparators(string rutWitSeparators)
{
    rutWitSeparators = rutWitSeparators.Replace(GroupSeparator, string.Empty).Replace
                       (ComponentSeparator, string.Empty);
    return rutWitSeparators;
}

The other property is IsValid. This one returns true if the value is between the minimum and maximum allowed length, and meets with the pattern of RUT, otherwise will be false. For validating the string, it is necessary to split the RUT in two parts: the left side, with the number, and right side, with the check digit; then get the check digit using GetModulus11CheckDigit() and compare it with the check digit obtained splitting the RUT.

C#
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;
        }
    }
}

The GetModulus11CheckDigit() method needs an integer value to apply the algorithm, returning a string that represents the check digit, and replacing it with 0 if is 11, or 'K' if is 10:

C#
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();
    }
}

The last property created is ShowThousandsSeparator, giving the possibility to display or hide the group separator:

C#
public bool ShowThousandsSeparator
{
    get
    {
        return showThousandsSeparator;
    }
    set
    {
        showThousandsSeparator = value;
        UseMask();
    }
}

Handling Behavior of the Control

Now it's time to think how the control is going to behave when:

  1. Show the mask when the control has the focus and hide it when it loses focus
  2. User enters a value typing in the keyboard
  3. User enters a value pasting it

First, we must define the constructor, where we add a handler for pasting and text changed events, the CharacterCasing property is defined like uppercase by default, the MaxLength property is the maximum allowed for RUT, RutCulture member is initialized with "es-CL" value, showThousandsSeparator is set to true, and the Value property is set to empty string.

C#
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;
}

For the mask, I created a method called UseMask(), where it checks if RutBox has the focus. If IsFocused is false, it tries to apply the format to the text. It's important to unsubscribe the TextChanged event while making the changes, or when we assign a new value to Text property, the event handler will be called.

C#
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;
}

So, to hide the mask, we override OnGotFocus() and call the base method and UseMask() method.

C#
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    this.UseMask();
}

And, to show the mask, we override OnLostFocus() and call the base method plus UseMask() method.

C#
protected override void OnLostFocus(RoutedEventArgs e)
{
    base.OnLostFocus(e);
    this.UseMask();
}

On the other hand, we must call UseMask() when the text changes. This is to cover the case where the value is changed manually in the code.

C#
private void RutBox_TextChanged(object sender, EventArgs e)
{
    this.UseMask();
}

For user input from keyboard, we override OnPreviewTextInput() and call the base method associated (again), and get the character entered. With the character, we'll validate if it is a number, the letter 'K' or a control character, and covering other cases.

C#
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;
    }
}

Finally, if the user pastes the value to RutBox, we check in the first instance if is a string, and then verify if the value matches with the RUT pattern. If it is valid, the value will be pasted to RutBox, but if is not, it won't be pasted.

C#
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 behaves as any other WPF control. You can drop it on a window and set the properties you want to modify.

Properties

The following properties are the control-specific properties that can be used:

Property Name Type Category Description
IsValid bool Data Indicates whether the value is a valid chilean RUT using modulus 11.
ShowThousandsSeparator bool Appearance Indicates whether the thousands separator will be displayed in the control when this loses the focus.
Value string Data Value of chilean RUT without dots or hyphen.

History

  • 9th February, 2020: Version 1.0

License

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


Written By
Engineer
Chile Chile
Graduated as Industrial Engineer (2019)
University of Santiago, Chile (USACH).

I started programming in 2013, using Python in first instance, but my enthusiasm for programming increased in 2015 with C#, using Windows Forms, and modelling relational databases with MS Access and MariaDB DBMS.

Comments and Discussions

 
QuestionNo need for custom control, the main functionality of the main control stays the same Pin
Издислав Издиславов20-Feb-20 13:24
Издислав Издиславов20-Feb-20 13:24 
QuestionRegex? Pin
Member 244330610-Feb-20 10:27
Member 244330610-Feb-20 10:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.