views:

108

answers:

1

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 following 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.

A: 

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;
    }
Jon J