How can I ignore case in a RegularExpression? - asp.net-mvc

I have an asp.net MVC application. There is an entity called File that it has a property called Name.
using System.ComponentModel.DataAnnotations;
public class File {
...
[RegularExpression(#"([^.]+[.](jpg|jpeg|gif|png|wpf|doc|docx|xls|xlsx ..., ErrorMessage = "Invali File Name"]
public string Name{ get; set; }
...
}
There is a RegularExpressionValidator that checks file extensions.
Is there a quick way I can tell it to ignore the case of the extension without having to explicitly add the upper case variants to my validation expression?
I need this RegularExpressionValidator for both Server-side and client-side.
"(?i)" can be used for Server-side, but this doesn't work client-side

One way I can think of is writing a custom validation attribute:
public class IgnorecaseRegularExpressionAttribute : RegularExpressionAttribute, IClientValidatable
{
public IgnorecaseRegularExpressionAttribute(string pattern): base("(?i)" + pattern)
{ }
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "icregex",
ErrorMessage = ErrorMessage
};
// Remove the (?i) that we added in the pattern as this
// is not necessary for the client validation
rule.ValidationParameters.Add("pattern", Pattern.Substring(4));
yield return rule;
}
}
and then decorate your model with it:
[IgnorecaseRegularExpression(#"([^.]+[.](jpg|jpeg|gif|png|wpf|doc|docx|xls|xlsx", ErrorMessage = "Invalid File Name"]
public string Name { get; set; }
Finally write an adapter on the client:
<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('icregex', [ 'pattern' ], function (options) {
options.rules['icregex'] = options.params;
options.messages['icregex'] = options.message;
});
jQuery.validator.addMethod('icregex', function (value, element, params) {
var match;
if (this.optional(element)) {
return true;
}
match = new RegExp(params.pattern, 'i').exec(value);
return (match && (match.index === 0) && (match[0].length === value.length));
}, '');
</script>
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.Name)
#Html.ValidationMessageFor(x => x.Name)
<input type="submit" value="OK" />
}
Of course you could externalize the client rules into a separate javascript file so that you don't have to repeat it everywhere.

Umh could you show the Client Validation?
umh I think that you can create your own Attribute that derives from RegularExpression and add the functionallity to ignore case.

Related

asp.net/MVC custom model validation attribute not working

(I've made some progress, but still not working, updates below...)
I am trying to implement ye olde start date is not greater than end date validation. This is the first time I've attempted to write a custom validation attribute. Based on what I've been reading out here, this is what I've come up with...
custom validation attribute:
public class DateGreaterThanAttribute : ValidationAttribute
{
private string _startDatePropertyName;
public DateGreaterThanAttribute(string startDatePropertyName)
{
_startDatePropertyName = startDatePropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var propertyInfo = validationContext.ObjectType.GetProperty(_startDatePropertyName);
if (propertyInfo == null)
{
return new ValidationResult(string.Format("Unknown property {0}", _startDatePropertyName));
}
var propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
if ((DateTime)value > (DateTime)propertyValue)
{
return ValidationResult.Success;
}
else
{
var startDateDisplayName = propertyInfo
.GetCustomAttributes(typeof(DisplayNameAttribute), true)
.Cast<DisplayNameAttribute>()
.Single()
.DisplayName;
return new ValidationResult(validationContext.DisplayName + " must be later than " + startDateDisplayName + ".");
}
}
}
view model:
public class AddTranscriptViewModel : IValidatableObject
{
...
[DisplayName("Class Start"), Required]
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MM/dd/yyyy}")]
[RegularExpression(#"^(1[012]|0?[1-9])[/]([12][0-9]|3[01]|0?[1-9])[/](19|20)\d\d.*", ErrorMessage = "Date out of range.")]
public DateTime? ClassStart { get; set; }
[DisplayName("Class End"), Required]
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MM/dd/yyyy}")]
[RegularExpression(#"^(1[012]|0?[1-9])[/]([12][0-9]|3[01]|0?[1-9])[/](19|20)\d\d.*", ErrorMessage = "Date out of range.")]
[DateGreaterThan("ClassStart")]
public DateTime? ClassEnd { get; set; }
...
}
Relevant portions of the front-end:
#using (Html.BeginForm("AddManualTranscript", "StudentManagement", FormMethod.Post, new { id = "studentManagementForm", #class = "container form-horizontal" }))
{
...
<div class="col-md-4" id="divUpdateStudent">#Html.Button("Save Transcript Information", "verify()", false, "button")</div>
...
<div class="col-md-2">
<div id="divClassStart">
<div>#Html.LabelFor(d => d.ClassStart, new { #class = "control-label" })</div>
<div>#Html.EditorFor(d => d.ClassStart, new { #class = "form-control" }) </div>
<div>#Html.ValidationMessageFor(d => d.ClassStart)</div>
</div>
</div>
<div class="col-md-2">
<div id="divClassEnd">
<div>#Html.LabelFor(d => d.ClassEnd, new { #class = "control-label" })</div>
<div>#Html.EditorFor(d => d.ClassEnd, new { #class = "form-control" }) </div>
<div>#Html.ValidationMessageFor(d => d.ClassEnd)</div>
</div>
</div>
...
}
<script type="text/javascript">
...
function verify() {
if ($("#StudentGrades").data("tGrid").total == 0) {
alert("Please enter at least one Functional Area for the transcript grades.");
}
else {
$('#studentManagementForm').trigger(jQuery.Event("submit"));
}
}
...
</script>
The behavior I'm seeing is that all other validations on all other fields on the form, which are all standard validations like Required, StringLength, and RegularExpression, etc., are working as expected: when I click the "save" button, the red text appears for those fields that don't pass. I have put a breakpoint in my IsValid code, and it doesn't hit unless all the other validations are passed. And even then, if the validation check fails, it doesn't stop the post.
Further reading led me to add the following to Global.asax.cs:
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(DateGreaterThanAttribute), typeof(DataAnnotationsModelValidator));
But that made no difference. I also tested ModelState.IsValid in the postback function, and it was false. But for the other validators if never gets that far. I even noticed in the markup that it seems like a lot of markup gets created on those fields that have validation attributes when the page is generated. Where does that magic occur and why is my custom validator out of the loop?
There's a lot of variation out there, but what I have here seems to generally line up with what I'm seeing. I've also read some about registering validators on the client side, but that seems to only apply to client-side validation, not model validation at submit/post. I won't be embarrassed if the answer is some silly oversight on my part. After about a day on this, I simply need it to work.
Update:
Rob's answer led me to the link referenced in my comment below, which then led me here client-side validation in custom validation attribute - asp.net mvc 4 which led me here https://thewayofcode.wordpress.com/tag/custom-unobtrusive-validation/
What I read there jived with what I had observed, that something was missing in the markup, and it looked like the author outlined how to get it in there. So I added the following to my validation attribute class:
public class DateGreaterThanAttribute : ValidationAttribute, IClientValidatable // IClientValidatable added here
...
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
//string errorMessage = this.FormatErrorMessage(metadata.DisplayName);
string errorMessage = ErrorMessageString;
// The value we set here are needed by the jQuery adapter
ModelClientValidationRule dateGreaterThanRule = new ModelClientValidationRule
{
ErrorMessage = errorMessage,
ValidationType = "dategreaterthan" // This is the name the jQuery adapter will use, "startdatepropertyname" is the name of the jQuery parameter for the adapter, must be LOWERCASE!
};
dateGreaterThanRule.ValidationParameters.Add("startdatepropertyname", _startDatePropertyName);
yield return dateGreaterThanRule;
}
And created this JavaScript file:
(function ($) {
$.validator.addMethod("dategreaterthan", function (value, element, params) {
console.log("method");
return Date.parse(value) > Date.parse($(params).val());
});
$.validator.unobtrusive.adapters.add("dategreaterthan", ["startdatepropertyname"], function (options) {
console.log("adaptor");
options.rules["dategreaterthan"] = "#" + options.params.startdatepropertyname;
options.messages["dategreaterthan"] = options.message;
});
})(jQuery);
(Notice the console.log hits... I never see those.)
After this, I'm now getting hits when I browse to the page in the DataGreaterThanAttribute constructor and the GetClientValidationRules. As well, the ClassEnd input tag now has the following markup in it:
data-val-dategreaterthan="The field {0} is invalid." data-val-dategreaterthan-startdatepropertyname="ClassStart"
So I'm getting closer. The problem is, the addMethod and adapater.add don't seem to be doing their jobs. When I inspect these objects in the console using the following:
$.validator.methods
$.validator.unobtrusive.adapters
...my added method and adapter are not there. If I run the code from my JavaScript file in the console, they do get added and are there. I also noticed that if I generally inspect the unobtrusive validation object with...
$("#studentManagementForm").data('unobtrusiveValidation')
...there is no evidence of my custom validation.
As I alluded earlier, there are many examples out here, and they all seem to do things just a little differently, so I'm still trying some different things. But I'm really hoping someone who has beaten this into submission before will come along and share that hammer with me.
If I can't get this to work, I'll be putting on the hard-hat and writing some hacky JavaScript to spoof the same functionality.
I think you need IEnumerable<ValidationResult> on your model.
I had to do something similar around 4 years ago and still have the snippet to hand if this helps:
public class ResultsModel : IValidatableObject
{
[Required(ErrorMessage = "Please select the from date")]
public DateTime? FromDate { get; set; }
[Required(ErrorMessage = "Please select the to date")]
public DateTime? ToDate { get; set; }
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
{
var result = new List<ValidationResult>();
if (ToDate < FromDate)
{
var vr = new ValidationResult("The to date cannot be before the from date");
result.Add(vr);
}
return result;
}
}

How to bind posted data named "file[]" to an MVC model?

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." });
}

Common validation for multiple inputs MVC 3 [duplicate]

I have a view model that has year/month/day properties for someone's date of birth. All of these fields are required. Right now, if someone doesn't enter anything for the date of birth they get 3 separate error messages.
What I want to do is somehow group those error messages together into 1 message that just says 'Date of birth is required'. So if 1 or more of those fields are blank, they will always just get the 1 validation message.
I NEED this to work on client-side validation via jquery validate and unobtrusive validate. I know this is possible with the jquery validate plugin by looking at this question. But I don't know how to achieve this with asp.net mvc using validation attributes on my model and unobtrusive validation. Hopefully there's some built in way to group properties for validation purposes, but if not can this be done with a custom validation attribute?
Here's what my existing model and view looks like:
The Model:
public class MyModel {
[Required(ErrorMessage = "Year is required")]
public int Year { get; set; }
[Required(ErrorMessage = "Month is required")]
public int Month { get; set; }
[Required(ErrorMessage = "Day is required")]
public int Day { get; set; }
}
The View:
<div>
<label>Date of birth: <span style="color:red;">*</span></label>
<div>#Html.DropDownListFor(m => m.Year, ApplicationModel.GetSelectListForDateRange(DateTime.Today.Year - 16, DateTime.Today.Year - 10), "", new{data_description="birthDate"})#Html.LabelFor(m => m.StudentBirthYear)</div>
<div>#Html.DropDownListFor(m => m.Month, ApplicationModel.GetSelectListForDateRange(1, 12, true), "", new{data_description="birthDate"})#Html.LabelFor(m => m.StudentBirthMonth)</div>
<div>#Html.DropDownListFor(m => m.Day, ApplicationModel.GetSelectListForDateRange(1, 31), "", new{data_description="birthDate"})#Html.LabelFor(m => m.StudentBirthDay)</div>
</div>
<div class="error-container">#Html.ValidationMessageFor(m => m.Year)</div>
<div class="error-container">#Html.ValidationMessageFor(m => m.Month)</div>
<div class="error-container">#Html.ValidationMessageFor(m => m.Day)</div>
I am somewhat late to the party (only couple of years) still...
Most appropriate solution is indeed creating a CustomAttribute but instead of giving you good advice an leaving to die I will show you how.
Custom Attribute:
public class GroupRequiredAttribute : ValidationAttribute, IClientValidatable
{
private readonly string[] _serverSideProperties;
public GroupRequiredAttribute(params string[] serverSideProperties)
{
_serverSideProperties = serverSideProperties;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (_serverSideProperties == null || _serverSideProperties.Length < 1)
{
return null;
}
foreach (var input in _serverSideProperties)
{
var propertyInfo = validationContext.ObjectType.GetProperty(input);
if (propertyInfo == null)
{
return new ValidationResult(string.Format("unknown property {0}", input));
}
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 = "grouprequired"
};
rule.ValidationParameters["grouprequiredinputs"] = string.Join(",", this._serverSideProperties);
yield return rule;
}
}
ViewModel: Decorate only one field on your viewModel like following:
[GroupRequired("Year", "Month", "Day", ErrorMessage = "Please enter your date of birth")]
public int? Year { get; set; }
public int? Month { get; set; }
public int? Day { get; set; }
Jquery: You will need to add adapters in my case it's jquery.validate.unobtrusive.customadapters.js or wherever you register your adapters (you might put this on the page just do it after unobtrusive validation runs).
(function ($) {
jQuery.validator.unobtrusive.adapters.add('grouprequired', ['grouprequiredinputs'], function (options) {
options.rules['grouprequired'] = options.params;
options.messages['grouprequired'] = options.message;
});
}(jQuery));
jQuery.validator.addMethod('grouprequired', function (value, element, params) {
var inputs = params.grouprequiredinputs.split(',');
var values = $.map(inputs, function (input, index) {
var val = $('#' + input).val();
return val != '' ? input : null;
});
return values.length == inputs.length;
});
and that should do it.
For those who are interested what this does: In C# land it grabs the ids of fields glues them with , and puts into custom attribute on Year field.
HTML should look something like this (If it doesn't debug C# attribute):
<input class="tooltip form-control input dob--input-long" data-val="true" data-val-grouprequired="Please enter your date of birth" data-val-grouprequired-grouprequiredinputs="Year,Month,Day" name="Year" placeholder="YYYY" tabindex="" type="text" value="">
Then Jquery validation splits them back into id's and checks if all of them are not empty and that's pretty much it.
You will want to mark fields as invalid somehow (now it would only mark the field attribute is sitting on) most appropriate solution IMHO is to wrap all fields in container with class field-error-wrapper and then add following to your page after Jquery validation is loaded:
$.validator.setDefaults({
highlight: function (element) {
$(element).closest(".field-error-wrapper").addClass("input-validation-error");
},
unhighlight: function (element) {
$(element).closest(".field-error-wrapper").removeClass("input-validation-error");
}
});
instead of marking field it will mark container and then you can write your css in a way that if container is marked with .input-validation-error then all fields inside turn red. I think my job here is done.
EDIT: Ok so there appears to be one more issue where fields get unmarked because validator thinks that day and month are valid and it needs to remove invalid class from parent, validator first marks invalid fields then unmarks valid which causes validation not to get highlighted, so I changed the sequence in which validation happens, I wouldn't recommend overriding this globally (cause I am not sure on what catastrophic side affects it might have) just paste it on the page where you have birthdate fields.
$(function () {
$.data($('form')[0], 'validator').settings.showErrors = function () {
if (this.settings.unhighlight) {
for (var i = 0, elements = this.validElements() ; elements[i]; i++) {
this.settings.unhighlight.call(this, elements[i], this.settings.errorClass, this.settings.validClass);
}
}
this.hideErrors();
for (var i = 0; this.errorList[i]; i++) {
var error = this.errorList[i];
this.settings.highlight && this.settings.highlight.call(this, error.element, this.settings.errorClass, this.settings.validClass);
this.showLabel(error.element, error.message);
}
if (this.errorList.length) {
this.toShow = this.toShow.add(this.containers);
}
if (this.settings.success) {
for (var i = 0; this.successList[i]; i++) {
this.showLabel(this.successList[i]);
}
}
this.toHide = this.toHide.not(this.toShow);
this.addWrapper(this.toShow).show();
};
});
Hope this saves you some time.
You could do that simply using CustomAttribute.
Just put this attribute on your model
[CustomValidation(typeof(MyModel), "ValidateRelatedObject")]
and then simply define the rules to validate the values in the following method:
public static ValidationResult ValidateRelatedObject(object value, ValidationContext context)
{
var context = new ValidationContext(value, validationContext.ServiceContainer, validationContext.Items);
var results = new List<ValidationResult>();
Validator.TryValidateObject(value, context, results);
// TODO: Wrap or parse multiple ValidationResult's into one ValidationResult
return result;
}
For more information, you could visit this link.
You should implement IValidatableObject and take of the Require. Then the validation on the server side will do the job, something like:
public class MyModel : IValidatableObject
{
public int Year { get; set; }
public int Month { get; set; }
public int Day { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (/*Validate properties here*/) yield return new ValidationResult("Invalid Date!", new[] { "valideDate" });
}
}
For client side validation you need to implement your own function, and prompt the error to the user somehow.
EDIT: Given that you still need client side validation, you should do something like this:
$("form").validate({
rules: {
Day: { required: true },
Month : { required: true },
Year : { required: true }
},
groups: {
Date: "Day Month Year"
},
errorPlacement: function(error, element) {
if (element.attr("id") == "Day" || element.attr("id") == "Month" || element.attr("id") == "Year")
error.insertAfter("#Day");
else
error.insertAfter(element);
}
});
Hi I hope this fulfill your requirement
//--------------------------HTML Code-----------------------------
<form id="myform">
<select name="day">
<option value="">select</option>
<option value="1">1</option>
<select>
<select name="mnth">
<option value="">select</option>
<option value="Jan">Jan</option>
<select>
<select name="yr">
<option value="">select</option>
<option value="2015">2015</option>
<select>
<br/>
<input type="submit" />
<br/>
<div id="msg"></div>
</form>
//-------------------------------JS Code to validate-------------
$(document).ready(function() {
$('#myform').validate({
rules: {
day: {
required: true
},
mnth: {
required: true
},
yr: {
required: true
}
},
errorPlacement: function (error, element) {
var name = $(element).attr("name");
error.appendTo($("#msg"));
},
messages: {
day: {
required: "Date of birth is required"
},
mnth: {
required: "Date of birth is required"
},
yr: {
required: "Date of birth is required"
}
},
groups: {
p: "day mnth yr"
},
submitHandler: function(form) { // for demo
alert('valid form');
return false;
}
});
});
Here is Running Example

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.

Binding conflict between a property named Title in my Model and View.Title in my View (in MVC)

My Model contains a property named Title, and in my Create view I set the page title using ViewBag.Title.
This creates the following problem: the form generated by Html.Editor will display the text from ViewBag.Title, instead of the model's Title value.
The only workaround I have found is first calling Html.Editor, and then setting the View.Title.
Does anyone have a better solution?
Edit 1: I am using MVC 3.
Edit 2: This is my DisplayTemplates/Object.cshtml:
#model dynamic
#using Iconum.VS10CS040.Library.Web.MVC3.Helpers
#if (ViewData.TemplateInfo.TemplateDepth > 1) {
<span class="editor-object simple">#ViewData.ModelMetadata.SimpleDisplayText</span>
} else {
foreach (var prop in ViewData.ModelMetadata.Properties.Where(
pm =>
pm.ShowForEdit
&& !ViewData.TemplateInfo.Visited(pm)
&& pm.ModelType != typeof(System.Data.EntityState)
&& !pm.IsComplexType
)
)
{
if (prop.HideSurroundingHtml) {
<text>#Html.Editor(prop.PropertyName)</text>
} else {
string css = "";
if (prop.Model != null && prop.Model.GetType() != null)
{
css += " " + prop.Model.GetType().ToString().ToLower().Replace('.', '-');
}
if (prop.DataTypeName != null)
{
css += " " + prop.DataTypeName.ToLower();
}
if (prop.IsRequired && prop.ModelType.FullName != "System.Boolean")
{
css += " required";
}
<div class="editor-container #css">
<div class="editor-label">
#if (!String.IsNullOrEmpty(Html.Label(prop.PropertyName).ToHtmlString()))
{
// Use LabelWithForThatMatchesTheIdOfTheInput instead of Label because of a bug (fixed in MVC 3)
#Html.LabelWithForThatMatchesTheIdOfTheInput(prop.PropertyName)
}
#if (prop.IsRequired && prop.ModelType.FullName != "System.Boolean")
{
#Html.Raw(" <span class=\"required\">*<span>");
}
</div>
<div class="editor-field">
#* This the line that causes my problem *#
#Html.Editor(prop.PropertyName)
#Html.ValidationMessage(prop.PropertyName)
</div>
</div>
}
} //foreach
// Loop though all items in the Model with an TemplateHint (UIHint)
foreach (var prop in ViewData.ModelMetadata.Properties.Where(
pm => pm.ShowForEdit
&& !ViewData.TemplateInfo.Visited(pm)
&& pm.ModelType != typeof(System.Data.EntityState)
&& !pm.IsComplexType
&& pm.TemplateHint != null
&& (
pm.TemplateHint == "jWYSIWYG0093"
||
pm.TemplateHint == "jQueryUIDatepicker"
||
pm.TemplateHint == "CKEditor"
)
)
)
{
// TODO: check for duplicate js file includes
#Html.Editor(prop.PropertyName, prop.TemplateHint + "-Script")
}
}
I would recommend using EditorFor instead of Editor.
Html.EditorFor(x => x.Title)
instead of:
Html.Editor("Title")
This way not only that the view takes advantage of your view model but it behaves as expected in this case.
Example with ASP.NET MVC 3.0 RTM (Razor):
Model:
public class MyViewModel
{
public string Title { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Title = "ViewBag title";
ViewData["Title"] = "ViewData title";
var model = new MyViewModel
{
Title = "Model title"
};
return View(model);
}
}
View:
#model AppName.Models.MyViewModel
#{
ViewBag.Title = "Home Page";
}
#Html.EditorFor(x => x.Title)
#{
ViewBag.Title = "Some other title";
}
So no matter how much we try to abuse here the editor template uses the correct model title (which is not the case if we used Html.Editor("Title")).
As suggested by the other answers, using EditorFor instead of Editor seems to work around the problem. However, using EditorFor requires knowledge of the model type and property type at compile-time, which isn't the case for Object.cshtml.
You can still do this by building up and calling the correct generically-constructed EditorFor method using reflection. The code to do this is really messy, so here are some re-usable extension methods to do it for you.
Use them like this in Object.cshtml where prop is an instance of ModelMetadata like in the question:
#Html.DisplayFor(prop)
#Html.LabelFor(prop)
#Html.EditorFor(prop)
#Html.ValidationMessageFor(prop)
Here are the extension methods:
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
namespace ASP
{
public static class NonStronglyTypedStronglyTypedHtmlHelpers
{
public static MvcHtmlString DisplayFor<TModel>(this HtmlHelper<TModel> html, ModelMetadata prop)
{
return StronglyTypedHelper(html, h => h.DisplayFor, prop);
}
public static MvcHtmlString EditorFor<TModel>(this HtmlHelper<TModel> html, ModelMetadata prop)
{
return StronglyTypedHelper(html, h => h.EditorFor, prop);
}
public static MvcHtmlString LabelFor<TModel>(this HtmlHelper<TModel> html, ModelMetadata prop)
{
return StronglyTypedHelper(html, h => h.LabelFor, prop);
}
public static MvcHtmlString ValidationMessageFor<TModel>(this HtmlHelper<TModel> html, ModelMetadata prop)
{
return StronglyTypedHelper(html, h => h.ValidationMessageFor, prop);
}
private static MvcHtmlString StronglyTypedHelper(HtmlHelper html, Func<HtmlHelper<object>, GenericHelper<object>> accessMethod, ModelMetadata prop)
{
var constructedMethod = MakeStronglyTypedHelper(html, accessMethod, prop);
var genericPropertyExpression = MakePropertyExpression(prop);
var typedHtmlHelper = MakeStronglyTypedHtmlHelper(html, prop.ContainerType);
return (MvcHtmlString)constructedMethod.Invoke(null, new object[] { typedHtmlHelper, genericPropertyExpression });
}
private static MethodInfo MakeStronglyTypedHelper(HtmlHelper html, Func<HtmlHelper<object>, GenericHelper<object>> accessMethod, ModelMetadata prop)
{
var objectTypeHelper = new HtmlHelper<object>(html.ViewContext, html.ViewDataContainer, html.RouteCollection);
var runMethod = accessMethod(objectTypeHelper);
var constructedMehtod = runMethod.Method;
var genericHelperDefinition = constructedMehtod.GetGenericMethodDefinition();
return genericHelperDefinition.MakeGenericMethod(prop.ContainerType, prop.ModelType);
}
private static object MakeStronglyTypedHtmlHelper(HtmlHelper html, Type type)
{
var genericTypeDefinition = typeof(HtmlHelper<>);
var constructedType = genericTypeDefinition.MakeGenericType(type);
var constructor = constructedType.GetConstructor(new[] { typeof(ViewContext), typeof(IViewDataContainer), typeof(RouteCollection) });
return constructor.Invoke(new object[] { html.ViewContext, html.ViewDataContainer, html.RouteCollection });
}
private static LambdaExpression MakePropertyExpression(ModelMetadata prop)
{
var propertyInfo = prop.ContainerType.GetProperty(prop.PropertyName);
var expressionParameter = Expression.Parameter(prop.ContainerType);
var propertyExpression = Expression.MakeMemberAccess(expressionParameter, propertyInfo);
return Expression.Lambda(propertyExpression, expressionParameter);
}
private delegate MvcHtmlString GenericHelper<TModel>(Expression<Func<TModel, object>> expression);
}
}
I found partial solution myself.
Just use:
#Html.EditorForModel()
instead of:
#foreach (var property in Model.GetMetadata().Properties)
{
<div class="editor-label">
#Html.Label(property.PropertyName)
</div>
<div class="editor-field">
#Html.Editor(property.PropertyName)
#Html.ValidationMessage(property.PropertyName)
</div>
}
Html.EditorForModel() method return same results, but without described problem.
I solve same problem. Use this syntax instead Html.Editor
#(Html.EditorFor(p => property.Model))

Resources