Guruprasad Rao Guruprasad Rao - 5 months ago 37
jQuery Question

FileExtension Validation using custom validation creates duplicate and invalid data-* attributes

This question raises after what I've tried from the answer mentioned in my previous question. I followed this article exactly the same way but validations for

image files
instead of
doc files
mentioned in the article.

Description: I have a
input
control of
type=file
which is to upload image files and this exists in one of the
partialview
. The
partialview
gets loaded on
click
of a
button
. And to apply
validations
mentioned in
model
, explicitly add
unobtrusive
to the
form
. But after following all the set-ups mentioned in the above-said article, I am not able to validate the file on
submit
also the the
data-*
created by
unobtrusive validation
is quite fishy or better say invalid. Below is the code to show how my setup looks like and here is the
html
which gets created by unobtrusive validation with invalid
data-*
attribute, may be because of which the validation fails to happen.

<input data-charset="file" data-val="true" data-val-fileextensions="" data-val-fileextensions-fileextensions="png,jpg,jpeg" id="File" multiple="multiple" name="File" type="file" value="">


Load Partial View Js

$('.getpartial').on('click', function () {
$('.loadPartial').empty().load('/Home/GetView',function () {
var form = $('form#frmUploadImages');
form.data('validator', null);
$.validator.unobtrusive.parse(form);
$(function () {
jQuery.validator.unobtrusive.adapters.add('fileextensions', ['fileextensions'], function (options) {
var params = {
fileextensions: options.params.fileextensions.split(',')
};
options.rules['fileextensions'] = params;
if (options.message) {
options.messages['fileextensions'] = options.message;
}
});

jQuery.validator.addMethod("fileextensions", function (value, element, param) {
var extension = getFileExtension(value);
var validExtension = $.inArray(extension, param.fileextensions) !== -1;
return validExtension;
});

function getFileExtension(fileName) {
var extension = (/[.]/.exec(fileName)) ? /[^.]+$/.exec(fileName) : undefined;
if (extension != undefined) {
return extension[0];
}
return extension;
};
}(jQuery));
})
})


ModelClass

public class ImageUploadModel
{
[FileValidation("png|jpg|jpeg")]
public HttpPostedFileBase File { get; set; }
}


View

@model ProjectName.Models.ImageUploadModel

@using (Html.BeginForm("UploadImages", "Admin", FormMethod.Post, htmlAttributes: new { id = "frmUploadImages", novalidate = "novalidate", autocomplete = "off", enctype = "multipart/form-data" }))
{
<div class="form-group">
<span class="btn btn-default btn-file">
Browse @Html.TextBoxFor(m => m.File, new { type = "file", multiple = "multiple", data_charset = "file" })
</span>&nbsp;
<span class="text-muted" id="filePlaceHolder">No files selected</span>
@Html.ValidationMessageFor(m => m.File, null, htmlAttributes: new { @class = "invalid" })
</div>
<div class="form-group">
<button class="btn btn-primary addImage pull-right">
<i class="fa fa-upload"></i> Upload
</button>
</div>
}


and finally my CustomFileValidation class

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FileValidationAttribute : ValidationAttribute, IClientValidatable
{
private List<string> ValidExtensions { get; set; }

public FileValidationAttribute(string fileExtensions)
{
ValidExtensions = fileExtensions.Split('|').ToList();
}

public override bool IsValid(object value)
{
HttpPostedFileBase file = value as HttpPostedFileBase;
if (file != null)
{
var fileName = file.FileName;
var isValidExtension = ValidExtensions.Any(y => fileName.EndsWith(y));
return isValidExtension;
}
return true;
}

public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientFileExtensionValidationRule(ErrorMessage, ValidExtensions);
yield return rule;
}
}
public class ModelClientFileExtensionValidationRule : ModelClientValidationRule
{
public ModelClientFileExtensionValidationRule(string errorMessage, List<string> fileExtensions)
{
ErrorMessage = errorMessage;
ValidationType = "fileextensions";
ValidationParameters.Add("fileextensions", string.Join(",", fileExtensions));
}
}

Answer

You need to move the block code

$(function () {
  ....
}(jQuery));

from inside the $('.getpartial').on(..) function to before it so that its is

<script>
  $(function () {
    ....
  }(jQuery));

  $('.getpartial').on('click', function () { // or just $('.getpartial').click(function() {
    $('.loadPartial').empty().load('/Home/GetView',function () { // recommend .load('@Url.Action("GetView", "Home")', function() {
      var form = $('form#frmUploadImages');
      form.data('validator', null);
      $.validator.unobtrusive.parse(form);
    });
  });
</script>

Currently your load the content, re-parse the validator and then add add the methods to jquery validation but its to late (the validator has already been parsed)

Sidenote: You do not need to wrap the validation functions in $(function () {. It can be deleted and simply use $.validator... instead of jQuery.validator.... as your doing elsewhere in your code.

As for the 'fishy' data-val-* attributes, that is exactly what your code generates. Your generating a ClientValidationRule named fileextensions (the ValidationType = "fileextensions"; code) and then you add a property of it also named fileextensions (the ValidationParameters.Add("fileextensions", ..) code which generates data-val-fileextensions-fileextensions="png,jpg,jpeg". As for data-val-fileextensions="", that is generated to store the error message but you have not generated one so its an empty string.

I would suggest a few changes to your code.

  1. Rename it to FileTypeAttribute so that you have the flexibility to add other file validation attributes, for example FileSizeAttribute to validate the maximum size.
  2. In the constructor, generate a default error message, for example add private const string _DefaultErrorMessage = "Only the following file types are allowed: {0}"; and in the last line of the constructor include ErrorMessage = string.Format(_DefaultErrorMessage, string.Join(" or ", ValidExtensions));
  3. Change ValidationParameters.Add("fileextensions", ...) to (say) ValidationParameters.Add("validtypes", ...) so it generates data-val-fileextensions-validtypes="png,jpg,jpeg" which is a bit more meaningful (note you will need to change the script to ...add('fileextensions', ['validtypes'], function() ....

Edit

Your code will not work with <input type="file" multiple="multiple" ... /> In order to do so your property needs to be IEnumerable (note a few minor changes to your code)

[FileType("png, jpg, jpeg")]
public IEnumerable<HttpPostedFileBase> Files { get; set; }

Then the validation attribute needs to check each file in the collection

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FileTypeAttribute : ValidationAttribute, IClientValidatable
{
    private const string _DefaultErrorMessage = "Only the following file types are allowed: {0}";
    private IEnumerable<string> _ValidTypes { get; set; }

    public FileTypeAttribute(string validTypes)
    {
        _ValidTypes = validTypes.Split(',').Select(s => s.Trim().ToLower());
        ErrorMessage = string.Format(_DefaultErrorMessage, string.Join(" or ", _ValidTypes));
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        IEnumerable<HttpPostedFileBase> files = value as IEnumerable<HttpPostedFileBase>;
        if (files != null)
        {
            foreach(HttpPostedFileBase file in files)
            {
                if (file != null && !_ValidTypes.Any(e => file.FileName.EndsWith(e)))
                {
                    return new ValidationResult(ErrorMessageString);
                }
            }
        }
        return ValidationResult.Success;
    }
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ValidationType = "filetype",
            ErrorMessage = ErrorMessageString
        };
        rule.ValidationParameters.Add("validtypes", string.Join(",", _ValidTypes));
        yield return rule;
    }
}

And finally the script needs to check each file

$.validator.unobtrusive.adapters.add('filetype', ['validtypes'], function (options) {
    options.rules['filetype'] = { validtypes: options.params.validtypes.split(',') };
    options.messages['filetype'] = options.message;
});

$.validator.addMethod("filetype", function (value, element, param) {
    for (var i = 0; i < element.files.length; i++) {
        var extension = getFileExtension(element.files[0].name);
        if ($.inArray(extension, param.validtypes) === -1) {
            return false;
        }
    }
    return true;
});

function getFileExtension(fileName) {
    if (/[.]/.exec(fileName)) {
        return /[^.]+$/.exec(fileName)[0].toLowerCase();
    }
    return null;
}