MVC3 - Custom Validation Attributes for Client & Server Side Validation with Unobtrusive Ajax

18. April 2012 19:19 by viperguynaz in   //  Tags:   //   Comments (0)
Most MVC sites require custom validation that can't be handled with MVC3's out-of-the-box validation attributes. This article builds on the example posted in the previous article and shows you how to use custom validation attributes for client-side validation with unobtrusive Ajax to validate a credit card number.

This is a follow up to the post - MVC3 - Combining Client & Server Side Validation with Unobtrusive Ajax.  Most MVC sites require custom validation that can't be handled with MVC3's out-of-the-box validation attributes.  This article builds on the example posted in the previous article and shows you how to use custom validation attributes for client-side validation with unobtrusive Ajax to validate a credit card number. 

A shout out to Darin Dimitrov for providing the solution on StackOverflow!

Creating a custom validation attribute is straight forward, create a class that inherits ValidationAttribute and override the IsValid method with your own implementation.  However, in order to make the custom validation attribute work with client-side validation and unobtrusive ajax, your class also needs to implement IClientValidatable and add the GetClientValidationRules method which simply uses the same error message for client validation as the server side and provides a unique name for this validator that will be used by the jquery unobtrusive adapter.

I have extended the registration partial view in the previous article by adding a Credit Card field.  To validate a credit card number, we first add a custom validation attribute:

public class CreditCardAttribute : ValidationAttribute, IClientValidatable
    {
        private CardType _cardTypes;
        public CardType AcceptedCardTypes
        {
            get { return _cardTypes; }
            set { _cardTypes = value; }
        }

        public CreditCardAttribute()
        {
            _cardTypes = CardType.All;
        }

        public CreditCardAttribute(CardType acceptedCardTypes)
        {
            _cardTypes = acceptedCardTypes;
        }

        public override bool IsValid(object value)
        {
            var number = Convert.ToString(value);

            if (String.IsNullOrEmpty(number))
                return true;

            return IsValidType(number, _cardTypes) && IsValidNumber(number);
        }

        public override string FormatErrorMessage(string name)
        {
            return "The " + name + " field contains an invalid credit card number.";
        }

        [Flags]
        public enum CardType
        {
            Unknown = 1,
            Visa = 2,
            MasterCard = 4,
            Amex = 8,
            Diners = 16,

            All = Visa | MasterCard | Amex | Diners,
            AllOrUnknown = Unknown | Visa | MasterCard | Amex | Diners
        }

        private static bool IsValidType(string cardNumber, CardType cardType)
        {
            // Visa
            if (Regex.IsMatch(cardNumber, "^(4)")
                && ((cardType & CardType.Visa) != 0))
                return cardNumber.Length == 13 || cardNumber.Length == 16;

            // MasterCard
            if (Regex.IsMatch(cardNumber, "^(51|52|53|54|55)")
                && ((cardType & CardType.MasterCard) != 0))
                return cardNumber.Length == 16;

            // Amex
            if (Regex.IsMatch(cardNumber, "^(34|37)")
                && ((cardType & CardType.Amex) != 0))
                return cardNumber.Length == 15;

            // Diners
            if (Regex.IsMatch(cardNumber, "^(300|301|302|303|304|305|36|38)")
                && ((cardType & CardType.Diners) != 0))
                return cardNumber.Length == 14;

            //Unknown
            if ((cardType & CardType.Unknown) != 0)
                return true;

            return false;
        }

        private static bool IsValidNumber(string number)
        {
            var deltas = new[] { 0, 1, 2, 3, 4, -4, -3, -2, -1, 0 };
            var checksum = 0;
            var chars = number.ToCharArray();
            for (var i = chars.Length - 1; i > -1; i--)
            {
                var j = chars[i] - 48;
                checksum += j;
                if (((i - chars.Length) % 2) == 0)
                    checksum += deltas[j];
            }

            return ((checksum % 10) == 0);
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            yield return new ModelClientValidationRule
            {
                ErrorMessage = ErrorMessage,
                ValidationType = "creditcard"
            };
        }
    }

Next, add the CardNumber property to the account model and decorate it with the custom validation attribute - CreditCard and include a custom error message:

[Required]
[CreditCard(ErrorMessage = "Please enter a valid credit card number")]
[DataType(DataType.Text)]
[Display(Name = "CreditCard")]
public string CardNumber { get; set; }

Finally, add the field to the Register partial view:

<div class="editor-label">
    @Html.LabelFor(m => m.CardNumber)
</div>
<div class="editor-field">
     @Html.TextBoxFor(m => m.CardNumber)
     @Html.ValidationMessageFor(m => m.CardNumber)
</div>

Now, run the project, fill out all the registration fields except the CreditCard Number and click Submit.  You should see the following validation for required:
Credit Card Required

Next, type in "12345" and you should see the "Please enter a valid credit card number" error message as you are typing:
Invalid Credit Card number

Finally, type in a valid Credit Card number, like the Visa test number "4111111111111111" and the error message will disappear and submit should result in a successful post.

The complete solution is attached below. 

Cheers and happy coding!

Don

AjaxTest.zip (7.24 mb)

Pingbacks and trackbacks (1)+

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading