4

I have problem validating one field at client side, Here is my code:

Model:

 public class Registration
    {
        public int Id { get; set; }
        [IsUserExistAttribute(ErrorMessage = "Email already exists!")]
        [EmailAddress(ErrorMessage = "Please enter valid Email")]
        [Required(ErrorMessage = "Email is required")]
        public string Email
        {
            get; set;

        }
        [MustBeTrue(ErrorMessage = "Please Accept the Terms & Conditions")]
        public bool TermsAndConditions { get; set; }


    }

ValidationAttribute:

public class IsUserExistAttribute : ValidationAttribute,IClientValidatable
    {
        public override bool IsValid(object email)
        {
            DemoDbContext demoContext=new DemoDbContext();
            string emailString = Convert.ToString(email);
            if (demoContext.Registrations.Any(a=>a.Email.Contains(emailString)))
            {
                return false;
            }
            return true;
        }

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

View:

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Registration</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.TermsAndConditions, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <div class="checkbox">
                    @Html.EditorFor(model => model.TermsAndConditions)
                    @Html.ValidationMessageFor(model => model.TermsAndConditions, "", new { @class = "text-danger" })
                </div>
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

Everthing upto this works fine, But my question is This IsUserExistAttribute doesn't validate it on client side, on server side it validates and give the message like Email already exists!

But, I want it to validate client side too. So, what is the good way to achieve this? Other approach is also welcome :)

I tried something like this seems no success:

 $.validator.addMethod("emailvalidate", function (value, element) {
            var is_valid = false;
            $.ajax({
                // as before...
                async: false,
                success: function (data) {
                    is_valid = data === 'True';
                }
            });
            return is_valid;
        }, "Username not available.");
Just code
  • 13,553
  • 10
  • 51
  • 93
  • Why are you trying to reinvent the wheel? MVC comes with a `[Remote]` attribute for just this purpose. –  Feb 23 '16 at 08:57
  • @StephenMuecke that will create concurrency problem. If I use `[remote]` validation – Just code Feb 23 '16 at 08:58
  • Then you not doing it right. –  Feb 23 '16 at 09:00
  • What is your url for ajax call? Is it hitting the method? Also why not `TextBoxFor` instead of `EditorFor`. Are you using any separate editor? – ManojAnavatti Feb 23 '16 at 09:01
  • @StephenMuecke ok can you advice? – Just code Feb 23 '16 at 09:02
  • 1
    In order to have client side validation, you also needs a script for `$.validator.unobtrusive.adapters.add(...` but the end result is you will be doing exactly what the `RemoteAttribute` does. If using the `RemoteAttribute` was not working, then show the code you tried so it can be corrected –  Feb 23 '16 at 09:04
  • If input type = text is rendered one of data-* attribute should be decorated with `emailvalidate` function. – ManojAnavatti Feb 23 '16 at 09:04
  • Have a look at this BradWilson's post. It will explain you better.http://bradwilson.typepad.com/blog/2010/10/mvc3-unobtrusive-validation.html – ManojAnavatti Feb 23 '16 at 09:06
  • @StephenMuecke I have added one remote validation with actionresult method in controller it was working well at client side. but if I enter same email address in separate tabs and click on the submit at same time. it was inserting two values. that was the only problem with remote validation. – Just code Feb 23 '16 at 09:10
  • Then your POST method should also be doing the check immediately before you submit. The controller should have a (say) `private bool IsValid(string email)` method that includes the database code, and the `[Remote("IsEmailValid", "yourController")]` where the `public JsonResult IsEmailValid(string Email)` method calls the `IsValid()` method. Then in the POST method you can also call `IsValid()` if you want both client and server side validation –  Feb 23 '16 at 09:17
  • Thank you, that is what I should be doing. but, My question is can I achieve the same with model validations? Or I need to put at client and server side both? – Just code Feb 23 '16 at 09:19
  • Having said that, I suspect it would be very rare that the condition in your last comment would occur, so why not just catch the exception that will be thrown if 2 forms are posted with identical emails –  Feb 23 '16 at 09:20
  • Correct :) thank you for your time @StephenMuecke – Just code Feb 23 '16 at 09:22
  • Well your could, but then your repeating the code to access the database twice - once in your validation attribute, and again in controller method called by the ajax, breaking the DRY principal and making your code harder to maintain. –  Feb 23 '16 at 09:24
  • Yeah, but that is necessary to avoid such conflict – Just code Feb 23 '16 at 09:26
  • I would strongly recommend using your validation attribute and go with the standard method of handling this. A validation attribute should never access a database and you have made app impossible to unit test. –  Feb 23 '16 at 09:42
  • So, Ideal scenario should be validation on serverside like `IsValid()` on controller? @StephenMuecke – Just code Feb 23 '16 at 11:42
  • That would be the better solution. In the POST method, call the `private bool IsValid(string email)` method I noted above (which is also called by the `RemoteAttribute` for client side validation) and if `false`, add a `ModelState` error and return the view. –  Feb 23 '16 at 11:45
  • Makes sense @StephenMuecke Please add it as an answer to close this Question – Just code Feb 24 '16 at 04:26

1 Answers1

3

MVC already comes with a RemoteAttribute for client side validation (refer How to: Implement Remote Validation in ASP.NET MVC) so there is little point in you trying to reinvent the wheel. (As a side note, your currently missing the $.validator.unobtrusive.adapters.add() method necessary to add the rules for client side validation).

But there are a few issues with your current validation attribute. A validation attribute should not be accessing the database and you have made it impossible to unit test your code. And you will be repeating the code in the attribute again in the controller method that is called by the ajax function, violating the DRY principal and making it harder to maintain your code.

Instead, create a private method in your controller (or a public method in a separate service) for you database code, for example

private bool IsEmailUnique(string email)
{
    return !db.Registrations.Any(a => a.Email.Contains(email)))
}

Then for client side validation, decorate you property with the RemoteAttribute

[Remote("IsEmailValid", "ControllerName", ErorMessage = "...")]
public string Email { get; set; }

and add the controller method

public JsonResult IsEmailValid(string email)
{
    bool isValid = IsEmailUnique(email);
    return Json(isValid, JsonRequestBehavior.AllowGet);
}

and then if you also want to have server side validation when you submit the form, call the same method and if not valid, add a ModelStateError

public ActionResult Register(Registration model)
{
    bool isValid = IsEmailUnique(model.Email);
    if (!isValid)
    {
        ModelState.AddModelError("Email", "...");
    }
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    // save and redirect
}