Using a custom validator in a variable length list in Microsoft MVC 2 (client-side validation issues)
I have created a variable length list according to the many great posts by Steve Sanderson on how to do this in MVC 2. His blog has a lot of great tutorials.
I then created a custom "requiredif" conditional validator following this overview http://blogs.msdn.com/b/simonince/archive/2010/06/11/adding-client-side-script-to-an-mvc-conditional-validator.aspx
I used the JQuery validation handler from the MSDN blog entry which adds the f开发者_如何学JAVAollowing to a conditional-validators.js I include on my page's scripts:
(function ($) {
$.validator.addMethod('requiredif', function (value, element, parameters) {
var id = '#' + parameters['dependentProperty'];
// Get the target value (as a string, as that's what actual value will be)
var targetvalue = parameters['targetValue'];
targetvalue = (targetvalue == null ? '' : targetvalue).toString().toLowerCase();
// Get the actual value of the target control
var actualvalue = ($(id).val() == null ? '' : $(id).val()).toLowerCase();
// If the condition is true, reuse the existing required field validator functionality
if (targetvalue === actualvalue)
return $.validator.methods.required.call(this, value, element, parameters);
return true;
});
})(jQuery);
Alas, this does not cause a client-side validation to fire ... only the server-side validation fires. The inherent "required" validators DO fire client-side, meaning I have my script includes set-up correctly for basic validation. Has anyone accomplished custom validators in a variable length list in MVC 2 using JQuery as the client-side validation method?
NOTE that this same custom validator works client-side using the exact same set-up on a non-variable length list.
Turns out that it was a field ID naming issue with the way that collection IDs render in a variable length list. The validator was attempting to name the element ID of the dependent property with the expected statement of:
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty);
I analyzed the HTML viewsource (posted in my comment, above), and actually, [ and ] characters are not output in the HTML of the collection-index elements... they're replaced with _... so, when I changed my CustomValidator.cs to have the dependent property set to:
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty).Replace("[", "_").Replace("]", "_");
... then the client-side validator works since the name matches. I'll have to dig deeper to see WHY the ID is getting renamed in Sanderson's collection index method, below...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Sendz.WebUI.Helpers
{
public static class HtmlPrefixScopeExtensions
{
private const string IdsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";
public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
{
var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
var itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();
// autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
html.ViewContext.Writer.WriteLine(
string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />",
collectionName, html.Encode(itemIndex)));
return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
}
public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
{
return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
}
private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
{
// We need to use the same sequence of IDs following a server-side validation failure,
// otherwise the framework won't render the validation error messages next to each item.
var key = IdsToReuseKey + collectionName;
var queue = (Queue<string>)httpContext.Items[key];
if (queue == null)
{
httpContext.Items[key] = queue = new Queue<string>();
var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
if (!string.IsNullOrEmpty(previouslyUsedIds))
foreach (var previouslyUsedId in previouslyUsedIds.Split(','))
queue.Enqueue(previouslyUsedId);
}
return queue;
}
#region Nested type: HtmlFieldPrefixScope
private class HtmlFieldPrefixScope : IDisposable
{
private readonly string _previousHtmlFieldPrefix;
private readonly TemplateInfo _templateInfo;
public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
{
_templateInfo = templateInfo;
_previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
}
#region IDisposable Members
public void Dispose()
{
_templateInfo.HtmlFieldPrefix = _previousHtmlFieldPrefix;
}
#endregion
}
#endregion
}
}
A complete validator / attribute reference...
public class RequiredIfAttribute : ValidationAttribute
{
private RequiredAttribute innerAttribute = new RequiredAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue)
{
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
public override bool IsValid(object value)
{
return innerAttribute.IsValid(value);
}
}
public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
: base(metadata, context, attribute) { }
public override IEnumerable<ModelValidationResult> Validate(object container)
{
// Get a reference to the property this validation depends upon
var field = Metadata.ContainerType.GetProperty(Attribute.DependentProperty);
if (field != null)
{
// Get the value of the dependent property
var value = field.GetValue(container, null);
// Compare the value against the target value
if ((value == null && Attribute.TargetValue == null) ||
(value != null && value.ToString().ToLowerInvariant().Equals(Attribute.TargetValue.ToString().ToLowerInvariant())))
{
// A match => means we should try validating this field
if (!Attribute.IsValid(Metadata.Model))
// Validation failed - return an error
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = ErrorMessage,
ValidationType = "requiredif"
};
var viewContext = (ControllerContext as ViewContext);
var depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty).Replace("[", "_").Replace("]", "_");
rule.ValidationParameters.Add("dependentProperty", depProp);
rule.ValidationParameters.Add("targetValue", Attribute.TargetValue.ToString());
yield return rule;
}
精彩评论