21

I am trying to conditionally validate the field within ASP.NET Core MVC. 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;
    }
}
2
  • SIde note Your ValidationAttribute should also implement IClientModelValidator if you want client side validation as well (refer Model validation in ASP.NET Core MVC) Commented Sep 13, 2018 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. Commented Sep 14, 2018 at 17:07

5 Answers 5

19

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; }
Sign up to request clarification or add additional context in comments.

8 Comments

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 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 (?)
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 sure have I'm using it in my current projects
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?
|
16

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

3 Comments

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.
I found a complex answer here, that I'll study. stackoverflow.com/a/15975880/292060
This is a severely underrated answer and worked fantastic first try. Thank you!
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

2 Comments

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?
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 which might work better/easier for this.
2

Found an answer

Changed

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

to

if (proprtyvalue.ToString() == DesiredValue.ToString() && value.ToString() == "N/A")

2 Comments

The correct approach would have been to give the label option a null value - <option value="">Please Select</option>
Definitely, something like a RequiredIf attribute should work in any scenario, not just this one specific use case.
0

This regex decorator checks and prevents entering address if it contains "PO Box" in it:

[RegularExpression(@"^(?i)(?!.*\bP\.?\s?O\.?\s?Box\b).*$", ErrorMessage = "We are unable to ship to PO Box addresses.")]
[StringLength(100, ErrorMessage = "This field is required")]
public string Address1 { get; set; }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.