Custom HtmlHelper for filtering the input in a text box

The MVC framework contains some helper methods that can be used inside views to programmatically generate basic HTML elements like text boxes, text areas, check boxes and radio buttons. The class that contains those helper methods is called HtmlHelper.

Filtered text box

All the methods of this class return a string that contains the HTML markup to render. Inside a view we can use the HtmlHelper methods by using the Razor syntax @Html. In this way we are accessing the Html property of the view which is an instance of the HtmlHelper class. For example, if we want to render a text box with the name “textboxId” we will write in our view:

@Html.TextBox("textboxId")

The HTML that will be rendered is:

<input id="textboxId" type="text" name="textboxId" value="" />

In addition to the default HtmlHelper methods we can create our own custom methods that generate HTML markup and “attach” them to the HtmlHelper class using extension methods.

As an example we will create a helper method for rendering a filtered text box that allows the user to enter only a given set of characters. If the user enters a character outside the allowed range then, that character will be filtered out and it will not appear in the text box. The control will allow us to specify six types of filters:

  1. AlphabeticCharacter – allows only alphabetic characters from a to z, A to Z and space characters,
  2. Numbers – allows only numbers,
  3. DecimalNumbers – allows numbers and the decimal separator as defined by the current culture info,
  4. DecimalNumbersWithGroupSeparator – allows numbers, the decimal separator and the currency group separator as defined by the current culture info,
  5. CustomValidCharacters – for this filter type we will define a string that contains all the characters that will be allowed in the text box,
  6. CustomInvalidCharacters – for this filter type we will define a string that contains all the characters that aren’t allowed in the text box.

Just to give a famous example, our filtered text box will offer a functionality similar to the FilteredTextBox control contained in the AjaxControlToolkit but the implementation, obviously, has nothing to do with the AjaxControlToolkit.

Now, let’s start writing the code. First we will create a new class called FilteredTextBox that will take care of the HTML rendering. Then we will add extension methods for the HtmlHelper class that will instantiate our new class and provide different filtering types. Because our class must render HTML markup we will have to make it implement the IHtmlString interface. This interface contains only one method ToHtmlString and it is used to tell the MVC framework that the class generates an HTML string that doesn’t need to be encoded. If we don’t specify this interface then, when the output will be rendered in the view, it will be HTML encoded automatically so, in our page, we will see HTML code instead of HTML elements.

To define the six types of supported filters I created an enumeration called FilterType. Here is the complete code of the helper class including the FilterType enumeration:

using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;

namespace Helpers
{
    public class FilteredTextBox : IHtmlString
    {
        #region Filter types

        public enum FilterType
        {
            /// <summary>
            /// Allows only alphabetic characters from a to z and A to Z and space characters.
            /// </summary>
            AlphabeticCharacter,

            /// <summary>
            /// Allows only numbers.
            /// </summary>
            Numbers,

            /// <summary>
            /// Allows numbers and the decimal separator as defined by the current culture info.
            /// </summary>
            DecimalNumbers,

            /// <summary>
            /// Allows numbers, the decimal separator and the currency group separator as defined by the current culture info.
            /// </summary>
            DecimalNumbersWithGroupSeparator,

            /// <summary>
            /// Allows all the characters specified in the ValidCharacters field.
            /// </summary>
            CustomValidCharacters,

            /// <summary>
            /// Allows all the characters except the ones specified in the InvalidCharacters field.
            /// </summary>
            CustomInvalidCharacters
        }

        #endregion

        #region Properties

        public HtmlHelper HtmlHelper { get; set; }

        public FilterType Type { get; set; }

        public string Name { get; set; }

        public object Value { get; set; }

        public string ValidCharacters { get; set; }

        public string InvalidCharacters { get; set; }

        public IDictionary<string, object> HtmlAttributes { get; set; }

        #endregion

        #region Implementation of IHtmlString

        /// <summary>
        /// Returns an HTML-encoded string.
        /// </summary>
        /// <returns>
        /// An HTML-encoded string.
        /// </returns>
        public string ToHtmlString()
        {
            return Render();
        }

        #endregion

        #region Constructors

        internal FilteredTextBox(HtmlHelper htmlHelper, string name, FilterType type, string validCharacters, string invalidCharacters)
        {
            HtmlHelper = htmlHelper;
            Name = name;
            Type = type;
            ValidCharacters = validCharacters;
            InvalidCharacters = invalidCharacters;
        }

        #endregion

        #region Fluent configuration

        /// <summary>
        /// Set the value of the text box.
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public FilteredTextBox SetValue(object value)
        {
            Value = value;
            return this;
        }

        /// <summary>
        /// Set the dictionary that contains the HTML attributes to set for the element.
        /// </summary>
        /// <param name="htmlAttributes"></param>
        /// <returns></returns>
        public FilteredTextBox SetHtmlAttributes(IDictionary<string, object> htmlAttributes)
        {
            HtmlAttributes = htmlAttributes;
            return this;
        }

        #endregion

        #region Methods

        public override string ToString()
        {
            return ToHtmlString();
        }

        private string Render()
        {
            var tagBuilder = new TagBuilder("input");

            if (HtmlAttributes != null)
                tagBuilder.MergeAttributes(HtmlAttributes);

            tagBuilder.MergeAttribute("type", "text");

            if (!string.IsNullOrEmpty(Name))
                tagBuilder.MergeAttribute("name", Name, true);

            var value = Value == null ? string.Empty : Value.ToString();
            var regex = string.Empty;
            switch (Type)
            {
                case FilterType.AlphabeticCharacter:
                    regex = "[^a-zA-Z ]";
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.Numbers:
                    regex = "[^0-9]";
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.DecimalNumbers:
                    regex = string.Format("[^0-9{0}]", Regex.Escape(System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator));
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.DecimalNumbersWithGroupSeparator:
                    regex = string.Format("[^0-9{0}{1}]", Regex.Escape(System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator), Regex.Escape(System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberGroupSeparator));
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.CustomValidCharacters:
                    regex = string.Format("[^{0}]", Regex.Escape(ValidCharacters));
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.CustomInvalidCharacters:
                    /*
                    regex = string.Join("|", InvalidCharacters.ToCharArray().Select(c => Regex.Escape(c.ToString(CultureInfo.InvariantCulture))));
                    replaced with the next line thanks to Siderite - http://siderite.blogspot.com/
                    */
                    regex = string.Format("[{0}]", Regex.Escape(InvalidCharacters));
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
            }

            tagBuilder.MergeAttribute("data-filter", "1");
            tagBuilder.MergeAttribute("value", value, true);
            tagBuilder.MergeAttribute("data-regex", regex, true);

            if (!string.IsNullOrEmpty(Name))
            {
                // If there are any errors for a named field, we add the css attribute.
                ModelState modelState;
                if (HtmlHelper.ViewData.ModelState.TryGetValue(Name, out modelState))
                {
                    if (modelState.Errors.Count > 0)
                    {
                        tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
                    }
                }
            }

            return tagBuilder.ToString(TagRenderMode.SelfClosing);
        }

        #endregion
    }
}

Because we want to call our class using the usual Razor expression @Html we will have to create extension methods that will invoke our new class:

using System;
using System.Collections.Generic;
using System.Web.Mvc;

namespace Helpers
{
    public static class HtmlExtensions
    {
        #region AlphabeticCharactersTextBox

        /// <summary>
        /// Returns a text input element that allows only alphabetic characters from a to z and A to Z and space characters.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field.</param>
        /// <returns></returns>
        public static FilteredTextBox AlphabeticCharactersTextBox(this HtmlHelper htmlHelper, string name)
        {
            return new FilteredTextBox(htmlHelper, name, FilteredTextBox.FilterType.AlphabeticCharacter, string.Empty, string.Empty);
        }

        /// <summary>
        /// Returns a text input element that allows only alphabetic characters from a to z and A to Z and space characters.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field.</param>
        /// <param name="value">The value of the input element.</param>
        /// <returns></returns>
        public static FilteredTextBox AlphabeticCharactersTextBox(this HtmlHelper htmlHelper, string name, object value)
        {
            return new FilteredTextBox(htmlHelper, name, FilteredTextBox.FilterType.AlphabeticCharacter, string.Empty, string.Empty).SetValue(value);
        }

        /// <summary>
        /// Returns a text input element that allows only alphabetic characters from a to z and A to Z and space characters.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field.</param>
        /// <param name="value">The value of the input element.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <returns></returns>
        public static FilteredTextBox AlphabeticCharactersTextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
        {
            return new FilteredTextBox(htmlHelper, name, FilteredTextBox.FilterType.AlphabeticCharacter, string.Empty, string.Empty).SetValue(value).SetHtmlAttributes(htmlAttributes);
        }

        #endregion

		//The rest of the code was omitted for brevity but you can download the complete source code from Codeplex: 
		//https://mvcfilteredtextbox.codeplex.com/downloads/get/843504
    }
}

This is enough for rendering the HTML code. You can see, in the Render method, that each generated HTML input element contains also an attribute called ‘data-regex’. In the ‘data-regex’ attribute we are storing a regular expression that can select the characters that are not allowed. For example, for the numeric filter, the regular expression that excludes all characters that are not numbers is [^0-9].

Now, the only thing that remains to be done before using the new helper methods is to add some client side code to filter the typed characters. We will write this JavaScript code in an external file and we will include a link to it in our page.

$(document).ready(function () {
    HtmlHelpers.Init();
});

var HtmlHelpers = {
    Init: function () {
        $("input[data-filter='1']").keypress(HtmlHelpers.FilteredTextBox.CheckValue);
    }
};

HtmlHelpers.FilteredTextBox = {
    CheckValue: function (event) {
        var code = event.keyCode || event.which;

        if (event.ctrlKey || event.metaKey || event.altKey || code == 0
            //Text editing characters
            || code ==	33 //Page Up
            || code ==	34 //Page Down
            || code ==	37 //Left arrow
            || code ==	38 //Up arrow
            || code ==	39 //Right arrow
            || code ==	40 //Down arrow            
            || code == 8 //Backspace
            || code == 36 //Home
            || code == 35 //End
            || code == 45 //Insert
            || code == 46 //Delete
            ) {
            return;
        }

        var value = String.fromCharCode(event.which);
        var regex = new RegExp($(this).attr("data-regex"), "");
        
        if (regex.test(value)) {
            event.preventDefault();
        }
    }
};

Now we are able to use our new class inside the view. For example:

@using Helpers

Allows only alphabetic characters
<br/>        
@Html.AlphabeticCharactersTextBox("tbAlphabeticCharacters", string.Empty, new Dictionary<string, object>{{"class", "testCssClass"}})
<br/>
Allows only numbers
<br/>
@Html.NumbersTextBox("tbNumbers").SetValue("123ABC").SetHtmlAttributes(new Dictionary<string, object>{ { "style", "background-color: green;" } })
<br/>
Allows numbers and the decimal separator as defined by the current culture info
<br/>
@Html.DecimalNumbersTextBox("tbDecimalNumbers")
<br/>
Allows numbers, the decimal separator and the currency group separator as defined by the current culture info
<br/>
@Html.DecimalNumbersWithGroupSeparatorTextBox("tbDecimalNumbersWithGroups")
<br/>
Allows the following characters: abcd\
<br/>
@Html.ValidCharactersFilteredTextBox("tbValidCharacters", "abcd\\")
<br/>
Allows all characters except: abcd\
<br/>
@Html.InvalidCharactersFilteredTextBox("tbInvalidCharacters", "abcd\\") 

The result of this view is:

Filtered text boxes example

Note 1: If the FilteredTextBox wouldn’t be implementing the IHtmlString interface than the rendered HTML would be automatically encoded and, when running the site, we would see something like in the following image:

Filtered text boxes example without IHtmlString

Note 2: Another thing to notice is that we created our class so that it allows method chaining:

@Html.NumbersTextBox("tbNumbers").SetValue("123ABC").SetHtmlAttributes(new Dictionary<string, object>{ { "style", "background-color: green;" } })

The method chaining is possible because the SetValue method returns the current object. This improves the readability of the code and is a good idea when writing HtmlHelpers that will be used massively. In the literature this is called a fluent approach and, if you like to read more about this, here are some links on the topic:

Stronly Typed Helpers

As you probably already noticed, there are helper methods that allow us to use lambda expressions to specify both the name of the element and the value to render. For example Html.TextBoxFor provides such a functionality. This kind of HtmlHelper methods are called strongly typed helpers and they follow the naming convention Html.HelperNameFor.

Our next goal is to create some strongly typed helper methods for our FilteredTextBox class. For this we will create new extension methods inside the static class FilteredTextBoxHelper. For example, for the alphabetic characters filtering the strongly typed helper method will be:

/// <summary>
/// Returns a text input element that allows only alphabetic characters from a to z and A to Z and space characters.
/// </summary>
/// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
/// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
/// <returns></returns>
public static FilteredTextBox AlphabeticCharactersTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression)
{
	var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);

	var text = metadata.Model;
	var name = ExpressionHelper.GetExpressionText(expression);
	return (new FilteredTextBox(htmlHelper, name, FilteredTextBox.FilterType.AlphabeticCharacter, string.Empty, string.Empty)).SetValue(text);
}

Now we will be able to write in our view something like this:

@Html.AlphabeticCharactersTextBoxFor(model => model.PropertyName)

In a similar way we can write strongly typed extension methods also for the other supported filter types. I will not write here the implementation because it’s similar to the AlphabeticCharactersTextBoxFor method but you can find them, together with the complete source code for the FilteredTextBox and FilteredTextBoxHelper classes on CodePlex: click here to download the source code.

Advertisements

6 comments on “Custom HtmlHelper for filtering the input in a text box

    • Busted! There’s no good reason for doing that, thanks for pointing that out. I just edited the post and made the correction 😀

      case FilterType.CustomInvalidCharacters:
      /*
      regex = string.Join(“|”, InvalidCharacters.ToCharArray().Select(c => Regex.Escape(c.ToString(CultureInfo.InvariantCulture))));
      replaced with the next line thanks to Siderite – http://siderite.blogspot.com/
      */
      regex = string.Format(“[{0}]”, Regex.Escape(InvalidCharacters));
      value = Regex.Replace(value, regex, string.Empty);
      break;

    • Hi Shuppy, thanks for your feedback! I updated the class and posted the source code on Codeplex so that now you can pass the CSS class in the following 2 ways:
      @Html.AlphabeticCharactersTextBox("tbAlphabeticCharacters", string.Empty, new Dictionary{{"class", "CssClassName"}})
      or
      @Html.NumbersTextBox("tbNumbers").SetHtmlAttributes(new Dictionary{ { "class", "CssClassName" } })

  1. Hi,
    There is a bug, if you fix that, it will be an awesum stuff.,..Backspace key or shift+home key or any text modifying combinations r nt working…….Except mouse, i cant edit the entered text or delete it

    • Hi Shan, thanks for writing, there is indeed a bug. Please try to edit the content of the file /Resources/js/scripts.js and replace all the code with the following one:

      $(document).ready(function () {
          HtmlHelpers.Init();
      });
      
      var HtmlHelpers = {
          Init: function () {
              $("input[data-filter='1']").keypress(HtmlHelpers.FilteredTextBox.CheckValue);
          }
      };
      
      HtmlHelpers.FilteredTextBox = {
          CheckValue: function (event) {
              var code = event.keyCode || event.which;
      
              if (event.ctrlKey || event.metaKey || event.altKey || code == 0
                  //Text editing characters
                  || code ==	33 //Page Up
                  || code ==	34 //Page Down
                  || code ==	37 //Left arrow
                  || code ==	38 //Up arrow
                  || code ==	39 //Right arrow
                  || code ==	40 //Down arrow            
                  || code == 8 //Backspace
                  || code == 36 //Home
                  || code == 35 //End
                  || code == 45 //Insert
                  || code == 46 //Delete
                  ) {
                  return;
              }
      
              var value = String.fromCharCode(event.which);
              var regex = new RegExp($(this).attr("data-regex"), "");
              
              if (regex.test(value)) {
                  event.preventDefault();
              }
          }
      };
      

      Please let me know if you notice other issues.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s