I have a C# app with a custom RequiredIf DataAnnotation implementation as follows:
public class RequiredIf : RequiredAttribute {
private String PropertyName { get; set; }
private Object DesiredValue { get; set; }
public RequiredIf (String property_name, Object desired_value) {
PropertyName = property_name;
DesiredValue = desired_value;
}
protected override ValidationResult IsValid (object value, ValidationContext context) {
Object instance = context.ObjectInstance;
Type type = instance.GetType();
Object prop = type.GetProperty(PropertyName);
Object property_value = type.GetProperty(PropertyName).GetValue(instance, null);
if ( property_value.ToString() == DesiredValue.ToString() ) {
ValidationResult result = base.IsValid(value, context);
return result;
}
return ValidationResult.Success;
}
}
This appears to work in nearly every case.
I have a ViewModel that I am trying to validate. The ViewModel has a child class. The basic implementation is:
public class ViewModel {
.........
public class ChildObject {
[RequiredIf("FieldToCheck", false)] // note that it's only required if the value is FALSE!
DateTime? ConditionalField { get; set; } // could be NULL
Boolean FieldToCheck { get; set; }
}
..........
}
When I submit the form with FieldToCheck set to true, my ViewModel validation still fails on this RequiredIf. I can view the value in the ViewModel variable in the controller and verify that it is true. However, I can set a breakpoint in the DataAnnotation's IsValid method and see in the debugger that the FieldToCheck is false - NOT what I submitted!
Is the Model Binder binding wrong? If so, why? And why is the bound value incorrect in the validator, but correct in the controller?
This is causing my ViewModel validation to fail whenever that DateTime field is left blank (NULL gets submitted). But if that Boolean is false, I don't even show the DateTime field in my form -- I don't want the user to submit anything in that field.
EDIT: This seems to happen only with Booleans, but it happens consistently regardless of where the Boolean/ConditionalField are created.
Does the Model Binder not bind Boolean members prior to validation?
EDIT2:
In the view, I have a standard form that I use to post the data:
#Model ViewModel
#using ( Html.BeginForm("Create", "Controller", FormMethod.Post ) {
.........
#Html.DropDownListFor(m => m.FieldToCheck, Model.FieldToCheckList)
.........
<button type="submit">Submit</button>
}
The Controller:
public class Controller : Controller {
[HttpPost]
public ActionResult Create (ViewModel view_model) {
// I set a breakpoint here, and if I submit FieldToCheck = true, the debugger shows that FieldToCheck is, indeed, true.
if ( ModelState.IsValid ) {
// save to db
return View();
}
return View();
}
}
I have a method that initializes fields in the ViewModel before rendering the form, and in it, I set FieldToCheckList as follows:
view_model.FieldToCheckList = new List<SelectListItem> {
new SelectListItem { Text = "Yes", Value = Boolean.TrueString },
new SelectListItem { Text = "No", Value = Boolean.FalseString }
}
So I am using a select to populate the value. However, I have tried multiple other form elements (including statically setting a HiddenFor to true) and it still results in the same problem. If I set a breakpoint in the IsValid method in the RequiredIf validator, all of my boolean values are false, regardless of submitted data. If I set a breakpoint in the Create method in Controller, my boolean values are set correctly.
I have also tested both Boolean and bool data types. It doesn't seem to affect anything.
EDIT4:
I didn't actually figure out the cause of the problem or a real solution, but I did find a workaround.
So, apparently the issue occurs when I do RequiredIf(AnyBooleanValue, AnythingButTrue). If the DesiredValue is set to anything except true, the Model Binder sets all boolean values in the ViewModel to the default value - false - regardless of what is submitted. I have tried multiple things - RequiredIf(FieldToCheck, "False"), RequiredIf(FieldToCheck, !true), etc. None work. But if I do RequiredIf(FieldToCheck, true), the values get bound correctly!
So the workaround is to add a new field:
public class ViewModel {
public Boolean FieldToCheck { get; set; }
public Boolean IsConditionalFieldRequired { get; set; }
[RequiredIf("IsConditionalFieldRequired",true)]
public string ConditionalField { get; set; }
}
In the view, I have IsConditionalFieldRequired as a hidden field, and whenever FieldToCheck is changed, I use jQuery to set IsConditionalFieldRequired to the inverse of FieldToCheck.
(I added this as an edit instead of an answer because I don't think it's a true solution, just a viable workaround. Obviously it's not the most eloquent way to get things done.)
This is an old question, but I just ran into the same problem today, where I was using a RequiredIf attribute and the ValidationContext.ObjectInstance properties did not have the correct bound values.
In my case, it turned out to be due to my view model class having a constructor which set some default property values. Inside the custom attribute code, the property values were those set in the constructor, but in the controller, the property values were from the posted form.
I worked around it by removing the constructor and setting default values in the view instead.
Related
my model implements the INotifyDataErrorInfo interface to validate it's properties, and it works fine, but the probleme is, the property HasErrors is by default false, so when i run my app at the first time and click save (form is empty) the view raise no errors, and the data is saved.
here is a snipet of my viewmodel
public LoggingViewModel()
{
_loggingCommand = new RelayCommand(checkCredentials, canExecuteLogginForm);
_logingModel = new LoggingModel();
// I raise this event in the 'OnErrorsChanged' method in the model,
// so my ViewModel can subscribe and check the 'HasErrors' property.
_logingModel.FormIsValid += (o, e) => _loggingCommand.RaiseCanExecuteChanged();
}
private bool canExecuteLogginForm()
{
return !_logingModel.HasErrors;
}
how do you handle this situation in your app?
for more info i created this github repos.
As the LogginModel is actually in an invalid state originally you should call the ValidateForm() method in its constructor to actually set it into this state and populate the _errors dictionary in order for the HasErrors property to return true as it should:
public class LoggingModel : PocoBase
{
public LoggingModel()
{
ValidateForm();
}
[Display(Name = "Name")]
[MaxLength(32), MinLength(4)]
public string UserName
{
get { return GetValue<string>(); }
set { SetValue(value); }
}
[Required]
public string Password
{
get { return GetValue<string>(); }
set { SetValue(value); }
}
}
ViewModel logic is correct.
Problem is in your validation logic inside the model which is returning HasErrors = False when HasErrors = true.
Take a look at how you are setting/returning/evaluating HasErrors.
Are you validating the Model on property get?
public bool HasErrors
{
get
{
bool hasErrors = false; // Default true here?
// Validation logic ...
return hasErrors;
}
}
Are you storing the HasError value in a property and setting it somewhere else?
public LoggingModel()
{
HasErrors = true; // Default true here?
}
public bool HasErrors { get; set; } // Gets set via validation logic
Just some ideas, like I said if you can show the structure on how you handle INotifyDataErrorInfo validation I can give a better answer.
There are some properties in my view model that are optional when saving, but required when submitting. In a word, we allow partial saving, but the whole form is submitted, we do want to make sure all required fields have values.
The only approaches I can think of at this moment are:
Manipulate the ModelState errors collection.
The view model has all [Required] attributes in place. If the request is partial save, the ModelState.IsValid becomes false when entering the controller action. Then I run through all ModelState (which is an ICollection<KeyValuePair<string, ModelState>>) errors and remove all errors raised by [Required] properties.
But if the request is to submit the whole form, I will not interfere with the ModelState and the [Required] attributes take effect.
Use different view models for partial save and submit
This one is even more ugly. One view model will contain all the [Required] attributes, used by an action method for submitting. But for partial save, I post the form data to a different action which use a same view model without all the [Required] attributes.
Obviously, I would end up with a lot of duplicate code / view models.
The ideal solution
I have been thinking if I can create a custom data annotation attribute [SubmitRequired] for those required properties. And somehow make the validation ignores it when partial saving but not when submitting.
Still couldn't have a clear clue. Anyone can help? Thanks.
This is one approach I use in projects.
Create a ValidationService<T> containing the business logic that will check that your model is in a valid state to be submitted with a IsValidForSubmission method.
Add an IsSubmitting property to the view model which you check before calling the IsValidForSubmission method.
Only use the built in validation attributes for checking for invalid data i.e. field lengths etc.
Create some custom attributes within a different namespace that would validate in certain scenarios i.e. [RequiredIfSubmitting] and then use reflection within your service to iterate over the attributes on each property and call their IsValid method manually (skipping any that are not within your namespace).
This will populate and return a Dictionary<string, string> which can be used to populate ModelState back to the UI:
var validationErrors = _validationService.IsValidForSubmission(model);
if (validationErrors.Count > 0)
{
foreach (var error in validationErrors)
{
ModelState.AddModelError(error.Key, error.Value);
}
}
I think there is more precise solution for your problem. Lets say you're submitting to one method, I mean to say you are calling same method for Partial and Full submit. Then you should do like below:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult YourMethod(ModelName model)
{
if(partialSave) // Check here whether it's a partial or full submit
{
ModelState.Remove("PropertyName");
ModelState.Remove("PropertyName2");
ModelState.Remove("PropertyName3");
}
if (ModelState.IsValid)
{
}
}
This should solve your problem. Let me know if you face any trouble.
Edit:
As #SBirthare commented that its not feasible to add or remove properties when model get updated, I found below solution which should work for [Required] attribute.
ModelState.Where(x => x.Value.Errors.Count > 0).Select(d => d.Key).ToList().ForEach(g => ModelState.Remove(g));
Above code will get all keys which would have error and remove them from model state. You need to place this line inside if condition to make sure it runs in partial form submit. I have also checked that error will come for [Required] attribute only (Somehow model binder giving high priority to this attribute even you place it after/below any other attribute). So you don't need to worry about model updates anymore.
My approach is to add conditional checking annotation attribute, which is learned from foolproof.
Make SaveMode part of the view model.
Mark the properties nullable so that the values of which are optional when SaveMode is not Finalize.
But add a custom annotation attribute [FinalizeRequired]:
[FinalizeRequired]
public int? SomeProperty { get; set; }
[FinalizeRequiredCollection]
public List<Item> Items { get; set; }
Here is the code for the Attribute:
[AttributeUsage(AttributeTargets.Property)]
public abstract class FinalizeValidationAttribute : ValidationAttribute
{
public const string DependentProperty = "SaveMode";
protected abstract bool IsNotNull(object value);
protected static SaveModeEnum GetSaveMode(ValidationContext validationContext)
{
var saveModeProperty = validationContext.ObjectType.GetProperty(DependentProperty);
if (saveModeProperty == null) return SaveModeEnum.Save;
return (SaveModeEnum) saveModeProperty.GetValue(validationContext.ObjectInstance);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var saveMode = GetSaveMode(validationContext);
if (saveMode != SaveModeEnum.SaveFinalize) return ValidationResult.Success;
return (IsNotNull(value))
? ValidationResult.Success
: new ValidationResult(string.Format("{0} is required when finalizing", validationContext.DisplayName));
}
}
For primitive data types, check value!=null:
[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredAttribute : FinalizeValidationAttribute
{
protected override bool IsNotNull(object value)
{
return value != null;
}
}
For IEnumerable collections,
[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredCollectionAttribute : FinalizeValidationAttribute
{
protected override bool IsNotNull(object value)
{
var enumerable = value as IEnumerable;
return (enumerable != null && enumerable.GetEnumerator().MoveNext());
}
}
This approach best achieves the separation of concerns by removing validation logic out of controller. Data Annotation attributes should handle that kind of work, which controller just need a check of !ModelState.IsValid. This is especially useful in my application, because I would not be able to refactor into a base controller if ModelState check is different in each controller.
In my view I have a check box and a text box if the check box is checked then I require the text box to be filled with some text. To do this I call
ModelState.AddModelError("item", "Please enter some text.");
only if the checkbox returns true and the text box isempty
when my page re-displays I receive the proper message where I have
#Html.ValidationMessageFor(model => model.item)
but I would like the text to go away after a use types something in the text box, without the user having to hit submit like it does with data annotation. How can I fix this?
I'm using c# Asp.net 4 with entity framework 5
ModelState.AddModelError is server-side validation, so the error message will not go away until you post to the server.
If you want the functionality you describe, you can define a custom validation attribute and apply it both client side and server-side. For example, you can define a "RequiredIf" custom validation attribute, which would make a field required if a certain other condition is met (in this case, if another property is true):
public class RequiredIfAttribute : RequiredAttribute
{
private String PropertyName { get; set; }
private Object DesiredValue { get; set; }
public RequiredIfAttribute(String propertyName, Object desiredvalue)
{
PropertyName = propertyName;
DesiredValue = desiredvalue;
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
Object instance = context.ObjectInstance;
Type type = instance.GetType();
Object proprtyvalue = type.GetProperty(PropertyName).GetValue(instance, null);
if (proprtyvalue.ToString() == DesiredValue.ToString())
{
ValidationResult result = base.IsValid(value, context);
return result;
}
return ValidationResult.Success;
}
}
Register it in your global.asax:
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute),typeof(RequiredAttributeAdapter);
Then you can use it like this:
public class YourModel {
// This is the property tied to your checkbox
public bool YourBooleanProperty { get; set; }
[RequiredIf("YourBooleanProperty", true)]
public string Item { get; set; }
}
You could also leverage the JQuery Validate plugin to perform the same conditional validation client-side.
Try with jquery, attach an eventListener to the field and remove the class CSS that MVC added to the field and hide the validation label
When I am changing the "model => model.id" to "model => model.Supplierid" i am getting below error
"The parameter 'expression' must evaluate to an IEnumerable when
multiple selection is allowed."
please have look on below code
// this my model class
public class clslistbox{
public int id { get; set; }
public int Supplierid { get; set; }
public List<SuppDocuments> lstDocImgs { get; set; }
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
public List<SuppDocuments> listDocImages()
{
List<SuppDocuments> _lst = new List<SuppDocuments>();
SuppDocuments _supp = new SuppDocuments();
_supp.Title = "title";
_supp.documentid = 1;
_lst.Add(_supp);
return _lst;
}
}
// this my controller
[HttpGet]
public ActionResult AddEditSupplier(int id)
{
clslistbox _lst = new clslistbox();
_lst.lstDocImgs= _lst.listDocImages();
return View(_lst);
}
// this is view where i am binding listboxfor
#model clslistbox
#using (Html.BeginForm("AddEditSupplier", "Admin", FormMethod.Post))
{
#Html.ListBoxFor(model => model.id, new SelectList(Model.lstDocImgs, "documentid", "title"))
}
Can anyone see the reason for it?
I think the changing of the property in the expression here is a red-herring - it won't work in either case.
Update
However, see at the end of my answer for some probably needlessly detailed exposition on why you didn't get an error first-time round.
End Update
You're using ListBoxFor - which is used to provide users with multiple selection capabilities - but you're trying to bind that to an int property - which cannot support multiple selection. (It needs to be an IEnumerable<T> at least to be able to bind a list box to it by default in MVC)
I think you mean to be using DropDownListFor - i.e. to display a list of items from which only one can be selected?
If you're actually looking for single-selection semantics in a listbox, that's trickier to do in MVC because it's Html helpers are geared entirely around listboxes being for multiple selection. Someone else on SO has asked a question about how to get a dropdown to look like a list box: How do I create a ListBox in ASP.NET MVC with single selection mode?.
Or you could generate the HTML for such a listbox yourself.
(Update) - Potentially needlessly detailed exposition(!)
The reason you don't get an exception first time round is probably because there was no value for id in ModelState when the HTML was generated. Here's the reflected MVC source (from SelectExtensions.SelectInternal) that's of interest (the GetSelectListWithDefaultValue call at the end is the source of your exception):
object obj =
allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string[])) :
htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string));
if (!flag && obj == null && !string.IsNullOrEmpty(name))
{
obj = htmlHelper.ViewData.Eval(name);
}
if (obj != null)
{
selectList =
SelectExtensions.GetSelectListWithDefaultValue(selectList, obj, allowMultiple);
}
Note first that the control variable allowMultiple is true in your case, because you've called ListBoxFor. selectList is the SelectList you create and pass as the second parameter. One of the things that MVC (unfortunately in some cases) does is to use ModelState to modify the select list you pass when re-displaying a view in order to ensure that values which were set in ModelState via a POST are re-selected when the view is reloaded (this is useful when page validation fails because you won't copy the values to your underlying model from ModelState, but the page should still show those values as being selected).
So as you can see on the first line, the model's current value for the expression/field you pass is fished out of model state; either as a string array or as a string. If that fails (returns null)then it makes another go to execute the expression (or similar) to grab the model value. If it gets a non-null value from there, it calls SelectExtensions.GetSelectListWithDefaultValue.
As I say - what you're trying to do will ultimately not work in either the case of Id or SupplierId (because they would need to be IEnumerable) but I believe this ModelState->Eval process is yielding a null value when you use Id, so the process of getting an 'adjusted' SelectList is skipped - so the exception doesn't get raised. The same is not true when you use SupplierId because I'll wager that there's either a value in ModelState at that point, or the ViewData.Eval successfully gets an integer value.
Not throwing an exception is not the same as working!.
End update
Try changing your property from int to int[]
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
Assuming above is the class used for binding the model , try changing the documentid property as below
public class SuppDocuments
{
public string Title { get; set; }
public int[] documentid { get; set; }
}
Before I start ... I can't easily migrate the project to MVC3. So.
The problem I'm having is that I've defined a custom validator attribute to check the max AND min length of a string property, StringLengthInRangeAttribute.
When the Controller calls ModelState.IsValid, on a list of Passengers only the validation of a Date property is throwing invalid, when nothing has been supplied. I guess that means my problem is not with the custom validator but all validation?
Update (additional info for clarity):
I have two symptoms of this problem :
1.The Required validator on the strings doesn't fire when they are empty
and
2.My custom validator never gets called (a breakpoint I set never gets hit).
Model:
public class Passenger
{
[Required(ErrorMessageResourceType = typeof(Resources.Messages.Passenger),
ErrorMessageResourceName = "RequireNumber")]
public int Number { get; set; }
[Required(ErrorMessageResourceType = typeof(Resources.Messages.Passenger),
ErrorMessageResourceName = "RequireSurname")]
[StringLengthInRange(MinLength = 2, MaxLength = 30, ErrorMessageResourceType = typeof(Resources.Messages.Passenger),
ErrorMessageResourceName = "MaxLengthSurname")]
public string Surname { get; set; }
}
Custom Validator:
public class StringLengthInRangeAttribute:ValidationAttribute
{
public int MinLength { get; set; }
public int MaxLength { get; set; }
public override bool IsValid(object value)
{
if (((string)value).Length < MinLength)
{
return false;
}
if (((string)value).Length > MaxLength)
{
return false;
}
return true;
}
}
Controller Action:
public ViewResult TailorHoliday(List<SearchAndBook.Models.ViewModels.Passenger> passengers,
int leadPassengerIndex)
{
if(!ModelState.IsValid)
{
return View("PassengerDetails", GetBookingState(_currentSession));
}
//...
return View();
}
Any advice appreciated. This is the first time I've used Data Annotations, so I'm quite prepared to feel stupid for missing something!
If you check the content of the ModelState property (in the debugger) you should be able to see every property that gets validated in ModelState.Keys and for each value you see the actual state in the ModelState.Values. If I look at your controller you seem to post a whole list of passengers. You should check as well, whether your values are really posted (use Firebug or Fiddler). Maybe your fields are outside the form.
Maybe you should show part of your view as well to spot the bug.