18

I am trying to conditionally validate the field within the MVC.NET Core. I have two radio buttons. If I select Yes (for the Ownership) I want to make a field below required (Activity dropdown)

However, no matter how hard I try, the value to be validated always comes from the Activity field, not from the Ownership field ("N\A" instead of "Yes")

Can somebody please tell me what I am doing wrong

The View (chtml)

<div class=" form-group">
    <div class="bisformdynamiclabel"></div>
    <br />
    @Html.RadioButtonFor(model => model.BIS232Request.JSONData.OwnershipActivity.Ownership, "Yes", new { id = "OwnershipAnswer_true", onclick = "displayOwnershipFieldsRow(true)" })
    <label for="OwnershipAnswer_true">Yes</label>
    @Html.RadioButtonFor(model => model.BIS232Request.JSONData.OwnershipActivity.Ownership, "No", new { id = "OwnershipAnswer_false", onclick = "displayOwnershipFieldsRow(false)" })
    <label for="OwnershipAnswer_false">No</label>
    <span class="alert-danger">
        @Html.ValidationMessage("OwnershipAnswer")
    </span>
</div>
<div class="row ownershipfieldsrow">
    <div class="col-xs-12 col-md-12">
        <div class=" form-group">
            <div class="bisformdynamiclabel"></div>
            <br />
            <input style="display:none" class="form-control" type="text" asp-for="BIS232Request.JSONData.OwnershipActivity.Activity" />
            <select class="form-control ownershipactivityselect" onchange="$('#BIS232Request_JSONData_OwnershipActivity_Activity').val($(this).val());  ">
                <option value="N/A">Please Select</option>
                <option value="Manufacturer">Manufacturer</option>
                <option value="Distributor">Distributor</option>
                <option value="Exporter">Exporter</option>
                <option value="Importer">Importer</option>
                <option value="Other">Other</option>
            </select>
            <span asp-validation-for="BIS232Request.JSONData.OwnershipActivity.Activity" class="alert-danger"></span>
            <span class="alert-danger">
                @Html.ValidationMessage("OwnershipAnswerActivity")
            </span>
        </div>
    </div>

The Model

[Required]
public string Ownership { get; set; }
[RequiredIf("Ownership", "OwnershipAnswer_true", "Activity is required if Ownership is selected")]
public string Activity { get; set; }        
public class RequiredIfAttribute : ValidationAttribute
{
    private String PropertyName { get; set; }
    private String ErrorMessage { get; set; }
    private Object DesiredValue { get; set; }

    public RequiredIfAttribute(String propertyName, Object desiredvalue, String errormessage)
    {
        this.PropertyName = propertyName;
        this.DesiredValue = desiredvalue;
        this.ErrorMessage = errormessage;
    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        Object instance = context.ObjectInstance;
        Type type = instance.GetType();
        Object proprtyvalue = type.GetProperty(PropertyName).GetValue(instance, null);
        if (proprtyvalue.ToString() == DesiredValue.ToString() && value == null)
        {
            return new ValidationResult(ErrorMessage);
        }
        return ValidationResult.Success;
    }
}
Kirk Larkin
  • 84,915
  • 16
  • 214
  • 203
James
  • 1,081
  • 4
  • 15
  • 34
  • SIde note Your `ValidationAttribute` should also implement `IClientModelValidator` if you want client side validation as well (refer [Model validation in ASP.NET Core MVC](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-2.1)) –  Sep 13 '18 at 22:13
  • Unrelated, but why are you using JS to set the value of a text box via a select list? You can just use `asp-for="BIS232Request.JSONData.OwnershipActivity.Activity"` directly on the select. – Chris Pratt Sep 14 '18 at 17:07

4 Answers4

16

Based on the original implementation I'd recommend extending RequiredAttribute rather than ValidationAttribute - then your default ErrorMessage and other defaults are set as per [Required]. Either way the "errormessage" property is redundant as you already have this as a property of ValidationAttribute and the original code generates a warning for the ErrorMessage property - you can also use nameof for the attribute decoration as well to keep things a lot tighter in your code:

My implementation is slightly more specific so that if a property is a bool I can indicate that a property is required (if say a checkbox is ticked):

[AttributeUsage(AttributeTargets.Property)]
public class RequiredIfTrueAttribute : RequiredAttribute
{
    private string PropertyName { get; set; }

    public RequiredIfTrueAttribute(string propertyName)
    {
        PropertyName = propertyName;
    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        object instance = context.ObjectInstance;
        Type type = instance.GetType();

        bool.TryParse(type.GetProperty(PropertyName).GetValue(instance)?.ToString(), out bool propertyValue);

        if (propertyValue && string.IsNullOrWhiteSpace(value?.ToString()))
        {
            return new ValidationResult(ErrorMessage);
        }

        return ValidationResult.Success;
    }
}

Example Usage:

public bool IsBusinessProfile { get; set; }

[RequiredIfTrue(nameof(IsBusinessProfile), ErrorMessage = "ABN is required for Business Profiles")]
public string Abn { get; set; }
Rob
  • 10,004
  • 5
  • 61
  • 91
  • 1
    This is exactly what I was looking for. When I ran it I got a null reference exception on `value` so I changed that line slightly to first check for null and then check the `ToString`: `if (propertyValue && (value == null || string.IsNullOrWhiteSpace(value.ToString())))` – FirstDivision Dec 12 '19 at 20:55
  • @FirstDivision thanks - I've edited that oversight - I believe (value?.ToString()) is the most succinct way to achieve this given IsNullOrWhiteSpace will cope with the conditional null (?) – Rob Dec 13 '19 at 03:59
  • Have you ever tested this? I don't think this works, as the ValidationContext is not the instance of the class where this attributed property is being validated, but rather the validated property itself. Thats what the official doc says about ValidationContext: `This class describes the type or member on which validation is performed.`. Therefore `type.GetProperty(PropertyName)` will return null in the above scenario. – Csharpest Dec 19 '19 at 13:37
  • @Csharpest sure have I'm using it in my current projects – Rob Dec 20 '19 at 02:34
  • Sorry to bother you, but can you show me a screenshot during runtime, where it shows, that ValidationContext is the actual model (parent of property) and not the attributed property itself? – Csharpest Dec 20 '19 at 07:36
  • @Csharpest - this is a pretty standard approach - https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-3.1 indicates in the "Custom Attributes" section that the ValidationContext should be your model as they are casting it to a "Movie" type using context.ObjectInstance. Are you looking at "context" or "context.ObjectInstance"? – Rob Jan 13 '20 at 11:39
  • I see, that's confusing. I just tried it again, but ObjectInstance still returns the property that has the custom attribute attached to. Why does context.ObjectInstance not hold the PageModel-Instance, where the actual property + its attribute are sitting in? – Csharpest Jan 13 '20 at 12:19
  • @Csharpest I suggest you create a new question with your code to get clarification on why this issue is occurring for you. – Rob Jan 13 '20 at 22:00
12

I built on the answer provided by Rob. This one is a generic validator instead of inheriting from Required, and also provides client-side validation. I am using .Net Core 3.0

using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System;
using System.Collections.Generic;
using System.Text;

namespace System.ComponentModel.DataAnnotations
{

    [AttributeUsage(AttributeTargets.Property)]
    public class RequiredIfTrueAttribute : ValidationAttribute, IClientModelValidator
    {
        private string PropertyName { get; set; }

        public RequiredIfTrueAttribute(string propertyName)
        {
            PropertyName = propertyName;
            ErrorMessage = "The {0} field is required."; //used if error message is not set on attribute itself
        }

        protected override ValidationResult IsValid(object value, ValidationContext context)
        {
            object instance = context.ObjectInstance;
            Type type = instance.GetType();

            bool.TryParse(type.GetProperty(PropertyName).GetValue(instance)?.ToString(), out bool propertyValue);

            if (propertyValue && (value == null || string.IsNullOrWhiteSpace(value.ToString())))
            {
                return new ValidationResult(ErrorMessage);
            }

            return ValidationResult.Success;
        }

        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
            MergeAttribute(context.Attributes, "data-val-requirediftrue", errorMessage);
        }

        private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
            attributes.Add(key, value);
            return true;
        }
    }
}

Client-Side Javascript

//Custom validation script for the RequiredIfTrue validator
/*
 * Note that, jQuery validation registers its rules before the DOM is loaded. 
 * If you try to register your adapter after the DOM is loaded, your rules will
 * not be processed. So wrap it in a self-executing function.
 * */
(function ($) {

    var $jQval = $.validator;

   $jQval.addMethod("requirediftrue",
       function (value, element, parameters) {
            return value !== "" && value != null;
        }
    );

    var adapters = $jQval.unobtrusive.adapters;
    adapters.addBool('requirediftrue');

})(jQuery);

Usage

    public bool IsSpecialField { get; set; }

    [RequiredIfTrue(nameof(IsSpecialField), ErrorMessage="This is my custom error message")]
    [Display(Name = "Address 1")]
    public string Address1 { get; set; }

    [RequiredIfTrue(nameof(IsSpecialField))]
    public string City { get; set; }
FirstDivision
  • 1,340
  • 3
  • 17
  • 37
  • I like your answer, but I don't see how the javascript is checking the other property for the "required if true" condition. It seems to only be checking "this" property has a value. – goodeye Feb 11 '20 at 02:45
  • I found a complex answer here, that I'll study. https://stackoverflow.com/a/15975880/292060 – goodeye Feb 11 '20 at 03:42
  • 2
    This is a severely underrated answer and worked fantastic first try. Thank you! – Eric Longstreet Aug 24 '20 at 03:22
3

Another, cleaner and more versatile, approach would be to implement a more generic attribute, not a specific "requiredIf" attribute, as you would have to make multiple custom attributes for every type of validation you happen to use.

Luckily, since .NET Core 2, Microsoft provides the IPropertyValidationFilter interface, that you can implement on a custom attribute. This interface defines a function ShouldValidateEntry, that allows control over whether the current entry should be validated or not; so this runs before any validators are called.

There is one default implementation in the Framework already, the ValidateNeverAttribute, but it is trivial to implement your own that does a conditional check on another value:

using System;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace Foo {
    // Implementation makes use of the IPropertyValidationFilter interface that allows
    // control over whether the attribute (and its children, if relevant) need to be
    // validated.
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public class ConditionalValidationAttribute : Attribute, IPropertyValidationFilter {
        public string OtherProperty { get; set; }
        public object OtherValue { get; set; }

        public ConditionalValidationAttribute(string otherProperty, object otherValue) {
            OtherProperty = otherProperty;
            OtherValue = otherValue;
        }

        public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) {
            // Default behaviour if no other property is set: continue validation
            if (string.IsNullOrWhiteSpace(OtherProperty)) return true;

            // Get the property specified by the name. Might not properly work with
            // nested properties.
            var prop = parentEntry.Metadata.Properties[OtherProperty]?.PropertyGetter?.Invoke(parentEntry.Model);

            return prop == OtherValue;
        }
    }
}

Just annotate the relevant properties with this attribute and any validators, also custom validators you implemented yourself, will only be called when necessary!

Implementation example: here

FWest98
  • 71
  • 7
  • This doesn't appear to work unless the parent property is bool. I am trying to use it with a select that should only validate if the selected value = 5. Any ideas? – John Nov 27 '21 at 16:12
  • I think it might have to do with the use of `==` in `return prop == OtherValue;`. Since `OtherValue` is an `object`, I think it will always just check if the two are reference equal. You could experiment with .Equals, or maybe add a Type parameter to your attribute constructor. Alternatively, with .NET 6 you can enable preview features and use [generic attributes](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-10#generic-attributes) which might work better/easier for this. – FWest98 Nov 28 '21 at 20:07
2

Found an answer

Changed

if (proprtyvalue.ToString() == DesiredValue.ToString() && value == null)

to

if (proprtyvalue.ToString() == DesiredValue.ToString() && value.ToString() == "N/A")
James
  • 1,081
  • 4
  • 15
  • 34
  • 2
    The correct approach would have been to give the label option a `null` value - `` –  Sep 13 '18 at 22:15
  • 1
    Definitely, something like a `RequiredIf` attribute should work in any scenario, not just this one specific use case. – Chris Pratt Sep 14 '18 at 17:05