I have forms that I have had to create custom validation attributes for. My custom attribute works at the server, but I do not want it to go to the server to validate this. I would love for it to work at the client-side.
What this validator does is looks to see what has been selected for a stated property. If the value of the property is selected, then this property is required.
The attribute is applied like this:
[AnotherTest("CurrentlyPracticing", 1,
ErrorMessage = "Required if practicing.")]
public string? ProfLicense { get; set; } = string.Empty;
As you can see, the ProfLicense
property is only required if CurrentlyPracticing = 1
.
Here is my model class, custom validation attribute code, and view for this project.
My model class (ignore the commented out portions - it is a work in progress):
public class ApplicationVM
{
// other attributes removed because of Stackoverflow space requirements
[Required(ErrorMessage = "You must answer this question.")]
[Display(Name = "Are you a licensed medical practitioner?")]
public int? LicMedPract { get; set; }
[RequiredIfTrue(nameof(LicMedPract), ErrorMessage = "An answer is required.")]
[Display(Name = "Currently Practicing?")]
public int? CurrentlyPracticing { get; set; }
[RequiredIfTrue(nameof(CurrentlyPracticing), ErrorMessage = "You must select a Specialty.")]
[Display(Name = "Practice Specialty:")]
public int? SpecialtyID { get; set; }
[RequiredIfTrue(nameof(SpecialtyID), ErrorMessage = "You must type an Other Specialty.")]
[StringLength(25)]
[Display(Name = "Other Specialty:")]
public string PracSpecOther { get; set; } = string.Empty;
[Display(Name = "License Number")]
[AnotherTest("CurrentlyPracticing", 1,
ErrorMessage = "Required if practicing.")]
public string? ProfLicense { get; set; } = string.Empty;
[MaxLength(2)]
[Display(Name = "State Licensed")]
[AnotherTest("CurrentlyPracticing", 1,
ErrorMessage = "Required if practicing.")]
public string? ProfLicenseState { get; set; } = string.Empty;
}
Here is my custom validation attribute:
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations;
namespace pportal2.common.Validation
{
public class AnotherTestAttribute : ValidationAttribute, IClientModelValidator
{
private readonly string _propertyName;
private readonly object _desiredValue;
private readonly RequiredAttribute _required;
public AnotherTestAttribute(string propertyName, Object desiredValue)
{
_propertyName = propertyName;
_desiredValue = desiredValue;
_required = new RequiredAttribute();
}
public void AddValidation(ClientModelValidationContext context)
{
var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-testif", errorMessage);
MergeAttribute(context.Attributes, "data-val-testif-propertyname", _propertyName);
MergeAttribute(context.Attributes, "data-val-testif-desiredvalue", _desiredValue.ToString());
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var dependentValue = validationContext.ObjectInstance.GetType().GetProperty(_propertyName).GetValue(validationContext.ObjectInstance, null);
if (dependentValue.ToString() == _desiredValue.ToString())
{
if (!_required.IsValid(value))
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
}
}
My view (here I think my issue is at the validator section. This is at the bottom of the scripts):
<div class="col-md-8 m-auto">
<h1 class="text-center">PACE Program Initial Application</h1>
</div>
<hr />
<div class="col-md-8 m-auto">
<form asp-action="Application" id="application">
<hr />
</div>
<div class="form-group">
<h4>
<label>Professional Information:</label>
</h4>
<div class="form-group col-sm-12">
<div class="row">
<div class="form-group col-sm-6">
<label asp-for="LicMedPract" class="control-label"></label>
<select asp-for="LicMedPract" class="form-control" asp-items="ViewBag.YesNo2"></select>
<span asp-validation-for="LicMedPract" class="text-danger"></span>
</div>
</div>
<div id="divHiddenPracticeInfo" style="display:none;">
<div class="row">
<div class="form-group col-sm-5 col-md-4 col-lg-3">
<label asp-for="CurrentlyPracticing" class="control-label"></label>
<select asp-for="CurrentlyPracticing" class="form-control" asp-items="ViewBag.YesNo2"></select>
<span asp-validation-for="CurrentlyPracticing" class="text-danger"></span>
</div>
<div class=" col-sm-5 col-md-4 col-lg-3" id="divPracticeSpecialty" style="display: none;">
<div class="form-group col-md-12">
<label asp-for="SpecialtyID" class="control-label"></label>
<select asp-for="SpecialtyID" class="form-control" asp-items="ViewBag.Specialty"></select>
<span asp-validation-for="SpecialtyID" class="text-danger"></span>
</div>
</div>
<div class=" col-sm-5 col-md-4 col-lg-3" id="divPracSpecOther" style="display: none;">
<div class="form-group col-md-12">
<label asp-for="PracSpecOther" class="control-label"></label>
<input asp-for="PracSpecOther" class="form-control" />
<span asp-validation-for="PracSpecOther" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<h5 class="col-md-12">Please include all letters and numbers.</h5>
<div class="form-group col-sm-5 col-md-4 col-lg-3">
<label asp-for="ProfLicense" class="control-label"></label>
<input asp-for="ProfLicense" class="form-control" />
<span asp-validation-for="ProfLicense" class="text-danger"></span>
</div>
<div class="form-group col-sm-5 col-md-4 col-lg-3">
<label asp-for="ProfLicenseState" class="control-label"></label>
<input asp-for="ProfLicenseState" class="form-control" />
<span asp-validation-for="ProfLicenseState" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="form-group col-sm-5 col-md-4 col-lg-3">
<label asp-for="DEA" class="control-label"></label>
<input asp-for="DEA" class="form-control" />
<span asp-validation-for="DEA" class="text-danger"></span>
</div>
</div>
</div>
</div>
</div>
<div asp-validation-summary="All" class="text-danger">
<span id="message2"></span>
</div>
<div class="form-group">
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
<br />
</div>
</form>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script type="text/javascript">
// Other script code removed because of space limitations on stackoverflow....
$.validator.addMethod("testif", function (value, element, parameters) {
var practicing = $(parameters[0]).val(), desiredvalue = parameters[1], proflicense = value;
console.log(practicing[0].val());
if (practicing && practicing[0] === 1) {
alert(value);
return value != null;
}
});
$.validator.unobtrusive.adapters.add("testif", "desiredvalue", function (options) {
options.rules.testif = {};
options.
options.messages["testif"] = options.message;
});
</script>
}
I wrote a small working sample you can try reference
ApplicationVM.cs
public class ApplicationVM
{
[Display(Name = "Currently Practicing?")]
public int? CurrentlyPracticing { get; set; }
[Display(Name = "State Licensed")]
[AnotherTest("CurrentlyPracticing", 1,ErrorMessage = "Required if practicing.")]
public string? ProfLicense { get; set; }
}
AnotherTestAttribute.cs
public class AnotherTestAttribute : ValidationAttribute, IClientModelValidator
{
private readonly string _propertyName;
private readonly object _desiredValue;
public AnotherTestAttribute(string propertyName, object desiredValue)
{
_propertyName = propertyName;
_desiredValue = desiredValue;
}
public void AddValidation(ClientModelValidationContext context)
{
var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-anothertest", errorMessage);
MergeAttribute(context.Attributes, "data-val-anothertest-propertyname", _propertyName);
MergeAttribute(context.Attributes, "data-val-anothertest-desiredvalue", _desiredValue.ToString());
}
private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//I didn't implement server validation
return ValidationResult.Success;
}
}
Controller
[HttpGet]
public IActionResult Index()
{
return View(new ApplicationVM());
}
[HttpPost]
public IActionResult Index(ApplicationVM applicationVM)
{
return View(applicationVM);
}
Index.cshtml
@model ApplicationVM
@{
List<SelectListItem> choices1 = new()
{
new SelectListItem { Value = "1",Text="Yes"},
new SelectListItem { Value = "0",Text="No"},
};
ViewBag.YesNo2 = choices1;
}
<div class="col-md-8 m-auto">
<h1 class="text-center">PACE Program Initial Application</h1>
</div>
<div class ="col-md-8 m-auto">@Model.ProfLicense</div>
<div class="col-md-8 m-auto">
<form asp-action="index" method="post">
<div class="form-group col-sm-5 col-md-4 col-lg-3">
<label asp-for="CurrentlyPracticing" class="control-label"></label>
<select asp-for="CurrentlyPracticing" class="form-control" id="CurrentlyPracticing" asp-items="ViewBag.YesNo2"></select>
<span asp-validation-for="CurrentlyPracticing" class="text-danger"></span>
</div>
<div class="form-group col-sm-5 col-md-4 col-lg-3">
<label asp-for="ProfLicense" class="control-label"></label>
<input asp-for="ProfLicense" class="form-control" />
<span asp-validation-for="ProfLicense" class="text-danger"></span>
</div>
<input type="submit" value="Submit" class="btn btn-primary" />
</form>
</div>
@section scripts {
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script>
$.validator.addMethod("anothertest",
function (value, element, parameters) {
var propertyname = $(element).data('val-anothertest-propertyname');
var desiredvalue = $(element).data('val-anothertest-desiredvalue');
if ((value.trim() == "") && (document.getElementById(propertyname).value==desiredvalue)) {
return false
}
else{
return true
}
});
$.validator.unobtrusive.adapters.addBool("anothertest");
</script>
}