I have two fields in my model
CreateDateTo
CreateDateFrom
which renders like this
<b>Start Date</b> #Html.EditorFor(model => model.AdvanceSearch.CreateDatefrom, new { #class = "picker startDate" })
<b>End Date</b> #Html.EditorFor(model => model.AdvanceSearch.CreateDateto, new { #class = "picker endDate" })
I have a validation scenario that enddate should not be greater then start date, currently I am validating it by jquery
$.validator.addMethod("endDate", function (value, element) {
var startDate = $('.startDate').val();
return Date.parse(startDate) <= Date.parse(value);
}, "* End date must be Equal/After start date");
I want to know that is there any way in MVC3 model validation to do this?
I would say that you should not rely solely on Javascript unless you are in control of your client's browser in some sort of intranet application. If the app is public facing - make sure you have both client and server side validation.
Also, a cleaner way of implementing the server side validation inside your model object can be done with a custom validation attribute shown below. Your validation then becomes centralised and you do not have have to explicitly compare the dates in your controller.
public class MustBeGreaterThanAttribute : ValidationAttribute
{
private readonly string _otherProperty;
public MustBeGreaterThanAttribute(string otherProperty, string errorMessage) : base(errorMessage)
{
_otherProperty = otherProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var otherProperty = validationContext.ObjectInstance.GetType().GetProperty(_otherProperty);
var otherValue = otherProperty.GetValue(validationContext.ObjectInstance, null);
var thisDateValue = Convert.ToDateTime(value);
var otherDateValue = Convert.ToDateTime(otherValue);
if (thisDateValue > otherDateValue)
{
return ValidationResult.Success;
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
}
This can then be applied to your model like so:
public class MyViewModel
{
[MustBeGreaterThan("End", "Start date must be greater than End date")]
public DateTime Start { get; set; }
public DateTime End { get; set; }
// more properties...
}
You need to create a custom validation against the model. You could put this in the controller after if(Model.IsValid)
if(Model.End<Model.StartDate)
....
But I would stick to javascript.
It works on the clientside and does not hit the server.
Unless you just need the added assurance.
Related
I am performing custom validation on a model property. The property is a proxy for other parts of the model and therefore requires no explicit user input. Server-side validation is working correctly but no client-side rules are generated.
I have been able to successfully generate the client rules but only when 'referencing the property in the view' using TextBoxFor, CheckBoxFor (or perhaps more appropriately HiddenFor) on the target property. However this feels like a hack, since the property doesn't even have a setter, so the value is guaranteed to be discarded.
Is there any way to force ASP.NET MVC to generate the client validation rules for a specific property without it being used in the view?
Example Code
public class Model {
public bool Option1 { get; set; }
public bool Option2 { get; set; }
public bool Option3 { get; set; }
[CustomValidator(ErrorMessage = "Validation Failed!")]
public bool AtLeastOneSelected => Option1 != false || Option2 != false || Option3 != false;
}
public class CustomValidator : ValidationAttribute, IClientValidatable {
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
// breakpoint below
return new List<ModelClientValidationRule>();
}
}
Example View
#Html.CheckBoxFor(m => m.Option1)
#Html.CheckBoxFor(m => m.Option2)
#Html.CheckBoxFor(m => m.Option3)
#Html.ValidationMessageFor(m => m.AtLeastOneSelected)
#*//Client rules will not be generated without this line*#
#*//#Html.TextBoxFor(m => m.AtLeastOneSelected)*#
Your sole problem here is that client-side validation doesn't work out of the box. This is because the validation client-side must be tied to a form field. There has to be something that triggers an valid/invalid determination, and without a form field you have nothing to do that with.
You can always write some custom client-side validation. Just check of your options and see if at least one is set, then you add/remove a message accordingly. You can check the jQuery Validation docs (what's behind client-side validation in MVC) for require_from_group. Seems like pretty much what you want in this particular example. You'll just have to add the rule manually.
$( "#myform" ).validate({
rules: {
Option1: {
require_from_group: [1, ".options"]
},
Option2: {
require_from_group: [1, ".options"]
},
Option3: {
require_from_group: [1, ".options"]
}
}
});
Then, you just need to add that class to each of your option fields.
I had a similar issue to yours in that I had to validate three inputs in addition to their own validation due to some silly UI requirements.
I did manage to make it work with all the built-in stuff and without any need for a hack.
I needed a ValidationAttribute
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Web.Mvc;
namespace ValidationAttributes
{
public sealed class MyValidationRule : ValidationAttribute, IClientValidatable
{
public MyValidationRule()
: base("My validation rule's error message {0}")
{
// initialise vars here
}
public override bool IsValid(object value)
{
var valid = value == null;
// validate logic here
return valid;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationMyValidationRule(FormatErrorMessage("SomeValueFor0ParamAbove"), MinimumAgeInYears);
}
}
}
... and a ModelClientValidationRule
using System.Globalization;
using System.Web.Mvc;
namespace ModelClientValidationRules
{
public class ModelClientValidationMyValidationRule : ModelClientValidationRule
{
public ModelClientValidationMyValidationRule(string errorMessage)
{
ErrorMessage = errorMessage;
ValidationType = "myvalidatorrule";
// add any params needed on the client side from the server side by using the following
ValidationParameters.Add("param_name", "param_value");
}
}
}
... and I needed to add a prop in my Model
[MyValidationRule]
public whatever SomeProperty { get; set; }
... and I needed to add the client side validation in the view
#Html.ValidationMessageFor(x => x.SomeProperty, "Validation message")
... and finally I needed to include some js in my scripts
jQuery.validator.addMethod("myvalidationrule", function (value, element, params) {
var valid = false;
// your validation logic goes here
// NB element will be undefined as the validator is not driven by an element
return valid;
});
jQuery.validator.unobtrusive.adapters.add("myvalidationrule", function (options) {
options.rules["myvalidationrule"] = options.params;
options.message["myvalidationrule"] = options.message;
});
I really hope this makes sense... :S
I have a form in .NET MVC 5. where the user can write a number, default is "0", If the user deletes a number e.g. "233" an leaving the field empty. The form would not submit.
How can I submit the form with an empty field?
public class myModel
{
public int nummer { get; set; }
public myModel(){}
public myModel(int i) {this.nummer = i;}
}
razor code:
using (Html.BeginForm("myAction", "myController", FormMethod.Post, new { #class = "form-inline" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text- danger" })
#Html.EditorFor(model => model.nummer, new { htmlAttributes = new { #class = "form-control " } })
<input type="submit" value="submit" name="btnSubmit"/>
}
I am not interested in a validation error message, but to have the value set to "0", by default.
The DefaultModelBinder initializes your model using the parameterless constructor (your second constructor is never called). You will need to make the property nullable to prevent client and server side validation errors
public int? nummer { get; set; }
and then in the POST method, test for null and if so, set the value to 0
if(!model.nummer.HasValue)
{
model.nummer = 0;
}
Alternatively you could write your own ModelBinder that tests for a null value and in the ModelBinder, set the value to 0
You will need to set your nummer field to be a nullable type, like so:
public class myModel
{
public int? nummer { get; set; }
...
}
This will allow a user to submit the form without a value entered.
Then within your controller action you will need to assign a default value if the field is null:
if (model.nummer == null) model.nummer = 0;
Alternatively, you could use a private property like so:
public class myModel
{
private int? privateNummer { get; set; }
public int? nummer
{
get { return this.privateNummer == null ? 0 : this.privateNummer; }
set
{
this.privateNummer = value;
}
}
}
in your controller before passing the model back to the view do this ..
model.find(id);
model.nummer =0;
return View(model)
In my ASP.NET MVC 4 project I have validator for one of my view models, that contain rules definition for RuleSets. Edit ruleset used in Post action, when all client validation passed. Url and Email rule sets rules used in Edit ruleset (you can see it below) and in special ajax actions that validate only Email and only Url accordingly.
My problem is that view doesn't know that it should use Edit rule set for client html attributes generation, and use default rule set, which is empty. How can I tell view to use Edit rule set for input attributes generation?
Model:
public class ShopInfoViewModel
{
public long ShopId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string Description { get; set; }
public string Email { get; set; }
}
Validator:
public class ShopInfoViewModelValidator : AbstractValidator<ShopInfoViewModel>
{
public ShopInfoViewModelValidator()
{
var shopManagementService = ServiceLocator.Instance.GetService<IShopService>();
RuleSet("Edit", () =>
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Enter name.")
.Length(0, 255).WithMessage("Name length should not exceed 255 chars.");
RuleFor(x => x.Description)
.NotEmpty().WithMessage("Enter name.")
.Length(0, 10000).WithMessage("Name length should not exceed 10000 chars.");
ApplyUrlRule(shopManagementService);
ApplyEmailRule(shopManagementService);
});
RuleSet("Url", () => ApplyUrlRule(shopManagementService));
RuleSet("Email", () => ApplyEmailRule(shopManagementService));
}
private void ApplyUrlRule(IShopService shopService)
{
RuleFor(x => x.Url)
.NotEmpty().WithMessage("Enter url.")
.Length(4, 30).WithMessage("Length between 4 and 30 chars.")
.Matches(#"[a-z\-\d]").WithMessage("Incorrect format.")
.Must((model, url) => shopService.Available(url, model.ShopId)).WithMessage("Shop with this url already exists.");
}
private void ApplyEmailRule(IShopService shopService)
{
// similar to url rule: not empty, length, regex and must check for unique
}
}
Validation action example:
public ActionResult ValidateShopInfoUrl([CustomizeValidator(RuleSet = "Url")]
ShopInfoViewModel infoViewModel)
{
return Validation(ModelState);
}
Get and Post actions for ShopInfoViewModel:
[HttpGet]
public ActionResult ShopInfo()
{
var viewModel = OwnedShop.ToViewModel();
return PartialView("_ShopInfo", viewModel);
}
[HttpPost]
public ActionResult ShopInfo(CustomizeValidator(RuleSet = "Edit")]ShopInfoViewModel infoViewModel)
{
var success = false;
if (ModelState.IsValid)
{
// save logic goes here
}
}
View contains next code:
#{
Html.EnableClientValidation(true);
Html.EnableUnobtrusiveJavaScript(true);
}
<form class="master-form" action="#Url.RouteUrl(ManagementRoutes.ShopInfo)" method="POST" id="masterforminfo">
#Html.TextBoxFor(x => x.Name)
#Html.TextBoxFor(x => x.Url, new { validationUrl = Url.RouteUrl(ManagementRoutes.ValidateShopInfoUrl) })
#Html.TextAreaFor(x => x.Description)
#Html.TextBoxFor(x => x.Email, new { validationUrl = Url.RouteUrl(ManagementRoutes.ValidateShopInfoEmail) })
<input type="submit" name="asdfasfd" value="Сохранить" style="display: none">
</form>
Result html input (without any client validation attributes):
<input name="Name" type="text" value="Super Shop"/>
After digging in FluentValidation sources I found solution. To tell view that you want to use specific ruleset, decorate your action, that returns view, with RuleSetForClientSideMessagesAttribute:
[HttpGet]
[RuleSetForClientSideMessages("Edit")]
public ActionResult ShopInfo()
{
var viewModel = OwnedShop.ToViewModel();
return PartialView("_ShopInfo", viewModel);
}
If you need to specify more than one ruleset — use another constructor overload and separate rulesets with commas:
[RuleSetForClientSideMessages("Edit", "Email", "Url")]
public ActionResult ShopInfo()
{
var viewModel = OwnedShop.ToViewModel();
return PartialView("_ShopInfo", viewModel);
}
If you need to decide about which ruleset would be used directly in action — you can hack FluentValidation by putting array in HttpContext next way (RuleSetForClientSideMessagesAttribute currently is not designed to be overriden):
public ActionResult ShopInfo(validateOnlyEmail)
{
var emailRuleSet = new[]{"Email"};
var allRuleSet = new[]{"Edit", "Url", "Email"};
var actualRuleSet = validateOnlyEmail ? emailRuleSet : allRuleSet;
HttpContext.Items["_FV_ClientSideRuleSet"] = actualRuleSet;
return PartialView("_ShopInfo", viewModel);
}
Unfortunately, there are no info about this attribute in official documentation.
UPDATE
In newest version we have special extension method for dynamic ruleset setting, that you should use inside your action method or inside OnActionExecuting/OnActionExecuted/OnResultExecuting override methods of controller:
ControllerContext.SetRulesetForClientsideMessages("Edit", "Email");
Or inside custom ActionFilter/ResultFilter:
public class MyFilter: ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
((Controller)context.Controller).ControllerContext.SetRulesetForClientsideMessages("Edit", "Email");
//same syntax for OnActionExecuted/OnResultExecuting
}
}
Adding to this as the library has been updated to account for this situation...
As of 7.4.0, it's possible to dynamically select one or multiple rule sets based on your specific conditions;
ControllerContext.SetRulesetForClientsideMessages("ruleset1", "ruleset2" /*...etc*);
Documentation on this can be found in the latest FluentValidation site:
https://fluentvalidation.net/aspnet#asp-net-mvc-5
Adding the CustomizeValidator attribute to the action will apply the ruleset within the pipeline when the validator is being initialized and the model is being automatically validated.
public ActionResult Save([CustomizeValidator(RuleSet="MyRuleset")] Customer cust) {
// ...
}
The basic question to start: How can you put a custom, unobtrusive validator ontop of a list of objects within your model? Like, say my model allows multiple file uploads, and thus I have a list of files, and I want my validator to run on each of those files?
Now for a specific example. I've got a custom, unobtrusive validator that checks to see if a file extension is not within a list of prohibited extensions:
public class FileExtensionValidatorAttribute : ValidationAttribute, IClientValidatable {
protected static string[] PROHIBITED_EXTENSIONS = {
// ... List of extensions I don't allow.
};
public override bool IsValid(object value) {
if (value is IEnumerable<HttpPostedFileBase>) {
foreach (var file in (IEnumerable<HttpPostedFileBase>)value) {
var fileName = file.FileName;
if (PROHIBITED_EXTENSIONS.Any(x => fileName.EndsWith(x))) return false;
}
} else {
var file = (HttpPostedFileBase)value;
var fileName = file.FileName;
if (PROHIBITED_EXTENSIONS.Any(x => fileName.EndsWith(x))) return false;
}
return true;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
var modelClientVlidationRule = new ModelClientValidationRule {
ErrorMessage = this.ErrorMessageString,
ValidationType = "fileextension",
};
modelClientVlidationRule.ValidationParameters.Add("prohibitedextensions", string.Join("|", PROHIBITED_EXTENSIONS));
yield return modelClientVlidationRule;
}
}
Take note in my IsValid that I built this to accept a single file or a list of files.
In my model class, I can make use of this on a single HttpPostedFileBase:
[FileExtensionValidator(ErrorMessage = "Invalid Extension")]
public HttpPostedFileBase Upload { get; set; }
Then I attach to jquery's validator in my view:
jQuery.validator.addMethod("fileExtension", function (value, element, param) {
var extension = "";
var dotIndex = value.lastIndexOf('.');
if (dotIndex != -1) extension = value.substring(dotIndex + 1).toLowerCase();
return $.inArray(extension, param.prohibitedExtensions) === -1;
});
jQuery.validator.unobtrusive.adapters.add('fileextension', ['prohibitedextensions'], function (options) {
options.rules['fileExtension'] = {
prohibitedExtensions: options.params.prohibitedextensions.split('|')
};
options.messages['fileExtension'] = options.message;
});
This all works great, client side and server side ...but only on a single HttpPostedFileBase. The problem is that I need to provide users the ability to upload one or more files. If I change my model to this:
[FileExtensionValidator(ErrorMessage = "Invalid Extension")]
public List<HttpPostedFileBase> Uploads { get; set; }
...the Client-side validation no longer runs; only the server-side works. This is evident when doing a view-source. The <input> tag that gets generated is missing all the data-val attributes it needs to run. In doing a debug, GetClientValidationRules is never called.
What am I missing?
Could this be because of how I render it? I'm simply using an EditorTemplate for HttpPostedFileBase:
#model System.Web.HttpPostedFileBase
#Html.TextBoxFor(m => m, new { type = "file", size = 60 })
...and my view renders it like this:
<p>#Html.EditorFor(m => m.Uploads)</p>
Any advice is appreciated.
Here's what I came up with.
I actually think the problem is ultimately caused because MVC doesn't know that I want that Data Annotation on the List to be applied to all of its members. Nor should it I suppose.
So I simply made a "viewmodel" wrapper around HttpPostedFileBase, and put my validator there:
public class UploadedFile {
[FileExtensionValidator(ErrorMessage = "Invalid Extension")]
public HttpPostedFileBase File { get; set; }
}
Then, in my actual model, I now just use a list of those instead:
public List<UploadedFile> Uploads { get; set; }
...with no more dataannotations here of course since they're now in UploadedFile.
Then, with minor modifications to the view and editortemplate to use these, this now works a-ok, client side and server side. (Still, feels clunky to me. If anyone has a simpler way I'm still happy to hear it.)
Given the following viewmodel:
public class SomeViewModel
{
public bool IsA { get; set; }
public bool IsB { get; set; }
public bool IsC { get; set; }
//... other properties
}
I wish to create a custom attribute that validates at least one of the available properties are true. I envision being able to attach an attribute to a property and assign a group name like so:
public class SomeViewModel
{
[RequireAtLeastOneOfGroup("Group1")]
public bool IsA { get; set; }
[RequireAtLeastOneOfGroup("Group1")]
public bool IsB { get; set; }
[RequireAtLeastOneOfGroup("Group1")]
public bool IsC { get; set; }
//... other properties
[RequireAtLeastOneOfGroup("Group2")]
public bool IsY { get; set; }
[RequireAtLeastOneOfGroup("Group2")]
public bool IsZ { get; set; }
}
I would like to validate on the client-side prior to form submission as values in the form change which is why I prefer to avoid a class-level attribute if possible.
This would require both the server-side and client-side validation to locate all properties having identical group name values passed in as the parameter for the custom attribute. Is this possible? Any guidance is much appreciated.
Here's one way to proceed (there are other ways, I am just illustrating one that would match your view model as is):
[AttributeUsage(AttributeTargets.Property)]
public class RequireAtLeastOneOfGroupAttribute: ValidationAttribute, IClientValidatable
{
public RequireAtLeastOneOfGroupAttribute(string groupName)
{
ErrorMessage = string.Format("You must select at least one value from group \"{0}\"", groupName);
GroupName = groupName;
}
public string GroupName { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
foreach (var property in GetGroupProperties(validationContext.ObjectType))
{
var propertyValue = (bool)property.GetValue(validationContext.ObjectInstance, null);
if (propertyValue)
{
// at least one property is true in this group => the model is valid
return null;
}
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
private IEnumerable<PropertyInfo> GetGroupProperties(Type type)
{
return
from property in type.GetProperties()
where property.PropertyType == typeof(bool)
let attributes = property.GetCustomAttributes(typeof(RequireAtLeastOneOfGroupAttribute), false).OfType<RequireAtLeastOneOfGroupAttribute>()
where attributes.Count() > 0
from attribute in attributes
where attribute.GroupName == GroupName
select property;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var groupProperties = GetGroupProperties(metadata.ContainerType).Select(p => p.Name);
var rule = new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage
};
rule.ValidationType = string.Format("group", GroupName.ToLower());
rule.ValidationParameters["propertynames"] = string.Join(",", groupProperties);
yield return rule;
}
}
Now, let's define a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new SomeViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(SomeViewModel model)
{
return View(model);
}
}
and a view:
#model SomeViewModel
<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>
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.IsA)
#Html.ValidationMessageFor(x => x.IsA)
<br/>
#Html.EditorFor(x => x.IsB)<br/>
#Html.EditorFor(x => x.IsC)<br/>
#Html.EditorFor(x => x.IsY)
#Html.ValidationMessageFor(x => x.IsY)
<br/>
#Html.EditorFor(x => x.IsZ)<br/>
<input type="submit" value="OK" />
}
The last part that's left would be to register adapters for the client side validation:
jQuery.validator.unobtrusive.adapters.add(
'group',
[ 'propertynames' ],
function (options) {
options.rules['group'] = options.params;
options.messages['group'] = options.message;
}
);
jQuery.validator.addMethod('group', function (value, element, params) {
var properties = params.propertynames.split(',');
var isValid = false;
for (var i = 0; i < properties.length; i++) {
var property = properties[i];
if ($('#' + property).is(':checked')) {
isValid = true;
break;
}
}
return isValid;
}, '');
Based on your specific requirements the code might be adapted.
Use of require_from_group from jquery-validation team:
jQuery-validation project has a sub-folder in src folder called additional.
You can check it here.
In that folder we have a lot of additional validation methods that are not common that is why they're not added by default.
As you see in that folder it exists so many methods that you need to choose by picking which validation method you actually need.
Based on your question, the validation method you need is named require_from_group from additional folder.
Just download this associated file which is located here and put it into your Scripts application folder.
The documentation of this method explains this:
Lets you say "at least X inputs that match selector Y must be filled."
The end result is that neither of these inputs:
...will validate unless at least one of them is filled.
partnumber: {require_from_group: [1,".productinfo"]},
description: {require_from_group: [1,".productinfo"]}
options[0]: number of fields that must be filled in the group
options2: CSS selector that defines the group of conditionally required fields
Why you need to choose this implementation :
This validation method is generic and works for every input (text, checkbox, radio etc), textarea and select. This method also let you specify the minimum number of required inputs that need to be filled e.g
partnumber: {require_from_group: [2,".productinfo"]},
category: {require_from_group: [2,".productinfo"]},
description: {require_from_group: [2,".productinfo"]}
I created two classes RequireFromGroupAttribute and RequireFromGroupFieldAttribute that will help you on both server-side and client-side validations
RequireFromGroupAttribute class definition
RequireFromGroupAttribute only derives from Attribute. The class is use just for configuration e.g. setting the number of fields that need to be filled for the validation. You need to provide to this class the CSS selector class that will be used by the validation method to get all elements on the same group. Because the default number of required fields is 1 then this attribute is only used to decorate your model if the minimum requirement in the spcefied group is greater than the default number.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RequireFromGroupAttribute : Attribute
{
public const short DefaultNumber = 1;
public string Selector { get; set; }
public short Number { get; set; }
public RequireFromGroupAttribute(string selector)
{
this.Selector = selector;
this.Number = DefaultNumber;
}
public static short GetNumberOfRequiredFields(Type type, string selector)
{
var requiredFromGroupAttribute = type.GetCustomAttributes<RequireFromGroupAttribute>().SingleOrDefault(a => a.Selector == selector);
return requiredFromGroupAttribute?.Number ?? DefaultNumber;
}
}
RequireFromGroupFieldAttribute class definition
RequireFromGroupFieldAttribute which derives from ValidationAttribute and implements IClientValidatable. You need to use this class on each property in your model that participates to your group validation. You must pass the css selector class.
[AttributeUsage(AttributeTargets.Property)]
public class RequireFromGroupFieldAttribute : ValidationAttribute, IClientValidatable
{
public string Selector { get; }
public bool IncludeOthersFieldName { get; set; }
public RequireFromGroupFieldAttribute(string selector)
: base("Please fill at least {0} of these fields")
{
this.Selector = selector;
this.IncludeOthersFieldName = true;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var properties = this.GetInvolvedProperties(validationContext.ObjectType); ;
var numberOfRequiredFields = RequireFromGroupAttribute.GetNumberOfRequiredFields(validationContext.ObjectType, this.Selector);
var values = new List<object> { value };
var otherPropertiesValues = properties.Where(p => p.Key.Name != validationContext.MemberName)
.Select(p => p.Key.GetValue(validationContext.ObjectInstance));
values.AddRange(otherPropertiesValues);
if (values.Count(s => !string.IsNullOrWhiteSpace(Convert.ToString(s))) >= numberOfRequiredFields)
{
return ValidationResult.Success;
}
return new ValidationResult(this.GetErrorMessage(numberOfRequiredFields, properties.Values), new List<string> { validationContext.MemberName });
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var properties = this.GetInvolvedProperties(metadata.ContainerType);
var numberOfRequiredFields = RequireFromGroupAttribute.GetNumberOfRequiredFields(metadata.ContainerType, this.Selector);
var rule = new ModelClientValidationRule
{
ValidationType = "requirefromgroup",
ErrorMessage = this.GetErrorMessage(numberOfRequiredFields, properties.Values)
};
rule.ValidationParameters.Add("number", numberOfRequiredFields);
rule.ValidationParameters.Add("selector", this.Selector);
yield return rule;
}
private Dictionary<PropertyInfo, string> GetInvolvedProperties(Type type)
{
return type.GetProperties()
.Where(p => p.IsDefined(typeof(RequireFromGroupFieldAttribute)) &&
p.GetCustomAttribute<RequireFromGroupFieldAttribute>().Selector == this.Selector)
.ToDictionary(p => p, p => p.IsDefined(typeof(DisplayAttribute)) ? p.GetCustomAttribute<DisplayAttribute>().Name : p.Name);
}
private string GetErrorMessage(int numberOfRequiredFields, IEnumerable<string> properties)
{
var errorMessage = string.Format(this.ErrorMessageString, numberOfRequiredFields);
if (this.IncludeOthersFieldName)
{
errorMessage += ": " + string.Join(", ", properties);
}
return errorMessage;
}
}
How to use it in your view model?
In your model here is how to use it :
public class SomeViewModel
{
internal const string GroupOne = "Group1";
internal const string GroupTwo = "Group2";
[RequireFromGroupField(GroupOne)]
public bool IsA { get; set; }
[RequireFromGroupField(GroupOne)]
public bool IsB { get; set; }
[RequireFromGroupField(GroupOne)]
public bool IsC { get; set; }
//... other properties
[RequireFromGroupField(GroupTwo)]
public bool IsY { get; set; }
[RequireFromGroupField(GroupTwo)]
public bool IsZ { get; set; }
}
By default you don't need to decorate your model with RequireFromGroupAttribute because the default number of required fields is 1. But if you want a number of required fields to be different to 1 you can do the following :
[RequireFromGroup(GroupOne, Number = 2)]
public class SomeViewModel
{
//...
}
How to use it in your view code?
#model SomeViewModel
<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 src="#Url.Content("~/Scripts/require_from_group.js")" type="text/javascript"></script>
#using (Html.BeginForm())
{
#Html.CheckBoxFor(x => x.IsA, new { #class="Group1"})<span>A</span>
#Html.ValidationMessageFor(x => x.IsA)
<br />
#Html.CheckBoxFor(x => x.IsB, new { #class = "Group1" }) <span>B</span><br />
#Html.CheckBoxFor(x => x.IsC, new { #class = "Group1" }) <span>C</span><br />
#Html.CheckBoxFor(x => x.IsY, new { #class = "Group2" }) <span>Y</span>
#Html.ValidationMessageFor(x => x.IsY)
<br />
#Html.CheckBoxFor(x => x.IsZ, new { #class = "Group2" })<span>Z</span><br />
<input type="submit" value="OK" />
}
Notice the group selector you specified when using RequireFromGroupField attribute is use in your view by specifing it as a class in each input involved in your groups.
That is all for the server side validation.
Let's talk about the client side validation.
If you check the GetClientValidationRules implementation in RequireFromGroupFieldAttribute class you will see I'm using the string requirefromgroup and not require_from_group as the name of method for the ValidationType property. That is because ASP.Net MVC only allows the name of the validation type to contain alphanumeric char and must not start with a number. So you need to add the following javascript :
$.validator.unobtrusive.adapters.add("requirefromgroup", ["number", "selector"], function (options) {
options.rules["require_from_group"] = [options.params.number, options.params.selector];
options.messages["require_from_group"] = options.message;
});
The javascript part is really simple because in the implementation of the adaptater function we just delegate the validation to the correct require_from_group method.
Because it works with every type of input, textarea and select elements, I may think this way is more generic.
Hope that helps!
I implemented Darin's awesome answer into my application, except I added it for strings and not boolean values. This was for stuff like name/company, or phone/email. I loved it except for one minor nitpick.
I tried to submit my form without a work phone, mobile phone, home phone, or email. I got four separate validation errors client side. This is fine by me because it lets the users know exactly what field(s) can be filled in to make the error go away.
I typed in an email address. Now the single validation under email went away, but the three remained under the phone numbers. These are also no longer errors anymore.
So, I reassigned the jQuery method that checks validation to account for this. Code below. Hope it helps someone.
jQuery.validator.prototype.check = function (element) {
var elements = [];
elements.push(element);
var names;
while (elements.length > 0) {
element = elements.pop();
element = this.validationTargetFor(this.clean(element));
var rules = $(element).rules();
if ((rules.group) && (rules.group.propertynames) && (!names)) {
names = rules.group.propertynames.split(",");
names.splice($.inArray(element.name, names), 1);
var name;
while (name = names.pop()) {
elements.push($("#" + name));
}
}
var dependencyMismatch = false;
var val = this.elementValue(element);
var result;
for (var method in rules) {
var rule = { method: method, parameters: rules[method] };
try {
result = $.validator.methods[method].call(this, val, element, rule.parameters);
// if a method indicates that the field is optional and therefore valid,
// don't mark it as valid when there are no other rules
if (result === "dependency-mismatch") {
dependencyMismatch = true;
continue;
}
dependencyMismatch = false;
if (result === "pending") {
this.toHide = this.toHide.not(this.errorsFor(element));
return;
}
if (!result) {
this.formatAndAdd(element, rule);
return false;
}
} catch (e) {
if (this.settings.debug && window.console) {
console.log("Exception occurred when checking element " + element.id + ", check the '" + rule.method + "' method.", e);
}
throw e;
}
}
if (dependencyMismatch) {
return;
}
if (this.objectLength(rules)) {
this.successList.push(element);
}
}
return true;
};
I know this is an old thread but I just came across the same scenario and found a few solutions and saw one that solves Matt's question above so I thought I would share for those who come across this answer. Check out: MVC3 unobtrusive validation group of inputs