How to bind posted data named "file[]" to an MVC model? - asp.net-mvc

I am using Redactor as an HTML editor, which has a component for uploading images and files.
Redactor takes care of the client side bit, and I need to provide the server side upload functionality.
I have no problem getting the uploads to work if I use Request.Files in the controller.
But I would like to bind the posted files to a Model, and I seem unable to do this, because the parameter they are sent with is files[] - with square brackets in the name.
My question:
Is it possible to bind the posted "file[]" to an MVC model? It's an invalid property name, and using file alone doesn't work.
This file input looks like this. I can specify a name other than file, but Redactor adds [] to the end, regardless of the name.
<input type="file" name="file" multiple="multiple" style="display: none;">
I am trying to bind to a property like this:
public HttpPostedFileBase[] File { get; set; }
When I watch the upload take place, I see this in the request (I presume that redactor may be adding the square brackets behind the scenes):
Content-Disposition: form-data; name="file[]"; filename="my-image.jpg"
Also relevant:
Redactor always sends the uploading request with content-type as multipart/form-data. So you don't need to add this enctype anywhere

You should create a custom model binder to bind uploaded files to one property.
First create a model with a HttpPostedFileBase[] property
public class RactorModel
{
public HttpPostedFileBase[] Files { get; set; }
}
then implement DefaultModelBinder and override BindProperty
public class RactorModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
int len = controllerContext.HttpContext.Request.Files.AllKeys.Length;
if (len > 0)
{
if (propertyDescriptor.PropertyType == typeof(HttpPostedFileBase[]))
{
string formName = string.Format("{0}[]", propertyDescriptor.Name);
HttpPostedFileBase[] files = new HttpPostedFileBase[len];
for (int i = 0; i < len; i++)
{
files[i] = controllerContext.HttpContext.Request.Files[i];
}
propertyDescriptor.SetValue(bindingContext.Model, files);
return;
}
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
Also you should add binder provider to your project, then register it in global.asax
public class RactorModenBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
if (modelType == typeof(RactorModel))
{
return new RactorModelBinder();
}
return null;
}
}
...
ModelBinderProviders.BinderProviders.Insert(0, new RactorModenBinderProvider());
this isn't a general solution, but I guess you get the point.

I encountered similar problem during the integration of jQuery.filer in an ASP.NET MVC project. As jQuery.filer adds "[]" to the end of name attribute of input (i.e. from files to files[]), I had to change the value of name attribute manually as shown below:
$('#FileUpload').attr('name', 'FileUpload');
Here is my approach used in some of project via AJAX and working without any problem. You might give a try and let me know if it works:
ViewModel:
[Display(Name = "Attachments")]
[DataType(DataType.Upload)]
public IEnumerable<HttpPostedFileBase> FileUpload { get; set; }
View:
#model ViewModel
#using (Html.BeginForm("Insert", "Controller", FormMethod.Post,
new { id = "frmCreate", enctype = "multipart/form-data" }))
{
#Html.TextBoxFor(m => m.FileUpload, new { type = "file", multiple = "multiple" })
<button id="btnSubmit" onclick="insert(event)" type="button">Save</button>
}
<script>
function insert(event) {
event.preventDefault();
//As jQuery.filer adds "[]" to the end of name attribute of input (i.e. from files to files[])
//we have to change the value of name attribute manually
$('#FileUpload').attr('name', 'FileUpload');
var formdata = new FormData($('#frmCreate').get(0));
$.ajax({
type: "POST",
url: '#Url.Action("Insert", "Cotroller")',
cache: false,
dataType: "json",
data: formdata,
/* If you are uploading files, then processData and contentType must be set
to falsein order for FormData to work (otherwise comment out both of them) */
processData: false,
contentType: false,
success: function (response, textStatus, XMLHttpRequest) {
//...
}
});
};
$(document).ready(function () {
$('#FileUpload').filer({
//code omitted for brevity
});
});
</script>
Controller:
public JsonResult Insert([Bind(Exclude = null)] ViewModel model)
{
if (ModelState.IsValid)
{
List<FileAttachment> fa = new List<FileAttachment>();
if (model.FileUpload != null)
{
FileAttachment fileAttachment = new FileAttachment //entity model
{
Created = DateTime.Now,
FileMimeType = upload.ContentType,
FileData = new byte[upload.ContentLength],
FileName = upload.FileName,
AuthorId = 1
};
upload.InputStream.Read(fileAttachment.FileData, 0, upload.ContentLength);
fa.Add(fileAttachment);
}
//code omitted for brevity
repository.SaveExperimentWithAttachment(model, fa);
return Json(new { success = true, message = "Record has been created." });
}
// If we got this far, something failed, redisplay form
return Json(new { success = false, message = "Please check the form and try again." });
}

Related

Controller name being lost when trying to post to controller action after file upload

I have a MVC Controller which contains a couple of actions. One action is responsible for changing rate. Another one is responsible for uploading a file.
the actions work correctly when I play with them. but as soon as I upload a file, if I try to change the rate the post action fails because the url it tries to post to lack the controller name in it. Here are the codes.
here is my code in the view:
Change rate:
<form method="post" action="#Url.Action("UploadPreparedContract")">
#Html.Hidden("userApplicationId", Model.UserApplicationId)
<div class="upload-section k-content">
#Html.Kendo().Upload().Name("files")
<input type="submit" value="Submit"/>
</div>
</form>
<script type="text/javascript">
jQuery(function($) {
var viewModel = kendo.observable({
currentDisclosedRate: "#Model.CurrentDisclosedRate",
changeRate: function(e) {
e.preventDefault();
var self = this;
var rawValue = $('#newDisclosureRate').val();
var rate = parseFloat(rawValue);
$.ajax({
type: "POST",
url: 'ChangeDisclosureRate',
data: { newRate: rate, userApplicationId: #Model.UserApplicationId},
}).done(function(result) {
Notification.success('Rate changed');
self.set("currentDisclosedRate", rawValue);
})
.fail(function(err) {
Notification.error('Not changed. Customer may have placed order');
});
},
});
kendo.bind($("#page"), viewModel);
});
and here is the controller
public class ContractPreparationController : Controller
{
// GET: Application/ContractPreparation
public ActionResult Index(int userApplicationId)
{
// logic to prepare model
return View(new ContractPreparationOutputModel()
{
// Model properties
});
}
[HttpPost]
public async Task<ActionResult> ChangeDisclosureRate(decimal newRate, int userApplicationId)
{
return await Command.ApplyAsync(new ChangeDisclosureRateCommand() {UserApplicationId = userApplicationId, NewDisclosureRate = BasisPoint.Percent(newRate) }) == Command.CommandResult.Succeeded
? new HttpStatusCodeResult(HttpStatusCode.OK)
: new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
[HttpPost]
public async Task<ActionResult> UploadPreparedContract(IEnumerable<HttpPostedFileBase> files, int userApplicationId)
{
if (files == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
if (files.Count() != 1)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "You must upload one file only");
var application = applicationRepository.GetUserApplication(userApplicationId);
if (application == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Invalid user");
var file = files.Single();
var memberDocument = new MemberDocument(blobService, application.FK_UserId);
await memberDocument.Uploadfile(file);
if (await Command.ApplyAsync(new UploadPreparedContractCommand() {FileGuid = memberDocument.FileGuid , UserApplicationId = userApplicationId, FileExtension = memberDocument.FileExtension}) == Command.CommandResult.Succeeded)
{
return RedirectToAction("Index", new {userApplicationId});
}
return new HttpStatusCodeResult(HttpStatusCode.InternalServerError); // No expected failure case
}
}
Use the Url.Action helper method to generate the correct relative url to the action method.
url: '#Url.Action("ChangeDisclosureRate","ContractPreparation")',
When razor executes the code for your view, it will run the Url.Action method and output the correct url (which will have the controller name if needed). You can see it if you do view source of the page.
Try adding the controller name to your ajax url parameter:
url: 'ContractPreparation/ChangeDisclosureRate'
Otherwise MVC doesn't know what controller to use.

MVC Custom Validation for List on Client Side

I'm trying to write a custom validator that works on the client side that validates that all checkboxes have been ticked.
Here's the declaration on the model:
[DeclarationsAccepted(ErrorMessage = "You must tick all declarations")]
public IList<DeclarationQuestion> DeclarationQuestions { get; set; }
And here's the attribute:
public class DeclarationsAccepted : ValidationAttribute, IClientValidatable
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var questions = value as IList<DeclarationQuestion>;
if (questions != null && questions.All(c => c.Answer))
{
return ValidationResult.Success;
}
return new ValidationResult("You must accepted all declarations to continue");
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "declarationsaccepted",
ErrorMessage = FormatErrorMessage(metadata.DisplayName)
};
yield return modelClientValidationRule;
}
}
So far so good, works server side.
For the client I'm wiring this up as follows:
jQuery.validator.addMethod('declarationsaccepted', function (value, element, params) {
//Implement logic here to check all boxes are ticked
console.log(value);
return false;
}, '');
jQuery.validator.unobtrusive.adapters.add('declarationsaccepted', {}, function (options) {
options.rules['declarationsaccepted'] = true;
options.messages['declarationsaccepted'] = options.message;
});
I'm displaying the checkboxes like this:
#{ var questionIndex = 0; }
#foreach (var question in Model.DeclarationQuestions)
{
#Html.CheckBoxFor(model => Model.DeclarationQuestions[questionIndex].Answer, new { id = "DeclarationQuestions" + questionIndex})
questionIndex++;
}
And then displaying the validation message using this:
#Html.ValidationMessageFor(c => c.DeclarationQuestions)
When I submit the form the message is displayed but only after a post back to the server. Is there any way to get this to work on the client side?
The reason you will not get client side validation is because the html helpers generate data-val-* attributes for controls associated with properties. jquery.validate.unobtrusive reads those attributes when the form is parsed and using rules, displays an error message in the appropriate element generated by ValidationMessageFor() associated with that control (it does this by matching up the id attributes of the elements - the error message is generated in a span with <span for="TheIdOfTheAssociatedControl" ...>).
You don't (and cant) generate a control for property DeclarationQuestions (only for properties of each item in DeclarationQuestions so there is nothing that can be matched up.
You could handle this by including your own error message placeholder and intercepting the .submit event
html (add css to style #conditions-error as display:none;)
<span id="delarations-error" class="field-validation-error">
<span>You must accept all declarations to continue.</span>
</span>
Script
var declarationsError = $('#delarations-error');
$('form').submit(function() {
var isValid = $('.yourCheckBoxClass').not(':checked') == 0;
if(!isValid) {
declarationsError.show(); // display error message
return false; // prevent submit
}
});
$('.yourCheckBoxClass').change(function() {
if($(this).is(':checked')) {
declarationsError.hide(); // hide error message
}
});
Side note: You loop for generating the checkboxes should be
for (int i = 0; i < Model.DeclarationQuestions.Count; i++)
{
#Html.CheckBoxFor(m => m.DeclarationQuestions[i].Answer, new { id = "DeclarationQuestions" + i})
}

How do I prevent IE save file dialog when using fileupload in asp.net mvc

When I try and upload a file using ASP.NET MVC it works fine in FF && Chrome, but in IE and Opera a dialog pops-up which asks me to either download, save or cancel.
Choosing either of the options, prevents the fileupload from working. How can I get round this problem?
public class ImportModel
{
[Required]
[FileExtensions("csv", ErrorMessage = "Please upload a valid .csv file")]
public HttpPostedFileBase File { get; set; }
}
[HttpPost]
public virtual ActionResult StartImport(ImportModel model = null)
{
if (model != null)
{
var status = _importService.StartImport(model);
return Json(status, JsonRequestBehavior.AllowGet);
}
return null;
}
View code below (summarised):
<div id="dlgImport" class="hidden">
#using (Html.BeginForm(MVC.Import.StartImport(), FormMethod.Post, new { #class = "smallForm", id = "frmImport", enctype = "multipart/form-data" }))
{
<div class="fields-inline">
<div class="editor-label">
#Html.Label("File")
</div>
<div class="editor-field">
#Html.TextBoxFor(x => x.File, new { #class="input-file", type = "file" })
#Html.ValidationMessageFor(x => x.File)
</div>
</div>
}
</div>
</div>
$(function() {
$("#frmImport").ajaxForm({
success: function (responseHtml) {
// response is wrapped in pre tags by the browser - the ajax upload is carried out using an iframe
var response = $.parseJSON($(responseHtml).text());
}
});
});
var vm = {
startImport: function () {
if ($("#frmImport").valid()) {
$("#frmImport").submit();
}
}
}
Now that you have posted your code it looks like that you are using the jquery form plugin. As explained in the documentation this plugin supports file uploads using AJAX but you cannot return JSON from your server side script:
Since it is not possible to upload files using the browser's
XMLHttpRequest object, the Form Plugin uses a hidden iframe element to
help with the task. This is a common technique, but it has inherent
limitations. The iframe element is used as the target of the form's
submit operation which means that the server response is written to
the iframe. This is fine if the response type is HTML or XML, but
doesn't work as well if the response type is script or JSON, both of
which often contain characters that need to be repesented using entity
references when found in HTML markup.
To account for the challenges of script and JSON responses, the Form
Plugin allows these responses to be embedded in a textarea element and
it is recommended that you do so for these response types when used in
conjuction with file uploads. Please note, however, that if there is
no file input in the form then the request uses normal XHR to submit
the form (not an iframe). This puts the burden on your server code to
know when to use a textarea and when not to.
So basically your upload controller action should respond with:
<textarea>{"foo":"bar"}</textarea>
instead of:
{"foo":"bar"}
Also you should not use the application/json response content type in this case.
You could write a custom action result to achieve that:
public class FileJsonResult : JsonResult
{
public FileJsonResult(object data)
: base()
{
Data = data;
JsonRequestBehavior = JsonRequestBehavior.AllowGet;
}
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.Write("<textarea>");
base.ExecuteResult(context);
context.HttpContext.Response.Write("</textarea>");
context.HttpContext.Response.ContentType = "text/html";
}
}
and then:
[HttpPost]
public virtual ActionResult StartImport(ImportModel model = null)
{
if (model != null)
{
var status = _importService.StartImport(model);
return new FileJsonResult(status);
}
new FileJsonResult(new { status = false, errorMessage = "The model was null" });
}
Now you might also need to adapt your success handler to strip the <textarea> tags:
$('#frmImport').ajaxForm({
success: function (responseHtml) {
var responseHtml = responseHtml
.replace(/\<textarea\>/i, '')
.replace(/\<\/textarea\>/i, '');
var response = $.parseJSON(responseHtml);
// do something with the response
}
});
I had the same problem with IE8 and this answer helped me a lot. But I needed to make some changes that worked very well in IE8, Chrome and Firefox.
Follow changes below:
success: function (responseText) {
try{
var response = $.parseJSON(responseText);
//code ...
}
catch(ex) {
//error code
}
}
[HttpPost]
public JsonResult Upload(HttpPostedFileBase file) {
//code
return Json(new { Success = "true", Message = "Done!" }, "text/html");
}

MVC3 - posting byte array to a controller - Database RowVersion

I am working on an MVC3 application. My client side ViewModel contains a SQL Server RowVersion property, which is a byte[]. It is rendered as an Object array on the client side. When I attempt to post my view model to a controller, the RowVersion property is always null.
I am assuming that the Controller serializer (JsonValueProviderFactory) is ignoring the Object array property.
I have seen this blog, however this does not apply, as I am posting JSON and not the form markup:
http://thedatafarm.com/blog/data-access/round-tripping-a-timestamp-field-with-ef4-1-code-first-and-mvc-3/
My view renders my viewmodel like so:
<script type="text/javascript">
var viewModel = #Html.Raw( Json.Encode( this.Model ) );
</script>
I then post the viewModel to the controller like so:
var data = {
'contact': viewModel
};
$.ajax({
type: 'POST',
url: '/Contact/Save',
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: 'json',
success: function (data) {
// Success
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(XMLHttpRequest.responseText);
}
});
Here is my action in the controller:
[HttpPost]
public JsonResult Save(Contact contact) {
return this.Json( this._contactService.Save( contact ) );
}
UPDATE: based on Darin's answer.
I was hoping for a cleaner solution, but since Darin provided the only answer, I will have to add a custom property that will serialize my byte[] "row_version" property to a Base64 string. And when the Base64 string is set to the new custom property, it converts the string back to a byte[]. Below is the custom "RowVersion" property that I added to my model:
public byte[] row_version {
get;
set;
}
public string RowVersion {
get {
if( this.row_version != null )
return Convert.ToBase64String( this.row_version );
return string.Empty;
}
set {
if( string.IsNullOrEmpty( value ) )
this.row_version = null;
else
this.row_version = Convert.FromBase64String( value );
}
}
My client side ViewModel contains a SQL Server RowVersion property, which is a byte[]
Make it so that instead of a byte[] your view model contains a string property which is the base64 representation of this byte[]. Then you won't have any problems roundtripping it to the client and back to the server where you will be able to get the original byte[] from the Base64 string.
Json.NET automatically encodes byte arrays as Base64.
You can use JsonNetResult instead of JsonResult:
from https://gist.github.com/DavidDeSloovere/5689824:
using System;
using System.Web;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public class JsonNetResult : JsonResult
{
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
var response = context.HttpContext.Response;
response.ContentType = !string.IsNullOrEmpty(this.ContentType) ? this.ContentType : "application/json";
if (this.ContentEncoding != null)
{
response.ContentEncoding = this.ContentEncoding;
}
if (this.Data == null)
{
return;
}
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
jsonSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
var formatting = HttpContext.Current != null && HttpContext.Current.IsDebuggingEnabled ? Formatting.Indented : Formatting.None;
var serializedObject = JsonConvert.SerializeObject(Data, formatting, jsonSerializerSettings);
response.Write(serializedObject);
}
}
Usage:
[HttpPost]
public JsonResult Save(Contact contact) {
return new JsonNetResult { Data = _contactService.Save(contact) };
}

MVC3 unobtrusive validation group of inputs

I need to validate 3 or more input fields (required at lest one). For example I have Email, Fax, Phone.
I require at least ONE to be filled in. I need both server and client 'unobtrusive validation'. please help. I looked into "Compare" method and tried modifying it but no luck. please help.
thanks
You could write a custom attribute:
public class AtLeastOneRequiredAttribute : ValidationAttribute, IClientValidatable
{
private readonly string[] _properties;
public AtLeastOneRequiredAttribute(params string[] properties)
{
_properties = properties;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (_properties == null || _properties.Length < 1)
{
return null;
}
foreach (var property in _properties)
{
var propertyInfo = validationContext.ObjectType.GetProperty(property);
if (propertyInfo == null)
{
return new ValidationResult(string.Format("unknown property {0}", property));
}
var propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
if (propertyValue is string && !string.IsNullOrEmpty(propertyValue as string))
{
return null;
}
if (propertyValue != null)
{
return null;
}
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = ErrorMessage,
ValidationType = "atleastonerequired"
};
rule.ValidationParameters["properties"] = string.Join(",", _properties);
yield return rule;
}
}
which could be used to decorate one of your view model properties (the one you want to get highlighted if validation fails):
public class MyViewModel
{
[AtLeastOneRequired("Email", "Fax", "Phone", ErrorMessage = "At least Email, Fax or Phone is required")]
public string Email { get; set; }
public string Fax { get; set; }
public string Phone { get; set; }
}
and then a simple controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
Rendering the following view which will take care of defining the custom client side validator adapter:
#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">
jQuery.validator.unobtrusive.adapters.add(
'atleastonerequired', ['properties'], function (options) {
options.rules['atleastonerequired'] = options.params;
options.messages['atleastonerequired'] = options.message;
}
);
jQuery.validator.addMethod('atleastonerequired', function (value, element, params) {
var properties = params.properties.split(',');
var values = $.map(properties, function (property, index) {
var val = $('#' + property).val();
return val != '' ? val : null;
});
return values.length > 0;
}, '');
</script>
#using (Html.BeginForm())
{
#Html.ValidationSummary(false)
<div>
#Html.LabelFor(x => x.Email)
#Html.EditorFor(x => x.Email)
</div>
<div>
#Html.LabelFor(x => x.Fax)
#Html.EditorFor(x => x.Fax)
</div>
<div>
#Html.LabelFor(x => x.Phone)
#Html.EditorFor(x => x.Phone)
</div>
<input type="submit" value="OK" />
}
Of course the custom adapter and validator rule should be externalized into a separate javascript file to avoid mixing script with markup.
I spent more than 36 hours why the code did not work for me.. At the end , I found out that in my case , I was not supposed to use the property names in this line of code
[AtLeastOneRequired("Email", "Fax", "Phone", ErrorMessage = "At least Email, Fax or Phone is required")]
But I had to use the HTMl element Ids in place of the property names and it worked like magic.
Posting this here if it might help somebody.
Since you are using MVC 3, take a look at great video Brad Wilson had on mvcConf. There's everything you need to create client + server Unobtrusive Validation
#Darin Dimitrov 's solution is probably the standard of creating a custom validation attribute that works with unobtrusive validation. However, using custom validation attributes for unobtrusive validation have some disadvantages such as:
The custom validation attribute is only attached to one properties, so client validation will not work if there's a change event on the other two inputs.
The error message works fine with ValidationSummary, but if you want to display 1 error message for the whole group (which I think is the norm), it would be nearly impossible.
To avoid the first problem, we can add custom validation attribute to each element in the group, which will cause another problem: we have to validate all elements of the group, instead of stopping at the first invalid group element. And of course, the second problem - separate error messages for each element - still remains.
There's another way to handle client side validation of group of inputs, using groups setting in jquery validator (https://jqueryvalidation.org/validate/#groups). The only problem (and a big one) is that unobtrusive validation doesn't support jquery validation's groups by default, so we have to customize a little bit.
Although this answer is hardly "unobtrusive", in my opinion, it is worth trying to get rid of unnecessary complication of code, if your final goal is to validate a group of inputs while using Microsoft's unobtrusive validator library.
First, because groups settings of default jquery validator is not available in jquery unobtrusive validator, we have to override unobtrusive settings (ref. How can I customize the unobtrusive validation in ASP.NET MVC 3 to match my style?)
$("form").on('submit', function () {
var form = this;
var validator = $(this).data("validator");
if (validator.settings && !validator.settings.submitHandler) {
$.extend(true, validator.settings.rules, validationSettings.rules);
$.extend(true, validator.settings.groups, validationSettings.groups);
initGroups(validator);
var fnErrorReplacement = validator.settings.errorPlacement;
validator.settings.errorPlacement = function (error, element) {
validationSettings.errorPlacement(error, element, fnErrorReplacement, form);
}
validator.settings.submitHandler = formSubmitHandler;
}
});
function formSubmitHandler(form) {
form.submit();
}
After that, override unobtrusive validator's groups, rules and errorPlacement settings.
var validationSettings = {
groups: {
checkboxgroup: "Email Fax Phone"
},
rules: {
Email: {
required: function () {
return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
}
},
Fax: {
required: function () {
return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
}
},
Phone: {
required: function () {
return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
}
}
}
,
errorPlacement: function (error, element, fnUnobtrusive, form) {
switch (element.attr("name")) {
case "Email":
case "Fax":
case "Phone":
onGroupError(error, "CheckBoxGroup", form);
break;
default:
fnUnobtrusive(error, element);
break;
}
}
}
function validateCheckboxGroup(names) {
var result = true;
$.each(names, function (index, value) {
if ($(value).is(":checked")) {
result = false;
}
});
return result;
}
Because unobtrusive validator does not implement groups setting of jquery validator, we need to reuse two functions from the two libraries to: (1).split group names (reusing code from jquery validator) and (2) append error element without remove 'input-validation-error' class (reusing function onError from unobtrusive library).
function initGroups(validators) {
validators.groups = {};
$.each(validators.settings.groups,
function (key, value) {
if (typeof value === "string") {
value = value.split(/\s/);
}
$.each(value,
function (index, name) {
validators.groups[name] = key;
});
});
}
function onGroupError(error, inputElementName, form) {
var container = $(form).find("[data-valmsg-for='" + inputElementName + "']"),
replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;
container.removeClass("field-validation-valid").addClass("field-validation-error");
error.data("unobtrusiveContainer", container);
if (replace) {
container.empty();
error.appendTo(container);
}
else {
error.hide();
}
}
Finally, use HtmlExtensions.ValidationMessage to create error span of the checkbox group.
#Html.ValidationMessage("CheckBoxGroup", new { #class = "text-danger" })
The keeping of "input-validation-error" class is necessary, so that jquery validator will validate all 3 elements (Email, Phone, Fax) of checkbox group as a whole, instead of validating one by one. The unobtrusive validation library remove this class by default on function onError, so we have to customize this as shown in function onGroupError above.

Resources