I'm implementing a custom validation attribute. This attribute does not only look at the value of the property it is applied to, but also at the value of another property. The other property is specified by its name.
I need to find a way to get the full id that the input for the other property will have in the final HTML output.
This is a simplified version of my validation attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class MyCustomValidationAttribute : ValidationAttribute, IClientModelValidator
{
private string _otherPropertyName;
public MyCustomValidationAttribute(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
var otherProperty = context.ObjectInstance.GetType().GetProperty(_otherPropertyName);
var otherPropertyValue = Convert.ToString(otherProperty.GetValue(context.ObjectInstance, null));
// Validation logic...
}
public void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val-mycustomvalidation", errorMessage);
// THIS ROW NEEDS TO BE FIXED
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", _otherProperyName);
}
private void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (!attributes.ContainsKey(key))
{
attributes.Add(key, value);
}
}
}
This demonstrates how it is used in a model class:
public class Report
{
[MyCustomValidation("Value2", ErrorMessage = "Error...")]
public string Value1 { get; set; }
public string Value2 { get; set; }
}
This is the JavaScript to make sure that the validation is also done on the client side:
$.validator.addMethod('mycustomvalidation',
function (value, element, parameters) {
var otherPropertyValue = $('#' parameters.otherpropertyname).val();
// Validation logic...
});
$.validator.unobtrusive.adapters.add('mycustomvalidation', ['otherpropertyname'],
function (options) {
options.rules.mycustomvalidation = options.params;
options.messages['mycustomvalidation'] = options.message;
});
My viewmodel for the page/view with the form looks like this:
public MyViewModel
{
public Report MyReport { get; set; }
}
Note that I'm not using Report as my viewmodel, but rather as the type of a property in the viewmodel. This is important since this is the root of my problem...
The code in the view to show the input for Value1 is nothing strange (I'm using Razor Pages):
<div>
<label asp-for="MyReport.Value1"></label>
<input asp-for="MyReport.Value1" />
<span asp-validation-for="MyReport.Value1"></span>
</div>
And the output becomes:
<label for="MyReport_Value1">Value1</label>
<input
type="text"
id="MyReport_Value1"
name="MyReport.Value1"
data-val="true"
data-val-mycustomvalidation="Error..."
data-val-mycustomvalidation-otherpropertyname="Value2"
value=""
>
<span
data-valmsg-for="MyReport.Value1"
data-valmsg-replace="true"
></span>
So the problem is that in the HTML output I need data-val-mycustomvalidation-otherpropertyname to be "MyReport_Value2" instead of just "Value2". Otherwise the validation code won't be able to find the second HTML input (with id MyReport_Value2) and perform the validation.
I figure this must be done in the method AddValidation() in the attribute class, but how do I get the full name that the HTML input will recieve?
I'm guessing there is some way to get this by using the context parameter. I've seen examples of something like "*.TemplateInfo.GetFullHtmlFieldId(PropertyName)" but I can't get it to work.
Any help is appreciated!
CodePudding user response:
You pass Value2
to MyCustomValidationAttribute
and set _otherPropertyName
with Value2
,and use
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", _otherProperyName);
so that html will be
data-val-mycustomvalidation-otherpropertyname="Value2"
You only need to pass Report_Value2
to MyCustomValidationAttribute
rather than Value2
.
public class Report
{
[MyCustomValidation("Report_Value2", ErrorMessage = "Error...")]
public string Value1 { get; set; }
public string Value2 { get; set; }
}
So that you will get
data-val-mycustomvalidation-otherpropertyname="Report_Value2"
CodePudding user response:
ValidationContext is binded to instance that belong to validating property i.e Model. Hence locating reference of ViewModel looks difficult. I can provide three different solution you can use which one suits your requirement.
Solution 1:
Using ValidationContext you can able to get Name of the class where Property belong to. This solution will work only if ViewModel Property Name must be same as Model Class Name. e.g. if Model Class is Student then property name must be Student. If property name is Student1 it wont work. Solution 2 & 3 will work even if Class name and property name are different.
Model
public class Student
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage = "Please enter name")]
public string Name { get; set; }
[Required]
[Country("Name")]
public string Country { get; set; }
}
ViewModel
public class StudentViewModel
{
public Student Student {get;set;} //Solution 1 wil not work for Student1
}
ValidationAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class CountryAttribute : ValidationAttribute, IClientModelValidator
{
private string _otherPropertyName;
private string _clientPropertyName;
public CountryAttribute(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var otherProperty = validationContext.ObjectInstance.GetType().GetProperty(_otherPropertyName);
var otherPropertyValue = Convert.ToString(otherProperty.GetValue(validationContext.ObjectInstance, null));
_clientPropertyName = otherProperty.DeclaringType.Name "_" otherProperty.Name;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", _clientPropertyName);
}
}
Solution 2:
Using ClientModelValidationContext you can able to get ViewModel reference that is passed from the controller to view. By using reflection we can get the name of the property i.e Model. To work with solution you need to pass empty ViewModel reference from controller.
Controller
public IActionResult New()
{
StudentViewModel studentViewModel = new StudentViewModel();
return View(studentViewModel);
}
ValidationAttribute
public void AddValidation(ClientModelValidationContext context)
{
var otherClientPropName = context.ModelMetadata.ContainerMetadata.Properties
.Single(p => p.PropertyName == this._otherPropertyName)
.GetDisplayName();
var viewContext = context.ActionContext as ViewContext;
if (viewContext?.ViewData.Model is StudentViewModel)
{
var model = (StudentViewModel)viewContext?.ViewData.Model;
var instanceName = model.GetType().GetProperties()[0].Name;
otherClientPropName = instanceName "_" otherClientPropName;
}
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", otherClientPropName);
}
Solution 3:
Using context.Attributes["id"] you can able to get current property id value as string . By using string manipulation you can get prefix then you can merge with other property name. This solution doesn't require empty ViewModel reference from controller.
Controller
public IActionResult New()
{
return View();
}
ValidationAttribute
public void AddValidation(ClientModelValidationContext context)
{
var otherClientPropName = context.ModelMetadata.ContainerMetadata.Properties
.Single(p => p.PropertyName == this._otherPropertyName)
.GetDisplayName();
var id = context.Attributes["id"];
var idPrefix = id.Split("_");
if (idPrefix.Length > 1)
{
otherClientPropName = idPrefix[0] "_" otherClientPropName;
}
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", otherClientPropName);
}
HTML Output
<input type="text" data-val="true" data-val-required="Please enter name" id="Student_Name" name="Student.Name" value="">
<input type="text" data-val="true" data-val-mycustomvalidation-otherpropertyname="Student_Name" data-val-required="The Country field is required." id="Student_Country" name="Student.Country" value="">