Share via


MVC File upload and unobtrusive validation

Upload a file with MVC and validate it with MVC and unobtrusive validation

You can download the code here

Introduction

This example will show you how to upload a file to a MVC controller, and how to validate the file on the client side(JavaScript) and server side.

First we need to create a form with a input type of upload

@using (Html.BeginForm("Index", "Profile", FormMethod.Post, new { @class = "form-horizontal", enctype = "multipart/form-data" }))
{
    <div class="form-group">
        @Html.LabelFor(m => m.Username, new { @class = "col-sm-2 control-label" })
        <div class="col-sm-10">
            @Html.TextBoxFor(m => m.Username, new { @class = "form-control", placeholder = "Username" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.FirstName, new { @class = "col-sm-2 control-label" })
        <div class="col-sm-10">
            @Html.TextBoxFor(m => m.FirstName, new { @class = "form-control", placeholder = "First Name" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.LastName, new { @class = "col-sm-2 control-label" })
        <div class="col-sm-10">
            @Html.TextBoxFor(m => m.LastName, new { @class = "form-control", placeholder = "Last Name" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Avatar, new { @class = "col-sm-2 control-label" })
        <div class="col-sm-10">
            @Html.TextBoxFor(m => m.Avatar, new { @class = "form-control", type = "file" })
            @Html.ValidationMessageFor(m => m.Avatar)
        </div>
    </div>    
    <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
            <button type="submit" class="btn btn-default">Save</button>
        </div>
    </div>
}

NOTE: The form enctype needs to be multipart/form-data

We have a simple controller that accepts the ProfileModel

public class  ProfileController : Controller
{
     [HttpGet]
     public ActionResult Index()
     {
         return View(new ProfileModel());
     }
  
     [HttpPost]
     public ActionResult Index(
         ProfileModel model)
     {
        if(!ModelState.IsValid)
        {
            return View(model);
        }
  
         return View(model);
     }
}

The ProfileModel code:

public class  ProfileModel
{
  public string  Username { get; set; }
  public string  FirstName { get; set; }
  public string  LastName { get; set; }
 
  [MinimumFileSizeValidator(0.5)]
  [MaximumFileSizeValidator(2.4)]
  [ValidFileTypeValidator("png")]
  //[FileUploadValidator(0.5, 2.4, "png")]
  public HttpPostedFileBase Avatar { get; set; }
}

The ProfileModel uses the following validators: MinimumFileSizeValidator,MaximumFileSizeValidator, ValidFileTypeValidator and FileUploadValidator

MinimumFileSizeValidator

public class  MinimumFileSizeValidator
    : ValidationAttribute, IClientValidatable
{        
    private string  _errorMessage = "{0} can not be smaller than {1} MB";     
 
    /// <summary>
    /// Minimum file size in MB
    /// </summary>
    public double  MinimumFileSize { get; private  set; }
 
    /// <param name="minimumFileSize">MinimumFileSize file size in MB</param>
    public MinimumFileSizeValidator(
        double minimumFileSize)
        : base()
    {
        MinimumFileSize = minimumFileSize;
    }
 
    public override  bool IsValid(
            object value)
    {
        if (value == null)
        {
            return true;
        }
 
        if (!IsValidMinimumFileSize((value as HttpPostedFileBase).ContentLength))
        {
            ErrorMessage = String.Format(_errorMessage, "{0}", MinimumFileSize);
            return false;
        }
 
        return true;
    }
 
    public override  string FormatErrorMessage(
        string name)
    {
        return String.Format(_errorMessage, name, MinimumFileSize);
    }
 
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
            ModelMetadata metadata
        , ControllerContext context)
    {
        var clientValidationRule = new  ModelClientValidationRule()
        {
            ErrorMessage   = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "minimumfilesize"
        };
 
        clientValidationRule.ValidationParameters.Add("size", MinimumFileSize);
 
        return new[] { clientValidationRule };
    }
 
    private bool  IsValidMinimumFileSize(
        int fileSize)
    {
        return ConvertBytesToMegabytes(fileSize) >= MinimumFileSize;
    }
 
    private double  ConvertBytesToMegabytes(
        int bytes)
    {
        return (bytes / 1024f) / 1024f;
    }        
}

MaximumFileSizeValidator

public class  MaximumFileSizeValidator
    : ValidationAttribute, IClientValidatable
{        
    private string  _errorMessage = "{0} can not be larger than {1} MB";        
 
    /// <summary>
    /// Maximum file size in MB
    /// </summary>
    public double  MaximumFileSize { get; private  set; }
 
    /// <param name="maximumFileSize">Maximum file size in MB</param>
    public MaximumFileSizeValidator(
        double maximumFileSize)
    {
        MaximumFileSize = maximumFileSize;
    }
 
    public override  bool IsValid(
        object value)
    {
        if (value == null) 
        {
            return true;
        }
 
        if (!IsValidMaximumFileSize((value as HttpPostedFileBase).ContentLength))
        {
            ErrorMessage = String.Format(_errorMessage, "{0}", MaximumFileSize);
            return false;
        }
 
        return true;
    }
 
    public override  string FormatErrorMessage(
        string name)
    {
        return String.Format(_errorMessage, name, MaximumFileSize);
    }
 
    public System.Collections.Generic.IEnumerable<ModelClientValidationRule> GetClientValidationRules(
            ModelMetadata metadata
        , ControllerContext context)
    {
        var clientValidationRule = new  ModelClientValidationRule()
        {
            ErrorMessage   = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "maximumfilesize"
        };
 
        clientValidationRule.ValidationParameters.Add("size", MaximumFileSize);
 
        return new[] { clientValidationRule };
    }
 
    private bool  IsValidMaximumFileSize(
        int fileSize)
    {
        return (ConvertBytesToMegabytes(fileSize) <= MaximumFileSize);
    }
 
    private double  ConvertBytesToMegabytes(
        int bytes)
    {
        return (bytes / 1024f) / 1024f;
    }   
}

ValidFileTypeValidator

public class  ValidFileTypeValidator
    : ValidationAttribute, IClientValidatable
{        
    private string  _errorMessage = "{0} must be one of the following file types: {1}";    
 
    /// <summary>
    /// Valid file extentions
    /// </summary>
    public string[] ValidFileTypes { get; private  set; }
 
    /// <param name="validFileTypes">Valid file extentions(without the dot)</param>
    public ValidFileTypeValidator(
        params string[] validFileTypes)
    {
        ValidFileTypes = validFileTypes;
    }
 
    public override  bool IsValid(
        object value)
    {
        var file = value as  HttpPostedFileBase;
 
        if (value == null || String.IsNullOrEmpty(file.FileName))
        {
            return true;
        }
 
        if (ValidFileTypes != null)
        {
            var validFileTypeFound = false;
 
            foreach (var validFileType in ValidFileTypes)
            {
                var fileNameParts = file.FileName.Split('.');
                if (fileNameParts[fileNameParts.Length - 1] == validFileType)
                {
                    validFileTypeFound = true;
                    break;
                }
            }
 
            if (!validFileTypeFound)
            {
                ErrorMessage = String.Format(_errorMessage, "{0}", ValidFileTypes.ToConcatenatedString(","));
                return false;
            }
        }
 
        return true;
    }
 
    public override  string FormatErrorMessage(
        string name)
    {
        return String.Format(_errorMessage, name, ValidFileTypes.ToConcatenatedString(","));
    }
 
    public System.Collections.Generic.IEnumerable<ModelClientValidationRule> GetClientValidationRules(
            ModelMetadata metadata
        , ControllerContext context)
    {
        var clientValidationRule = new  ModelClientValidationRule()
        {
            ErrorMessage   = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "validfiletype"
        };
 
        clientValidationRule.ValidationParameters.Add("filetypes", ValidFileTypes.ToConcatenatedString(","));
 
        return new[] { clientValidationRule };
    }
}

FileUploadValidator

public class  FileUploadValidator
    : ValidationAttribute, IClientValidatable
{   
    private MinimumFileSizeValidator _minimumFileSizeValidator;
    private MaximumFileSizeValidator _maximumFileSizeValidator;
    private ValidFileTypeValidator   _validFileTypeValidator;                 
         
    /// <param name="validFileTypes">Valid file extentions(without the dot)</param>
    public FileUploadValidator(              
            params string[] validFileTypes)
        : base()
    {
        _validFileTypeValidator = new  ValidFileTypeValidator(validFileTypes);
    }
         
    /// <param name="maximumFileSize">Maximum file size in MB</param>
    /// <param name="validFileTypes">Valid file extentions(without the dot)</param>
    public FileUploadValidator(              
            double maximumFileSize
        , params  string[] validFileTypes)
        : base()
    {
        _maximumFileSizeValidator = new  MaximumFileSizeValidator(maximumFileSize);
        _validFileTypeValidator   = new  ValidFileTypeValidator(validFileTypes);
    }
 
    /// <param name="minimumFileSize">MinimumFileSize file size in MB</param>
    /// <param name="maximumFileSize">Maximum file size in MB</param>
    /// <param name="validFileTypes">Valid file extentions(without the dot)</param>
    public FileUploadValidator(
            double minimumFileSize
        , double  maximumFileSize
        , params  string[] validFileTypes)
        : base()
    {
        _minimumFileSizeValidator = new  MinimumFileSizeValidator(minimumFileSize);
        _maximumFileSizeValidator = new  MaximumFileSizeValidator(maximumFileSize);
        _validFileTypeValidator = new  ValidFileTypeValidator(validFileTypes);
    }
 
    protected override  ValidationResult IsValid(
            object value
        , ValidationContext validationContext)
    {
        if (value == null)
        {
            return ValidationResult.Success;
        }
 
        try
        {
            if (value.GetType() != typeof(HttpPostedFileWrapper))
            {
                throw new  InvalidOperationException("");
            }
 
            var errorMessage = new  StringBuilder();
            var file = value as  HttpPostedFileBase;
 
            if (_minimumFileSizeValidator != null)
            {
                if (!_minimumFileSizeValidator.IsValid(file))
                {
                    errorMessage.Append(String.Format("{0}. ", _minimumFileSizeValidator.FormatErrorMessage(validationContext.DisplayName)));                        
                }
            }
 
            if (_maximumFileSizeValidator!=null)
            {
                if (!_maximumFileSizeValidator.IsValid(file))
                {
                    errorMessage.Append(String.Format("{0}. ", _maximumFileSizeValidator.FormatErrorMessage(validationContext.DisplayName)));
                }
            }
 
            if (_validFileTypeValidator != null)
            {
                if (!_validFileTypeValidator.IsValid(file))
                {
                    errorMessage.Append(String.Format("{0}. ", _validFileTypeValidator.FormatErrorMessage(validationContext.DisplayName)));
                }
            }
 
            if (String.IsNullOrEmpty(errorMessage.ToString()))
            {
                return ValidationResult.Success;
            }
            else
            {
                return new  ValidationResult(errorMessage.ToString());
            }
        }
        catch(Exception excp)
        {
            return new  ValidationResult(excp.Message);
        }
    }
 
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
            ModelMetadata metadata
        , ControllerContext context)
    {
        var clientValidationRule = new  ModelClientValidationRule()
        {
            ErrorMessage   = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "fileuploadvalidator"
        };
             
        var clientvalidationmethods  = new  List<string>();
        var parameters               = new  List<string>();
        var errorMessages            = new  List<string>();
 
        if (_minimumFileSizeValidator != null) 
        {
            clientvalidationmethods.Add(_minimumFileSizeValidator.GetClientValidationRules(metadata, context).First().ValidationType);
            parameters.Add(_minimumFileSizeValidator.MinimumFileSize.ToString());
            errorMessages.Add(_minimumFileSizeValidator.FormatErrorMessage(metadata.GetDisplayName()));
        }
 
        if (_maximumFileSizeValidator != null)
        {
            clientvalidationmethods.Add(_maximumFileSizeValidator.GetClientValidationRules(metadata, context).First().ValidationType);
            parameters.Add(_maximumFileSizeValidator.MaximumFileSize.ToString());
            errorMessages.Add(_maximumFileSizeValidator.FormatErrorMessage(metadata.GetDisplayName()));
        }
 
        if (_validFileTypeValidator != null)
        {
            clientvalidationmethods.Add(_validFileTypeValidator.GetClientValidationRules(metadata, context).First().ValidationType);
            parameters.Add(String.Join(",", _validFileTypeValidator.ValidFileTypes));
            errorMessages.Add(_validFileTypeValidator.FormatErrorMessage(metadata.GetDisplayName()));
        }
 
        clientValidationRule.ValidationParameters.Add("clientvalidationmethods", clientvalidationmethods.ToConcatenatedString(","));
        clientValidationRule.ValidationParameters.Add("parameters"             , parameters.ToConcatenatedString("|"));
        clientValidationRule.ValidationParameters.Add("errormessages"          , errorMessages.ToConcatenatedString(","));
 
        yield return  clientValidationRule;
    }              
 
    private double  ConvertBytesToMegabytes(
        long bytes) 
    {
        return (bytes / 1024f) / 1024f;
    } 
}

This concludes the server side validation.

Each validator has a corresponding client-side validator that does the client side validation.

First we need to call the addSingleVal function for each validator

$.validator.unobtrusive.adapters.addSingleVal("minimumfilesize", "size");
$.validator.unobtrusive.adapters.addSingleVal("maximumfilesize", "size");
$.validator.unobtrusive.adapters.addSingleVal("validfiletype", "filetypes");

We can then add the validators

minimumfilesize validator

$.validator.addMethod('minimumfilesize', function  (value, element, minSize) {
 return convertBytesToMegabytes(element.files[0].size) >= parseFloat(minSize);
});

maximumfilesize validator

$.validator.addMethod('maximumfilesize', function  (value, element, maxSize) {
 return convertBytesToMegabytes(element.files[0].size) <= parseFloat(maxSize);
});

validfiletype validator

$.validator.addMethod('validfiletype', function  (value, element, validFileTypes) {
 if (validFileTypes.indexOf(',') > -1) {
   validFileTypes = validFileTypes.split(',');
 } else  {
   validFileTypes = [validFileTypes];
 }
 
 var fileType = value.split('.')[value.split('.').length - 1];
 
 for (var i = 0; i < validFileTypes.length; i++) {
  if (validFileTypes[i] === fileType) {
   return true;
  }
 }
 return false;
});

fileuploadvalidator

$.validator.unobtrusive.adapters.add('fileuploadvalidator', ['clientvalidationmethods', 'parameters',  'errormessages'],  function  (options) {
 options.rules['fileuploadvalidator'] = {
    clientvalidationmethods: options.params['clientvalidationmethods'].split(','),
    parameters: options.params['parameters'].split('|'),
    errormessages: options.params['errormessages'].split(',')
 };
});
$.validator.addMethod("fileuploadvalidator", function  (value, element, param) {
 if (value == ""  || value == null  || value == undefined) {
    return true;
 }
 //array of jquery validation rule names
 var validationrules = param["clientvalidationmethods"];
 
 //array of paramteres required by rules, in this case regex patterns
 var patterns = param["parameters"];
 
 //array of error messages for each rule
 var rulesErrormessages = param["errormessages"];
 
 var validNameErrorMessage = new Array();
 var index = 0
 
 for (i = 0; i < patterns.length; i++) {
    var valid = true;
    var pattern = patterns[i].trim();
 
    //get a jquery validator method.  
    var rule = $.validator.methods[validationrules[i].trim()];
 
    //create a paramtere object
    var parameter = new Object();
    parameter = pattern;
 
    //execute the rule
    var isValid = rule.call(this, value, element, parameter);
 
    if (!isValid) {
        //if rule fails, add error message
        validNameErrorMessage[index] = rulesErrormessages[i];
        index++;
    }
 }
 //if we have more than on error message, one of the rule has failed
 if (validNameErrorMessage.length > 0) {
    //update the error message for 'validname' rule
    $.validator.messages.fileuploadvalidator = validNameErrorMessage.toString();
    return false;
 }
 return true;
 }, "The file is not valid"//default error message
);

and the convertBytesToMegabytes function

function convertBytesToMegabytes(bytes) {
 return (bytes / 1024) / 1024;
}