Why is IValidatableObject.Validate only called if property validation passes? - asp.net

In my model, it seems that Validate() is only called AFTER both properties pass validation.
public class MyModel : IValidatableObject
{
[Required]
public string Name { get; set;}
[Required]
public string Nicknames {get; set;}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(Nicknames != null && Nicknames.Split(Environment.NewLine.ToCharArray()).Count() < 2)
return yield result new ValidationResult("Enter at least two nicknames, new [] { "Nicknames" });
}
}
When a user enters a single line of text in the Nicknames text area but leaves the Name text box empty, only the Required error message for the Name property is displayed. The error message that should be displayed from the Validate() function never shows up.
Only after entering a name in the Name text box and some text in the Nicknames text is the Validate() function called.
Is this how it's supposed to work? It seems odd that a user is shown an error message on a subsequent page when the error is being caused on the current page.

This is by design. Object-level validation does not fire until all the properties pass validation because otherwise it is possible that the object is incomplete. The Validate method is meant for thing like comparing one property to another. In your case you should write a custom property validator.

Related

Asp.Net Core Model binding, how to get empty field to bind as a blank string?

If a form like the one below is submitted and MyField is left blank on the form, then by default Asp.Net Core model binding will place null into the corresponding property on the model as indicated below.
Example Form
<form asp-controller="SomeController" asp-action="SomeAction">
<label asp-for="MyField">My Field</label><input asp-for="MyField" type="text" />
<button type="submit">Submit</button>
</form>
Example Model
public class MyModel{
public string MyField { get; set; }
}
Example Action Method
[HttpPost]
public IActionResult Post(MyModel m) {
//m.MyField will be null if the field was left empty
//but I want it set to a blank string by the model binder
}
However, since MyField is actually transmitted in the Http Post body I'd prefer that the model binder set the MyField property on the model to a blank string rather than setting it to null. I'd prefer to reserve null for cases where MyField is not transmitted in the Http Post body. How can the model binder be changed to exhibit this behavior?
Studying the ASP.NET Core code for SimpleTypeModelBinder at https://github.com/aspnet/Mvc/blob/rel/1.1.3/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinder.cs I could see that there is a ModelMetadata.ConvertEmptyStringToNull setting which is set to true by default that is causing the blank string to be converted to null on data binding. But the property is read only so at first I couldn't figure out how to changes its value.
#rsheptolut's post on this page https://github.com/aspnet/Mvc/issues/4988 led me to a solution.
Solution:
The value has to get set at startup. This can be done via this class:
public class CustomMetadataProvider : IMetadataDetailsProvider, IDisplayMetadataProvider {
public void CreateDisplayMetadata(DisplayMetadataProviderContext context) {
if (context.Key.MetadataKind == ModelMetadataKind.Property) {
context.DisplayMetadata.ConvertEmptyStringToNull = false;
}
}
}
When it's hooked into MvcOptions in the ConfigureServices method of the startup.cs file like so
services.AddMvc()
.AddMvcOptions(options => options.ModelMetadataDetailsProviders.Add(new CustomMetadataProvider ()));
Now site wide, the default for a blank field that is posted back will be for the data binder to set the corresponding model property to a blank string rather than to null. Yea!
Have you tried making the property have a default value of empty string?
public string MyField { get; set; } = string.Empty;
an uglier solution to try is:
private string myField = string.Empty;
public string MyField
{
get { return myField ?? string.Empty; }
set { myField = value; }
}
I think it should work

Custom error for view-model int overflow (invalid values)

In my model I have an nullable int.
In the view I have an input of type 'number'.
The problem is that the user could possibly input a number larger than the maximum possible integer value, which results in the following error:
The value [value] is not valid for [field]
I want to change this message, and putting a [Range(...)] validation on the attribute does not help, as non of my validations are being called, because the framework cannot parse the value to an int.
--------- EDIT ---------
My validations perform normally at normal input as shown here:
What I mean about my validations not being called is that I've read that the if the framework fails to parse the value to an int (because the value exceeds the max value of an integer), it ignores any further "unnecessary" validation.
Therefore my Range validation is ignored, as shown here:
I've tried creating a custom ValidationAttribute, but that has the same behaviour (Being called correctly at normal integer input, but is ignored at overflowing integer input)
--------- /EDIT ---------
All the solutions I have seen, proposes binding a custom resource file, in which I could override the default message for invalid values.
But I want to be able to specify a custom message at each attribute in case of an invalid value.
I thought about using strings instead of ints, and creating a custom ValidationAttribute which tries to parse it to an int.
Are there any alternatives?
// EDIT: I could avoid this problem (to some degree) by having client-side validation - But I want to know if it is possible from the server-side, in case of someone editing the html and forcing a higher value
There is couple of ways in which you can validate user input:
proper usage of Data Annotations - you could take a look here: http://www.asp.net/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6 and here: https://msdn.microsoft.com/en-us/library/ee256141(v=vs.100).aspx
usage of jQuery Validation - http://jqueryvalidation.org/
Here is an example of custom validation attribute:
public class MyIntegerValidationAttribute : ValidationAttribute
{
public string[] PropertyNames { get; }
public MyIntegerValidationAttribute(params string[] propertyNames)
{
PropertyNames = propertyNames;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
//here you have values of your properties
var values = properties.Select(p => p.GetValue(validationContext.ObjectInstance, null)).OfType<int>();
if (YOUR_CUSTOM_CONDITION)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
}
public class ViewModel
{
[MyIntegerValidationAttribute("B", "C", ErrorMessage = "My error message")]
public int A { get; set; }
public int B { get; set; }
public int C { get; set; }
}

ASP.NET MVC Model validation error beside field

I have a self validation model in my code based on this link:
ASP.NET MVC: Custom Validation by DataAnnotation
public class TestModel : IValidatableObject
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Title == null)
yield return new ValidationResult("The title is mandatory.", new [] { "Title" });
if (Description == null)
yield return new ValidationResult("The description is mandatory.", new [] { "Description" });
}
}
All of this works well. But my question is this: The error messages above are displayed as ValidationSummary errors. Is there any way to make the title error message display beside the title field (on the form view) and the description error message display beside the description field, just like in client side validation?
First make sure that you have added the correct razor markup next to each field, for example:
#Html.ValidationMesageFor(m => m.Title)
This will display the error message only if there is an error in the ModelState that is associated with the same field, so ensure ModelState["Title"] contains the error, otherwise you won't see the message.
This all is customized, at least, by CSS, or you can always use javascript (jquery).
UPDATE:
Well, i think, this is quite little info, therefore, try to add a some more.Basically, you also can use html.helper ValidationMessageFor + span tag. For instance:
#Html.EditorFor(x=>x.ModelProperty)<span>#Html.ValidationMessageFor(x=>x.ModelProperty)</span>
And in action method after your magic object validates itself, analyze result of your validation.
Just raugh demo:
var result = myObject.Validate(someContext);
if (result != result.Success)
{
ModelState.AddModelError("Title"); // if error in Title prop (otherwise "Description")
return View (myObject);
}
Or, if your model is validated through ValidationAttribute, you can check out this via ModelState.IsValid or ModelState.IsValidField methods.

IValidatableObject Validate method firing when DataAnnotations fails

I've a ViewModel which has some DataAnnotations validations and then for more complex validations implements IValidatableObject and uses Validate method.
The behavior I was expecting was this one: first all the DataAnnotations and then, only if there were no errors, the Validate method. How ever I find out that this isn't always true. My ViewModel (a demo one) has three fileds one string, one decimal and one decimal?. All the three properties have only Required attribute. For the string and the decimal? the behavior is the expected one, but for the decimal, when empty, Required validation fails (so far so good) and then executes the Validate method. If I inspect the property its value is zero.
What is going on here? What am I missing?
Note: I know that Required attribute is suppose to check if the value is null. So I'd expect to be told not to use Required attribute in not-nullable types (because it wont ever trigger), or, that somehow the attribute understand the POST values and note that the field wasn't filled. In the first case the attribute shouldn't trigger and the Validate method should fire. In the second case the attribute should trigger and the Validate method shouldn't fire. But my result are: the attributes triggers and the Validate method fires.
Here is the code (nothing too special):
Controller:
public ActionResult Index()
{
return View(HomeModel.LoadHome());
}
[HttpPost]
public ActionResult Index(HomeViewModel viewModel)
{
try
{
if (ModelState.IsValid)
{
HomeModel.ProcessHome(viewModel);
return RedirectToAction("Index", "Result");
}
}
catch (ApplicationException ex)
{
ModelState.AddModelError(string.Empty, ex.Message);
}
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, "Internal error.");
}
return View(viewModel);
}
Model:
public static HomeViewModel LoadHome()
{
HomeViewModel viewModel = new HomeViewModel();
viewModel.String = string.Empty;
return viewModel;
}
public static void ProcessHome(HomeViewModel viewModel)
{
// Not relevant code
}
ViewModel:
public class HomeViewModel : IValidatableObject
{
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "string")]
public string String { get; set; }
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "decimal")]
public decimal Decimal { get; set; }
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "decimal?")]
public decimal? DecimalNullable { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
yield return new ValidationResult("Error from Validate method");
}
}
View:
#model MVCTest1.ViewModels.HomeViewModel
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Html.BeginForm(null, null, FormMethod.Post))
{
<div>
#Html.ValidationSummary()
</div>
<label id="lblNombre" for="Nombre">Nombre:</label>
#Html.TextBoxFor(m => m.Nombre)
<label id="lblDecimal" for="Decimal">Decimal:</label>
#Html.TextBoxFor(m => m.Decimal)
<label id="lblDecimalNullable" for="DecimalNullable">Decimal?:</label>
#Html.TextBoxFor(m => m.DecimalNullable)
<button type="submit" id="aceptar">Aceptar</button>
<button type="submit" id="superAceptar">SuperAceptar</button>
#Html.HiddenFor(m => m.Accion)
}
Considerations after comments' exchange:
The consensual and expected behavior among developers is that IValidatableObject's method Validate() is only called if no validation attributes are triggered. In short, the expected algorithm is this (taken from the previous link):
Validate property-level attributes
If any validators are invalid, abort validation returning the failure(s)
Validate the object-level attributes
If any validators are invalid, abort validation returning the failure(s)
If on the desktop framework and the object implements IValidatableObject, then call its Validate method and return any failure(s)
However, using question's code, Validate is called even after [Required] triggers. This seems an obvious MVC bug. Which is reported here.
Three possible workarounds:
There's a workaround here although with some stated problems with it's usage, apart from breaking the MVC expected behavior. With a few changes to avoid showing more than one error for the same field here is the code:
viewModel
.Validate(new ValidationContext(viewModel, null, null))
.ToList()
.ForEach(e => e.MemberNames.ToList().ForEach(m =>
{
if (ModelState[m].Errors.Count == 0)
ModelState.AddModelError(m, e.ErrorMessage);
}));
Forget IValidatableObject and use only attributes. It's clean, direct, better to handle localization and best of all its reusable among all models. Just implement ValidationAttribute for each validation you want to do. You can validate the all model or particular properties, that's up to you. Apart from the attributes available by default (DataType, Regex, Required and all that stuff) there are several libraries with the most used validations. One which implements the "missing ones" is FluentValidation.
Implement only IValidatableObject interface throwing away data annotations. This seems a reasonable option if it's a very particular model and it doesn't requires much validation. On most cases the developer will be doing all that regular and common validation (i.e. Required, etc.) which leads to code duplication on validations already implemented by default if attributes were used. There's also no re-usability.
Answer before comments:
First of all I've created a new project, from scratch with only the code you provided. It NEVER triggered both data annotations and Validate method at the same time.
Anyway, know this,
By design, MVC3 adds a [Required]attribute to non-nullable value types, like int, DateTime or, yes, decimal. So, even if you remove required attribute from that decimal it works just like it is one there.
This is debatable for its wrongness (or not) but its the way it's designed.
In you example:
'DataAnnotation' triggers if [Required] is present and no value is given. Totally understandable from my point of view
'DataAnnotation' triggers if no [Required] is present but value is non-nullable. Debatable but I tend to agree with it because if the property is non-nullable, a value must be inputted, otherwise don't show it to the user or just use a nullable decimal.
This behavior, as it seems, may be turned off with this within your Application_Start method:
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
I guess the property's name is self-explanatory.
Anyway, I don't understand why do you want to the user to input something not required and don't make that property nullable. If it's null then it is your job to check for it, if you don't wan't it to be null, before validation, within the controller.
public ActionResult Index(HomeViewModel viewModel)
{
// Complete values that the user may have
// not filled (all not-required / nullables)
if (viewModel.Decimal == null)
{
viewModel.Decimal = 0m;
}
// Now I can validate the model
if (ModelState.IsValid)
{
HomeModel.ProcessHome(viewModel);
return RedirectToAction("Ok");
}
}
What do you think it's wrong on this approach or shouldn't be this way?

ASP.NET MVC DropDownList Validation

I have
[DisplayName("Country")]
public List<SelectListItem> Countries { get; set; }
property in strong-type Model View class for DropDownList.
When I try to check if the ModelState.IsValid on form postback it's always false & error for Countries tells "Can't convert [value] to SelectListItem" or some of a kind.
I figured out there is no straight-forward mapping for drop down selected value (looks like I'll have to read value from Form value collection), but how can I ignore binding and validation for List property? I just want to make ModelState.IsValid attribute to be true if all the other fields are populated properly.
Thank you in advance
Finally I used workaround.
My model now:
class Model
{
...
[DisplayName("Country")]
List<Country> Countries;
Guid CountrySelected <-- new field!
...
}
I use Html.DropDownList("CountrySelected", Model.Countries.Select(x => new SelectItemList.. )
instead of HtmlDropDownListFor. HtmlDropDownListFor maps [id selected] to List not to CountrySelected property. Now [id selected] is mapped to CountrySelected
Is it because the value submitted is of type String or Country rather than a list of SelectListItem or List<SelectListItem>.
What are you binding to the control in the UI?
try
[DisplayName("Country")]
public List<Country> Countries { get; set; }
Where Country is the type name from your DAL.
EDIT:
Based on the error you are recieving, it sounds like the model is expecting the value to be a String so try swapping List<Country> for List<String>.
[DisplayName("Country")]
public List<string> Countries { get; set; }

Resources