Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional validation in MVC.NET Core (RequiredIf)

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;
    }
}
like image 948
James Avatar asked Sep 13 '18 20:09

James


People also ask

How do you validate a condition in C#?

To validate user's input we just need to annotate our viewmodel properties with Required attribute. Our ViewModel class will be like this: [Required(ErrorMessage = "Name is required")] public string Name { get; set; }

Can we do validation in MVC using data annotations?

In ASP.NET MVC, Data Annotation is used for data validation for developing web-based applications. We can quickly apply validation with the help of data annotation attribute classes over model classes.


3 Answers

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; }
like image 150
Rob Avatar answered Oct 22 '22 20:10

Rob


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; }
like image 43
FirstDivision Avatar answered Oct 22 '22 21:10

FirstDivision


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

like image 3
FWest98 Avatar answered Oct 22 '22 21:10

FWest98