1

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));
    }
}
Community
  • 1
  • 1
Guruprasad J Rao
  • 29,410
  • 14
  • 101
  • 200
  • 2
    Your 2 `jQuery.validator` and the `getFileExtension()` functions should not be inside the `$('.getpartial').on('click', function () {` - move them to before the method (everything inside and including `$(function () { .... }(jQuery));` –  Oct 30 '15 at 08:32
  • 1
    Also that code will only validate one file so it probably will not work with `multiple="multiple"` (although it could be modified to do so) –  Oct 30 '15 at 08:37
  • @StephenMuecke.. Wonderful.. That did the trick.. However, the `data-*` attribute i.e. `data-val-fileextensions="" data-val-fileextensions-fileextensions="png,jpg,jpeg"` still remains same in the `DOM`.. Isn't that `invalid`? – Guruprasad J Rao Oct 30 '15 at 09:13
  • `data-val-fileextensions-fileextensions="png,jpg,jpeg"` is generated because of the `ModelClientFileExtensionValidationRule()` method which has `ValidationType = "fileextensions";` and ` ValidationParameters.Add("fileextensions", ..)` - its a bit confusing because they should be different names. And `data-val-fileextensions=""` is because you do not have an error message. –  Oct 30 '15 at 09:35
  • I'll add an answer a bit later explaining why (and the potential issues with `multiple="multiple"`) –  Oct 30 '15 at 09:36
  • Sure @StephenMuecke.. will wait.. also as an optional I would be more glad if you also add `filesize` length check in your answer using the same custom validation.. :) – Guruprasad J Rao Oct 30 '15 at 09:39
  • @StephenMuecke.. Also **[this article](https://www.captechconsulting.com/blogs/handling-multiple-validation-rules-in-aspnet-mvc-using-dataannotations-and-unobtrusive-jquery-validator---part-2)** shows how to add list of validations, but again its totally bouncing above my head.. :( Tried many things but finding it difficult to understand itself.. :( – Guruprasad J Rao Oct 30 '15 at 10:00
  • The answer is tool long already, but [this article](http://www.bradwestness.com/2014/09/24/client-side-file-upload-validation/) is worth studying for the `FileSizeAttribute` –  Oct 30 '15 at 10:18

1 Answers1

1

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;
}
  • Alright buddy.. I understand.. Will do all the changes and thanks for `FileSizeValidation` link.. also You said you would be mentioning about multiple file upload handling.. Can you please show the same if possible? – Guruprasad J Rao Oct 30 '15 at 10:25
  • 1
    The first issue is that your property is `HttpPostedFileBase` not `IEnumerable`. Then there is the issue that the `IsValid()` method is also based on one file (not multiple) and the script is also validating one file (not looping through each file). No time time but I'll play around with it tomorrow and let you know. –  Oct 30 '15 at 10:33
  • Sure bro.. Thank you so much.. I will also try my best to solve it.. :) – Guruprasad J Rao Oct 30 '15 at 11:45
  • 1
    See update (combines some of the suggestions I made previously) –  Oct 31 '15 at 03:01
  • One small doubt, when I say `public IEnumerable Files { get; set; }` how to create a strongly typed view for this? – Guruprasad J Rao Nov 02 '15 at 18:42
  • 1
    `public IEnumerable Files { get; set; }` needs to be a property of your `ImageUploadModel` model (instead of your current `public HttpPostedFileBase File { get; set; }`) and then in the view use `@Html.TextBoxFor(m => m.Files, new { type = "file", multiple = "multiple" })` –  Nov 02 '15 at 23:30
  • Oh.. As simple as that.. Thank you so much Stephen... :) – Guruprasad J Rao Nov 03 '15 at 01:36