I am trying to write a custom validation attribute that will conditionally require fields based on boolean properties of the model.
I have my attribute implementing IClientValidatable. I have the name of the property to check, but I dont know how to get the client id of the target property.
public IEnumerable<ModelClientValidationRule>
GetClientValidationRules(ModelMetadata metadata,
ControllerContext context)
{
var clientT开发者_Go百科arget = ?????;
var rule = new ModelClientValidationRule()
{
ErrorMessage =
FormatErrorMessage(metadata.DisplayName ?? metadata.PropertyName),
ValidationType = "requiredif"
};
rule.ValidationParameters["target"] = clientTarget;
yield return rule;
}
The javascript:
$.validator.addMethod("requiredif", function (value, element, target)
{
//check on value of target
});
$.validator.unobtrusive.adapters.addSingleVal("requiredif", "target");
How can I get the client id of the target property so that the client side javascript can check on the value?
I took Nathan's excellent answer, added some comments, and wrapped it in an extension method named GetHtmlId, so that now I can use code like this to get the HTML ID of any other element on the same page:
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule {
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "requiredif",
};
// Find the value on the control we depend on...
string depProp = this.GetHtmlId(metadata, context, this.DependentPropertyName);
rule.ValidationParameters.Add("dependentproperty", depProp);
yield return rule;
}
And here's the extension method:
using System;
using System.Collections.Generic;
using System.Web.Mvc;
namespace sbs.Lib.Web.ValidationAttributes
{
public static class IClientValidatableExtensions
{
/// <summary> Returns the HTML ID of the specified view model property. </summary>
/// <remarks> Based on: http://stackoverflow.com/a/21018963/1637105 </remarks>
/// <param name="metadata"> The model metadata. </param>
/// <param name="viewContext"> The view context. </param>
/// <param name="propertyName"> The name of the view model property whose HTML ID is to be returned. </param>
public static string GetHtmlId(this IClientValidatable me,
ModelMetadata metadata, ControllerContext context,
string propertyName)
{
var viewContext = context as ViewContext;
if (viewContext == null || viewContext.ViewData.TemplateInfo.HtmlFieldPrefix == string.Empty) {
return propertyName;
} else {
// This is tricky. The "Field ID" returned by GetFullHtmlFieldId is the HTML ID
// attribute created by the MVC view engine for the property whose validator is
// being set up by the caller of this routine. This code removes the property
// name from the Field ID, then inserts the specified property name.
// Of course, this only works for elements on the same page as the caller of
// this routine!
string fieldId = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("");
fieldId = fieldId.Remove(fieldId.LastIndexOf("_"));
return fieldId + "_" + propertyName;
}
}
}
}
This has only been tested with MVC5, but I suspect it hasn't changed from MVC3.
This is kind of ugly, but it seems to work. There are two assumptions:
- The MVC Framework actually always passes in a
ViewContext
for theControllerContext
argument ofGetClientValidationRules(....)
. In all my testing this has been the case, but I can't guarantee this 100%. - The other property is on the same model class level (not a sub property of a complex type for instance)
If both of these assumptions hold true, then the following seems to work:
var viewContext = (ViewContext)context;
if(ViewData.TemplateInfo.HtmlFieldPrefix != string.Empty)
{
string fieldId = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("");
fieldId = fieldId.Remove(fieldId.LastIndexOf("_"));
fieldId = fieldId + "_" + BooleanPropertyName
}
else
{
string fieldId = BooleanPropertyName
}
This works for me, might need a bit of tweaking, but you can get the idea:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "requiredif",
};
string depProp = BuildDependentPropertyId(metadata, context as ViewContext);
// find the value on the control we depend on;
// if it's a bool, format it javascript style
// (the default is True or False!)
string targetValue = (this.TargetValue ?? "").ToString();
if (this.TargetValue != null && this.TargetValue.GetType() == typeof(bool))
targetValue = targetValue.ToLower();
rule.ValidationParameters.Add("dependentproperty", depProp);
rule.ValidationParameters.Add("targetvalue", targetValue);
yield return rule;
}
private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
{
return QualifyFieldId(metadata, this.DependentProperty, viewContext);
}
Attribute the property like:
[RequiredIf("SelectedPeriod", "DateRange", ErrorMessageResourceName = "FromDateRequired", ErrorMessageResourceType = typeof(Common))]
public DateTime? StartDate { get; set; }
//dependent property
public string SelectedPeriod { get; set; }
And this is how to get the field id:
protected string QualifyFieldId(ModelMetadata metadata, string fieldId, ViewContext viewContext)
{
// build the ID of the property
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(fieldId);
var thisField = metadata.PropertyName + "_";
if (depProp.StartsWith(thisField))
// strip it off again
depProp = depProp.Substring(thisField.Length);
else if (null != metadata.ContainerType && !string.IsNullOrEmpty(metadata.ContainerType.Name))
{
depProp = metadata.ContainerType.Name + "_" + fieldId;
}
return depProp;
}
Have a look at this article. it discusses DataAnnotation validation on server side and it also demonstrates how to hook these attributes on client side by implementing IClientVaildatable and writing some jquery on client side.
精彩评论