1

I want to add a custom validation attribute to my model to check if any of the answers contain duplicates. That is, if the user types in the same answer for any of the fields, I want to display an error when they type in a duplicate answer.

Here's my model:

public class SecurityQuestions
{
    public int Question1Id { get; set; }
    public int Question2Id { get; set; }
    public int Question3Id { get; set; }
    public int Question4Id { get; set; }
    public int Question5Id { get; set; }
    public int Question6Id { get; set; }

    [UniqueAnswersOnly]
    public string Answer1 { get; set; }
    [UniqueAnswersOnly]
    public string Answer2 { get; set; }
    [UniqueAnswersOnly]
    public string Answer3 { get; set; }
    [UniqueAnswersOnly]
    public string Answer4 { get; set; }
    [UniqueAnswersOnly]
    public string Answer5 { get; set; }
    [UniqueAnswersOnly]
    public string Answer6 { get; set; }
}

Here's my attempt at custom attribute:

public class UniqueAnswersOnly: ValidationAttribute, IClientValidatable
    {

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            //Get a list of all properties that are marked with [UniqueAnswersOnly]
            var props = validationContext.ObjectInstance.GetType().GetProperties().Where(
                prop => Attribute.IsDefined(prop, typeof(UniqueAnswersOnly)));

            var values = new HashSet<string>();

            //Read the values of all other properties
            foreach(var prop in props)
            {
                var pValue = (string)prop.GetValue(validationContext.ObjectInstance, null);
                if (prop.Name!=validationContext.MemberName && !values.Contains(pValue))
                {
                    values.Add(pValue);
                }
            }

            if (values.Contains(value))
            {
                return new ValidationResult("Duplicate answer", new[] { validationContext.MemberName });
            }
            return null;
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule()
            {
                ErrorMessage = metadata.DisplayName + " is required!",
                ValidationType = "duplicateanswers"
            };

            yield return rule;
        }
    }

The problem I'm having now is the the validation is sucessful even though I enter in duplicate answers. I can still continue to next dialog (I am expecting validation to fail if duplicates are entered). I think it's because my custom attribute isn't being fired or hit because I added breakpoints but nothing is hit.

In my controller, I have if(ModelState.IsValid) { //continue to next dialog} and the model state does return valid.

Kala J
  • 2,040
  • 4
  • 45
  • 85
  • The method signature tells you what to return `ValidationResult`. Here's a similar [example](http://stackoverflow.com/questions/11959431/how-to-create-a-custom-validation-attribute) for a custom validation attribute. The problem is you'll need to know the property names and the `IValidateObject` answer from that question is probably a better approach in your case. – Jasen Jul 07 '15 at 17:41
  • Okay, my question is how do I return ValidateResult = false instead of success? I can return success but not false? Also, what parameters should I pass into my custom attribute? I want to pass in my model list, can I do that? – Kala J Jul 07 '15 at 17:44
  • If this is a one time usage I'd consider just adding your function to the controller or a service class and use its result in the action directly. – Jasen Jul 07 '15 at 17:44
  • Well yeah but I can't display qtips on client side that way with my current architecture. So I need to use custom attributes – Kala J Jul 07 '15 at 17:47
  • If successful you'd return null or `ValidationResult.Success`. If it fails your validation check return a new ValidationResult with the error message(s). – Jasen Jul 07 '15 at 17:49
  • Your validation attribute works if you submit the model and check `ModelState.IsValid`. – Jasen Jul 09 '15 at 05:41
  • It doesn't work on my end. Any ideas? – Kala J Jul 09 '15 at 16:33
  • I have no idea and it would be hard for anyone else to tell you. Make a new MVC project and add your attribute see if it works in a simplified environment. Check that your model is fully populated on post. Try removing all the logic so your validator forces failure. – Jasen Jul 09 '15 at 17:05

2 Answers2

2

You could create a custom validation attribute like this:

public class UniqueAnswersOnly : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        //Get a list of all properties that are marked with [UniqueAnswersOnly]
        var props = validationContext.ObjectInstance.GetType().GetProperties().Where(
            prop => Attribute.IsDefined(prop, typeof(UniqueAnswersOnly)));

        var values = new HashSet<string>();

        //Read the values of all other properties
        foreach(var prop in props)
        {
            var pValue = (string)prop.GetValue(validationContext.ObjectInstance);
            if (prop.Name!=validationContext.MemberName && !values.Contains(pValue))
            {
                values.Add(pValue);
            }
        }

        if (values.Contains(value))
        {
            return new ValidationResult("Duplicate answer", new[] { validationContext.MemberName });
        }
        return null;
    }
}

and here is a test case:

public class SecurityQuestions
{
    public int Question1Id { get; set; }
    public int Question2Id { get; set; }
    public int Question3Id { get; set; }
    public int Question4Id { get; set; }
    public int Question5Id { get; set; }
    public int Question6Id { get; set; }
    [UniqueAnswersOnly]
    public string Answer1 { get; set; }
    [UniqueAnswersOnly]
    public string Answer2 { get; set; }
    [UniqueAnswersOnly]
    public string Answer3 { get; set; }
    [UniqueAnswersOnly]
    public string Answer4 { get; set; }
    [UniqueAnswersOnly]
    public string Answer5 { get; set; }
    [UniqueAnswersOnly]
    public string Answer6 { get; set; }
}


class Program
{
    static void Main(string[] args)
    {
        var questions = new SecurityQuestions();
        questions.Answer1 = "Test";
        questions.Answer2 = "Test";
        questions.Answer3 = "Test3";
        questions.Answer4 = "Test4";
        questions.Answer5 = "Test5";
        questions.Answer6 = "Test6";

        var vc = new ValidationContext(questions, null, null);
        var results = new List<ValidationResult>();
        var validationResult = Validator.TryValidateObject(questions, vc, results, true);
    }
}

Edit:

I created a default MVC project and added your current code. That validation works just fine.

I added this to the home controller:

    public ActionResult AskQuestions()
    {
        var questions = new SecurityQuestions();

        return View(questions);
    }

    [HttpPost]
    public ActionResult CheckQuestions(SecurityQuestions questions)
    {
        if (ModelState.IsValid)
        {
            return View();
        }            
        else
        {
            return HttpNotFound();
        }
    }

And the AskQuestions view looks like this:

@model WebApplicationValidation.Models.SecurityQuestions

@{
    ViewBag.Title = "AskQuestions";
}

<h2>AskQuestions</h2>


@using (Html.BeginForm("CheckQuestions", "Home",FormMethod.Post))
{
    @Html.AntiForgeryToken()

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

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

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

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

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

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

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

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

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

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

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

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

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

If I run the app the ModelState.IsValid() is false if I enter two identical answers. And setting a breakpoint in the validation routine shows that it's being run.

Markus Luedin
  • 409
  • 2
  • 12
  • Okay, thank you so much! I'll give it a try but one thing I noticed is if I want to display custom error message, I want to add these parameters to [UniqueAnswersOnly], do I need to pass them to the constructor and handle them somehow? There are the parameters I want to addd to my attribute: ErrorMessageResourceName = "Validation_Error_DuplicateAnswers", ErrorMessageResourceType = typeof(Resources.Resources)) – Kala J Jul 07 '15 at 18:23
  • Opps, side note: I am using MVC 4 with .NET 4.0 in which GetValue needs 2 parameters not 1. – Kala J Jul 07 '15 at 18:24
  • Yes, you can create a constructor to supply the validator with parameters. public UniqueAnswersOnly(string resourceName) { } [UniqueAnswersOnly("Validation_Error_DuplicateAnswers")] public string Answer1 { get; set; } – Markus Luedin Jul 07 '15 at 18:32
  • also for the UniqueAnswersOnly, do I need to add a check to my controller action? That is, when I click on Submit, then I want the validation to kick in server side or is this validation already kicking in on submit? So, that I can't go to the next page without all the fields being unique first. – Kala J Jul 07 '15 at 18:34
  • I think all you have to do is add the attribute to your model, and MVC does all the checking for you. But I'm not a web developer, so not 100% sure... – Markus Luedin Jul 07 '15 at 18:42
  • Client-side validation will prevent the submission (from reaching your controller) but you should always perform the server-side validation check. – Jasen Jul 07 '15 at 18:45
  • Hey guys, I followed Markus's example but I was able to proceed without any error being thrown even though I had duplicate answers! I think I need to do a check in my controller but how do I do that with what I have from custom attribute? – Kala J Jul 07 '15 at 18:47
  • Interesting enough, when debugging, UniqueAnswersOnly custom attribute class never gets hit!! Even though I added them to my model! Did I need to do something else? – Kala J Jul 07 '15 at 18:51
  • Hard to say without seeing the code. There must be a fundamental problem, try to get a default attribute like [Required] working first. http://www.asp.net/mvc/overview/getting-started/introduction/adding-validation – Markus Luedin Jul 07 '15 at 19:01
  • Required works for me. I just can't get the custom attribute to work. I can get the built in attributes to work though. Is it because I need client side validation for the custom attribute? Or do I need to do something on the server side besides what I have done with this example? – Kala J Jul 07 '15 at 19:19
  • Hello, I updated my post to reflect what I have done so far. Thanks! – Kala J Jul 08 '15 at 19:38
  • I added some more details to my answer above. I think you have a problem with your MVC code, not the validation... – Markus Luedin Jul 11 '15 at 10:36
1

You can implement the IValidatableObject interface in your Model like below, and use jQuery to take care of the validation on the client side.

public class SecurityQuestions : IValidatableObject
{
    public int Question1Id { get; set; }

    public int Question2Id { get; set; }

    public int Question3Id { get; set; }

    public int Question4Id { get; set; }

    public int Question5Id { get; set; }

    public int Question6Id { get; set; }

    public string Answer1 { get; set; }

    public string Answer2 { get; set; }

    public string Answer3 { get; set; }

    public string Answer4 { get; set; }

    public string Answer5 { get; set; }

    public string Answer6 { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var securityAnswers = new List<string>();
        securityAnswers.Add(this.Answer1);
        securityAnswers.Add(this.Answer2);
        securityAnswers.Add(this.Answer3);
        securityAnswers.Add(this.Answer4);
        securityAnswers.Add(this.Answer5);
        securityAnswers.Add(this.Answer6);

        bool hasDuplicates = securityAnswers.GroupBy(x => x).Where(g => g.Count() > 1).Any();

        if (hasDuplicates)
        {
            yield return new ValidationResult(
                "There are duplicate Answers...");
        }
    }
}
ataravati
  • 8,891
  • 9
  • 57
  • 89