gabe
gabe

Reputation: 1950

Semi-Complex View Model Property Validation in ASP.NET MVC 3

I am struggling to complete a server-client validation solution for a semi-complex scenario. I have a core type called DateRange:

public class DateRange {
    public DateRange (DateTime? start, DateTime? end) { ... }
    public DateTime? Start { get; private set; }
    public DateTime? End { get; private set; }
}

I have a view model like:

public class MyViewModel {
    public DateRange Period { get; set; }
}

I have a %mvcproject%\Views\Shared\EditorTemplates\DateRange.cshtml like:

@model MyCore.DateRange

@Html.Editor("Start", "Date")
@Html.Editor("End", "Date")

I also have a DateRangeModelBinder to bind the two form inputs into the DateRange property. The problem I'm having is with a DateRangeRequiredAttribute:

public class DateRangeRequired : ValidationAttribute, IClientValidatable,
    IMetadataAware
{
    private const string DefaultErrorMessage =
           "{0} is required.";

    public DateRangeRequired(bool endIsRequired = true)
        : base(() => DefaultErrorMessage)
    {
        EndIsRequired = endIsRequired;
    }

    public bool EndIsRequired { get; set; }

    public override bool IsValid(object value)
    {
        if (value == null)
        {
            return false;
        }
        if (!value.GetType().IsAssignableFrom(typeof(DateRange)))
        {
            throw new ArgumentException("Value is not a DateRange.");
        }
        var dateRange = value as DateRange;
        return (dateRange.Start.HasValue && !EndIsRequired) ||
               (dateRange.Start.HasValue && dateRange.End.HasValue && EndIsRequired);
    }

    public override string FormatErrorMessage(string name)
    {
        return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule()
        {
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "daterangerequired"
        };
        rule.ValidationParameters.Add("endisrequired", EndIsRequired.ToString().ToLower());
        yield return rule;
    }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.DataTypeName = "DateRange";
    }
}

I can't get it to hook up to the two inputs. It's almost like there needs to be a ValidatorTemplate that pairs with the EditorTemplate because of the split inputs. Any ideas? Let me know if additional clarification is needed.

Upvotes: 3

Views: 845

Answers (1)

Darin Dimitrov
Darin Dimitrov

Reputation: 1039268

You haven't shown exactly how your custom DateRangeRequiredAttribute implementation looks like, so let me suggest an example:

public class DateRangeRequiredAttribute : ValidationAttribute, IClientValidatable
{
    private readonly string _otherProperty;
    public DateRangeRequiredAttribute(string otherProperty)
    {
        _otherProperty = otherProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var property = validationContext.ObjectType.GetProperty(_otherProperty);
        if (property == null)
        {
            return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "Unknown property {0}", _otherProperty));
        }
        var otherValue = property.GetValue(validationContext.ObjectInstance, null);
        if (!(value is DateTime) || !(otherValue is DateTime))
        {
            return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "The two properties to compare must be of type DateTime"));
        }

        if ((DateTime)value >= (DateTime)otherValue)
        {
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
        }
        return null;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "daterange"
        };
        rule.ValidationParameters.Add("other", "*." + _otherProperty);
        yield return rule;
    }
}

then you could decorate your view model with it:

public class DateRange
{
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
    [DateRangeRequired("End", ErrorMessage = "Please select a start date before the end date")]
    public DateTime? Start { get; set; }

    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
    [Required]
    public DateTime? End { get; set; }
}

and finally in the view register the adapter:

jQuery.validator.unobtrusive.adapters.add(
    'daterange', ['other'], function (options) {
        var getModelPrefix = function (fieldName) {
            return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
        };
        var appendModelPrefix = function (value, prefix) {
            if (value.indexOf('*.') === 0) {
                value = value.replace('*.', prefix);
            }
            return value;
        };
        var prefix = getModelPrefix(options.element.name),
        other = options.params.other,
        fullOtherName = appendModelPrefix(other, prefix),
        element = $(options.form).find(':input[name="' + fullOtherName + '"]')[0];

        options.rules['daterange'] = element;
        if (options.message) {
            options.messages['daterange'] = options.message;
        }
    }
);

jQuery.validator.addMethod('daterange', function (value, element, params) {
    // TODO: some more advanced date checking could be applied here
    // currently it uses the current browser culture setting to perform
    // the parsing. If you needed to use the server side culture, this code
    // could be adapted respectively

    var date = new Date(value);
    var otherDate = new Date($(params).val());
    return date < otherDate;
}, '');

After reading this pornography, you might consider using FluentValidation.NET which renders this extremely simple validation scenario a couple of lines to implement (which is how such simple validation scenarios should be done). I would strongly recommend you this library. I am using it in all my projects because I am sick of DataAnnotations for validation. They are so pretty limited.

Upvotes: 3

Related Questions