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:
- Get the sequence of numbers without the check digit and reverse it.
18798442 -> 24489781
- 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 |
- Sum the products obtained in the last step (2).
4 + 12 + 16 + 40 + 54 +49 + 16 + 3 = 194
- Get the division remainder between 1) the result of the sum in step 3, and 2) 11.
194 % 11 = 7
- To 11, subtract the remainder of step 4. The result of subtraction is the check digit.
11 - 7 = 4
- 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
:
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:
private const string ComponentSeparator = "-";
Also, a string
to set the culture name to use later with the CultureInfo
class:
private const string CultureName = "es-CL";
The minimum and maximum length allowed for RUT (including the check digit):
private const int MaxLengthAllowed = 9;
private const int MinLengthAllowed = 8;
The pattern of RUT and Regex option of ignore case (considering the letter 'K
'):
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.
private readonly CultureInfo RutCulture;
private readonly string GroupSeparator;
Finally, I declared a field to display or hide the thousands separator:
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
).
public string Value
{
get
{
return GetRutWithoutSeparators(this.Text);
}
set
{
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.
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.
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
:
private string GetModulus11CheckDigit(long number)
{
long sum = 0;
int multiplier = 2;
while (number != 0)
{
multiplier = multiplier > 7 ? 2 : multiplier;
sum += (number % 10) * multiplier;
number /= 10;
multiplier++;
}
sum = 11 - (sum % 11);
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:
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:
- Show the mask when the control has the focus and hide it when it loses focus
- User enters a value typing in the keyboard
- 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.
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.
private void UseMask()
{
this.TextChanged -= this.RutBox_TextChanged;
if (this.IsFocused)
{
this.Text = this.Value;
}
else
{
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)
{
string checkDigit = this.Value.Substring(this.Value.Length - 1, 1);
this.Text = string.Join(ComponentSeparator,
string.Format(RutCulture, "{0:N0}", rutWithoutCheckDigit), checkDigit);
this.Text = showThousandsSeparator ? this.Text :
this.Text.Replace(GroupSeparator, string.Empty);
this.SelectionStart = this.Text.Length;
}
else
{
this.Text = string.Empty;
}
}
}
this.TextChanged += this.RutBox_TextChanged;
}
So, to hide the mask, we override OnGotFocus()
and call the base method and UseMask()
method.
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.
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.
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.
protected override void OnPreviewTextInput(TextCompositionEventArgs e)
{
base.OnPreviewTextInput(e);
char characterFromText = Convert.ToChar(e.Text);
if (!char.IsDigit(characterFromText) &&
!char.Equals(char.ToUpper(characterFromText), 'K') && !char.IsControl(characterFromText))
{
e.Handled = true;
}
else if (this.SelectionStart != this.Text.Length &&
char.Equals(char.ToUpper(characterFromText), 'K'))
{
e.Handled = true;
}
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.
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