17

I have used the jquery validation plug in for a couple years now but this is my first effort to mixing with MVC 3 unobtrusive validation.

Information on the client side of things is scattered all over the internet and it's hard to find any thing that is deep enough to explain it to people that have not used it yet. I have been burning up Google for an hour now for an example on how to create a custom client side validator.

@Html.TextBoxFor(model => Model.CCPayment.CardNumber, new { @class = "textInput validateCreditCard", maxLength = "20" })

$(document).ready(function () {
      jQuery.validator.unobtrusive.adapters.add('validateCreditCard', {}, function (value, element) {
        alert('foo');
    });
});

if I run the above code in the bottom of my view it does absolutely nothing. I have even tried jquery.validator.addmethod() and still nothing. All the client side validation that is emitted from my model validation annotations works fine.

<div class="ctrlHolder">
                <label>
                    <em>*</em>
                    Card Number:
                </label>
                @Html.TextBoxFor(model => Model.CCPayment.CardNumber, new { @class = "textInput validateCreditCard", maxLength = "20" })

                <p class="field-validation-valid formHint" data-valmsg-for="CCPayment.CardNumber"></p>
             </div>
JBeckton
  • 7,095
  • 13
  • 51
  • 71
  • Did you include the script "jquery.validate.unobtrusive.js" in your page? – Chandu Jan 28 '11 at 21:48
  • yes... all the other client side validation works, in my model I have some annotations making fields required and they all work fine. I just want to add a custom validation method to the form validation. – JBeckton Jan 28 '11 at 22:03

2 Answers2

27

Here's how you could proceed. First you need to write a custom validator attribute in order to ensure that the validation is enforced on the server side. You could use the one described in this blog post:

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.";
    }

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

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

    private 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 bool IsValidNumber(string number)
    {
        int[] DELTAS = new int[] { 0, 1, 2, 3, 4, -4, -3, -2, -1, 0 };
        int checksum = 0;
        char[] chars = number.ToCharArray();
        for (int i = chars.Length - 1; i > -1; i--)
        {
            int j = ((int)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 = this.ErrorMessage,
            ValidationType = "creditcard"
        };
    }
}

Notice that I've modified it to make it implement the IClientValidatable interface and added the GetClientValidationRules method which simply uses the same error message for client validation as the server side and provides an unique name for this validator that will be used by the jquery unobtrusive adapter. Now all that's left is to decorate your model property with this attribute:

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

and in your view:

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script type="text/javascript">
    $(function () {
        jQuery.validator.unobtrusive.adapters.addBool('creditcard');
    });
</script>

@using (Html.BeginForm())
{
    @Html.TextBoxFor(x => x.CardNumber)
    @Html.ValidationMessageFor(x => x.CardNumber)
    <input type="submit" value="OK" />
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Darin, thanks for the detailed response. I am using IValidatableObject in my model rather than custom attributes does this mean that I cannot link server and client side validation unless i use custom attributes? – JBeckton Jan 28 '11 at 22:43
  • Can I not add the custom client validation with out linking it to the server side validation? – JBeckton Jan 28 '11 at 22:44
  • 1
    @JBeckton, you could but that would be extremely useless and an enormous security vulnerability. In order for this to work you will need to have the proper HTML5 `data-*` attributes emitted on your textbox for the adapter to work. You should perform your validation logic at least on the server. The client side validation is for cosmetic purposes only. – Darin Dimitrov Jan 28 '11 at 22:45
  • I always validate client side if anything but I was wondering why the client validation has to be linked to the server validation. As for emitting the data-* attributes the html helpers will not allow this behaviour as I have tried passing them in the html attributes obj but get errors. – JBeckton Jan 28 '11 at 23:45
  • I couldn't get this done in VB.Net. Any suggestion? I think the problem is in yield implementation. data-* are written in HTML but there is no client side validation. – mhmd Feb 04 '12 at 13:10
  • 3
    On the solution above, I didn't understand how client side validation can be done because there is no javascript code for checking whether the credit card number is valid or not. Does someone can explain me? – Bronzato Mar 24 '12 at 12:26
  • Where is the javascript? – David Dec 29 '13 at 17:47
  • 1
    @David, Bronzato, I hope you don't expect to write javascript that will validate whether a credit card is valid :-) Guys be more serious please :-) You could of course do it but that would be insane. This validation should be performed on the server. – Darin Dimitrov Dec 29 '13 at 17:49
  • 2
    I agree that it should be run server side but it's not one or the other. Have the validation on the client side results in a much better user xp. But it has to run on the server side, totally agree with that. – David Jan 02 '14 at 11:26
2

You need ValidateFor in addition to TextBoxFor. I can't tell from your question if you've already done that or not. You also need EnableClientValidation before the form.

Craig Stuntz
  • 125,891
  • 12
  • 252
  • 273
  • EnableClientValidation is set in my web.config, I removed the ValidatorFor helpers because they are not needed with the new mvc 3 unobtrusive validation. instead all you need is an html element with a class of field-validation-valid – JBeckton Jan 28 '11 at 22:16
  • 1
    If you don't have `ValidateFor`, then you won't see server-side validation messages. In MVC 2, client-side validation won't work without them, either. – Craig Stuntz Jan 28 '11 at 22:19
  • ok I gotcha, if js is disabled I won't see the validation messages. With is enabled i see them but as client side val messages. – JBeckton Jan 28 '11 at 22:39
  • You might also have a few validations which are server-only (e.g., those which require DB access). – Craig Stuntz Jan 28 '11 at 22:41