简体   繁体   中英

Get full HTML field name for client side validation in ASP.NET Core

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"
    class="text-danger field-validation-valid"
></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!

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"

ValidationContext is binded to instance that belong to validating property ie 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. eg 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 ie 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 class="form-control" type="text" data-val="true" data-val-required="Please enter name" id="Student_Name" name="Student.Name" value="">
<input class="form-control input-validation-error" 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="">

This is a method that also works when there are fields rendered that are deeper children of the model.

//Build the client id of the property name.
var dependentClientId = dependentPropertyName;
var clientId = context.Attributes["id"];
var clientIdArr = clientId.Split("_");
if (clientIdArr.Length > 1)
{
    //Replace the last value of the array with the dependent property name.
    clientIdArr[clientIdArr.Length - 1] = dependentPropertyName;
    dependentClientId = string.Join("_", clientIdArr);
}

MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", dependentClientId );

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM