Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JQuery Validation: Add custom method to validate on submit

I've got a simple js/jquery function I want to run when the form is submitted to do extra validation. My form allows multiple file inputs, and I want to make sure the combined total of all files is under a file limit I've set.

var totalFileSize = 0;
$("input:file").each(function () {
    var file = $(this)[0].files[0];
    if (file) totalFileSize += file.size;
});

return totalFileSize < maxFileSize; // I've set maxFileSize elsewhere.

The problem is that I want to run this as part of jquery validation. My form, in other places, uses standard MVC3 validation and a separate custom unobtrusive validation I've wrote. If this File Size validator fails, I want it to behave like those other jquery validators do: I want to, obviously, stop the submit, and display an error message in the same summary box as the others.

Is there some way to just call a simple method like this as part of validation on submit? I thought about $.validator.addMethod, but if I were to add that to each input:file element it would run the same validator multiple times on submit, and thus show the error message more than once. It would be great if there's a way to add this validator but not tie it to any elements.

like image 530
Ber'Zophus Avatar asked Dec 27 '22 23:12

Ber'Zophus


1 Answers

You could write a custom validation attribute and register a custom client side adapter. Let me elaborate.

Let's suppose that you have a view model to represent a list of files to upload and that you want to limit the total size of all uploaded files to 2 MB. Your view model might certainly look something along the lines of:

public class MyViewModel
{
    [MaxFileSize(2 * 1024 * 1024, ErrorMessage = "The total file size should not exceed {0} bytes")]
    public IEnumerable<HttpPostedFileBase> Files { get; set; }
}

Now let's define this custom [MaxFileSize] validation attribute which will obviously perform the server side validation but in addition to that it will implement the IClientValidatable interface allowing to register a custom unobtrusive client side validation rule that will allow to transpose this validation logic on the client (obviously only for browsers that support the HTML5 File API allowing you to determine on the client the size of the selected file => IE is totally out of the question for something like this and users that use this browser will have to satisfy them with server side only validation or do something better - use Internet Explorer for the only useful task in this world that this peace of software can do: go over the internet once you get a clean install of Windows to download a real web browser):

public class MaxFileSizeAttribute : ValidationAttribute, IClientValidatable
{
    public MaxFileSizeAttribute(int maxTotalSize)
    {
        MaxTotalSize = maxTotalSize;
    }

    public int MaxTotalSize { get; private set; }

    public override bool IsValid(object value)
    {
        var files = value as IEnumerable<HttpPostedFileBase>;
        if (files != null)
        {
            var totalSize = files.Where(x => x != null).Sum(x => x.ContentLength);
            return totalSize < MaxTotalSize;
        }

        return true;
    }

    public override string FormatErrorMessage(string name)
    {
        return base.FormatErrorMessage(MaxTotalSize.ToString());
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = FormatErrorMessage(MaxTotalSize.ToString()),
            ValidationType = "maxsize"
        };
        rule.ValidationParameters["maxsize"] = MaxTotalSize;
        yield return rule;
    }
}

The next step is to have a controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new MyViewModel());
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        if (!ModelState.IsValid)
        {
            // Server side validation failed => redisplay the view so
            // that the user can fix his errors
            return View(model);
        }

        // Server side validation passed => here we can process the
        // model.Files collection and do something useful with the 
        // uploaded files knowing that their total size will be smaller
        // than what we have defined in the custom MaxFileSize attribute
        // used on the view model
        // ...

        return Content("Thanks for uploading all those files");
    }
}

and a corresponding view:

@model MyViewModel

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script type="text/javascript">
    (function ($) {
        $.validator.unobtrusive.adapters.add('maxsize', ['maxsize'], function (options) {
            options.rules['maxsize'] = options.params;
            if (options.message) {
                options.messages['maxsize'] = options.message;
            }
        });

        $.validator.addMethod('maxsize', function (value, element, params) {
            var maxSize = params.maxsize;
            var $element = $(element);
            var files = $element.closest('form').find(':file[name=' + $element.attr('name') + ']');
            var totalFileSize = 0;
            files.each(function () {
                var file = $(this)[0].files[0];
                if (file && file.size) {
                    totalFileSize += file.size;
                }
            });
            return totalFileSize < maxSize;
        }, '');
    })(jQuery);
</script>


@Html.ValidationMessageFor(x => x.Files)
@using (Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    <div>
        @foreach (var item in Enumerable.Range(1, 3))
        {
            @Html.TextBoxFor(x => x.Files, new { type = "file" })
        }
    </div>
    <button type="submit">OK</button>
}

Obviously the javascript shown here have nothing to do inside a view. It must go into a separate reusable javascript file that the view could reference. I have included it inline here for more readability and ease of reproducing the scenario but in a real world never write inline javascript.

like image 89
Darin Dimitrov Avatar answered Jan 31 '23 08:01

Darin Dimitrov