2

I want to validate various telephone number properties on a DTO property of my model using a custom DataAnnotationsAttribute. I don't want to duplicate the DataAnnotations onto ViewModels, to keep the code DRY, and instead I have registered a custom adapter for client-side validation using DataAnnotationsModelValidatorProvider. This adapter provides ModelClientValidationRemoteRules, normally used by the RemoteAttribute. jQuery unobtrusive validation then calls into my validate action, which validates the individual fields.

This setup isn't really adequate however.

  1. The attribute currently uses the its ContainerType to work out which validation action to call. The DTO is used on different viewmodels at different levels of nesting, however, so we don't know exactly what prefix to use on the action. Depending on the location of the ProfileDto in the model hierarchy, the action prefix would need to change

  2. The validation action uses Request.Form.Keys to work out which property which should be validating. I know it is best practice to stay away from the Request object in Action for the sake of unit testing etc.

Is there a good way to include the name of the field to validate in postback, so I can have it on my action as an additional parameter instead of using Request.Form?

Is there a way to get the model binder to bind my properties, given that they will posted back with a prefix dependent on the child model's name?

Thanks in advance!


The attribute is as follows:

public class PhoneNumberAttribute : ValidationAttribute
{
    public PhoneNumberType RequiredType { get; set; }

    public PhoneNumberAttribute()
        : base("{0} is not a valid phone number.")
    {
    }

    public override bool IsValid(object value)
    {
        string s = value as string;
        if (s == null)
        {
            return false;
        }
        if (!PhoneNumberUtils.IsValidNumber(s, RequiredType))
        {
            return false;
        }

        return true
    }

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

and the adapter:

public class PhoneNumberAttributeAdapter : DataAnnotationsModelValidator<PhoneNumberAttribute>
{
    public PhoneNumberAttributeAdapter(ModelMetadata metadata, ControllerContext context, PhoneNumberAttribute attribute)
        : base(metadata, context, attribute)
    {
    }

    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
    {
        var errorMessage = Attribute.FormatErrorMessage(Metadata.GetDisplayName());
        var routeData = new RouteValueDictionary { 
            { "controller", "Validate" },
            { "action", Metadata.ContainerType.Name },
        };

        var path = RouteTable.Routes.GetVirtualPathForArea(ControllerContext.RequestContext, routeData);

        var rule = new ModelClientValidationRemoteRule(
            errorMessage, 
            path.VirtualPath, 
            "POST", 
            "*." + Metadata.PropertyName);

        return new[] { rule };
    }
}

here is the Action:

public ActionResult ProfileDto([Bind(Prefix = "Dto")]ProfileDto model)
{
    string fieldToValidate = Request.Form.Keys[0];
    if (ModelState.IsValidField(fieldToValidate))
    {
        return Json(true);
    }

    var fieldErrors = ModelState[fieldToValidate].Errors;

    return Json(fieldErrors.First().ErrorMessage);
}
Joe Taylor
  • 2,145
  • 1
  • 19
  • 35

1 Answers1

0

Take a look at this example here where is show how to get the nested properties even with prefix in the custom jQuery validator.

Secondly, MVC model binder should bind your prefix automatically.

Johannes Setiabudi
  • 2,057
  • 17
  • 29