I'm hoping I'm missing something simple here.
I've configured Fluent Validation for integration with MVC and it's been working quite well up until now. I'm now working on a scenario where a user is performing a standard create of what's called a "service". A service has hours that have to be defined.
The view model for this Create action is defined as follows:
[Validator(typeof (CreateServiceViewModelValidator))]
public class CreateServiceViewModel
{
public string Name { get; set; }
//...Other properties...
public Collection<CreateServiceHoursViewModel> ServiceHours { get; set; }
}
and CreateServiceHoursViewModel is defined as...
public class CreateServiceHoursViewModel
{
//...Other properties...
public DayOfWeek DayOfWeekId { get; set; }
public DateTimeOffset? OpenTime { get; set; }
public DateTimeOffset? CloseTime { get; set; }
}
The quick and dirty version of the UI ends up as follows:
The problem:
The fluent validation messages for the collection of hours are not showing the expected error message. They're displaying the standard error messages from Fluent Validation.
Here are my validators:
public class CreateServiceViewModelValidator : AbstractValidator<CreateServiceViewModel>
{
public CreateServiceViewModelValidator()
{
RuleFor(f => f.Name).NotEmpty()
.WithMessage("You must enter a name for this service.");
RuleFor(f => f.Description)
.NotEmpty().WithMessage("Service must have a description")
.Length(3, 256).WithMessage("Description must be less than 256 characters.");
RuleFor(f => f.ServiceHours).SetCollectionValidator(new CreateServiceHoursViewModelValidator());
}
}
and the HoursValidator
public class CreateServiceHoursViewModelValidator : AbstractValidator<CreateServiceHoursViewModel>
{
public CreateServiceHoursViewModelValidator()
{
DateTimeOffset test;
DayOfWeek enumTest;
RuleFor(r => r.DayOfWeekId).Must(byteId => Enum.TryParse(byteId.ToString(), out enumTest)).WithMessage("Not a valid day of week...");
RuleFor(f => f.OpenTime)
.NotEmpty().WithMessage("Please specify an opening time...")
.Must(openTime =>
DateTimeOffset.TryParse(openTime.HasValue ? openTime.Value.ToString() : String.Empty, out test))
.WithMessage("Not a valid time...");
RuleFor(f => f.CloseTime)
.NotEmpty().WithMessage("Please specify a closing time...")
.Must(closeTime =>
DateTimeOffset.TryParse(closeTime.HasValue ? closeTime.Value.ToString() : String.Empty, out test))
.WithMessage("Not a valid time...");
}
}
and with errors on the hours collection:
When I run the validate method manually in my controller action the correct error messages are returned...
var validator = new CreateServiceViewModelValidator();
var results = validator.Validate(model);
foreach (var result in results.Errors)
{
Console.WriteLine("Property name: " + result.PropertyName);
Console.WriteLine("Error: " + result.ErrorMessage);
Console.WriteLine("");
}
This returns the messages I'd expect.
What am I missing or doing incorrect that the error messages for the hours collection from the fluent validations aren't being persisted to my view? (The main object validators work as expected)
Any info appreciated!
(I can update with my view if needed. I felt this question was plenty long already. Suffice it to say I have a view that uses an editor template to iterate the collection of service hours.)
#for (int weekCounter = 0; weekCounter <= 6; weekCounter++)
{
#Html.DisplayFor(model => model.ServiceHours[weekCounter])
}
(cross-posted to http://fluentvalidation.codeplex.com/discussions/267990)
The error messages you're seeing aren't coming from FluentValidation.
"The value is not valid for CloseTime" is an MVC error message that is generated before FluentValidation has a chance to kick in.
This is happening because FluentValidation works by validating the entire object once all the properties have been set, but in your case the string "*Enter closing time here" is not a valid DateTime, therefore MVC cannot actually set the property to a valid datetime, and generates an error.
Related
This is the first time I'm trying to implement FluentValidation since I need to cover a complex validation scenario.
The class I'm trying to validate has a large quantity of properties, complex objects and several collections.
I didn't have troubles to validate properties of the main class or even checking if collections are not empty, but I do have problems while validating objects properties within each collection.
To implement this I followed the examples documented here (check under "Re-using Validators for Collections"):
http://fluentvalidation.codeplex.com/wikipage?title=creatingavalidator
These are my model classes (reduced to improve readability)
public class Caso
{
public int Id { get; set; }
public string Descripcion { get; set; }
public List<Medicamento> Medicamentos { get; set; }
}
public class Medicamento
{
public int Id { get; set; }
public string Nombre { get; set; }
}
These are the validator classes:
public class CasoValidator : AbstractValidator<CasoAdverso>
{
public CasoValidator()
{
RuleSet("Iniciar", () =>
{
// Validated OK
RuleFor(x => x.Descripcion).NotEmpty();
// Validated OK
RuleFor(x => x.Medicamentos).Must(x => x != null && x.Count > 0).WithMessage("No puede iniciar un caso sin medicamentos cargados");
RuleFor(x => x.Medicamentos).SetCollectionValidator(new MedicamentoValidator());
});
}
}
public class MedicamentoValidator : AbstractValidator<Medicamento>
{
public MedicamentoValidator()
{
// NOT Validated. Even if the object property is empty the error message doesn't appear. I also checked using "NotNull" and "NotEmpty" clauses
RuleFor(x => x.Nombre).NotNull().WithMessage("Debe especificar un nombre");
}
}
(Note: I'm using RuleSet because of different validation schemas which are dependent of the document status in the workflow)
I'm executing the validation manually from the controller (no MVC integration)
[HttpPost]
public ActionResult Iniciar(Caso c)
{
CasoValidator validator = new CasoValidator();
FluentValidation.Results.ValidationResult validate = validator.Validate(c, ruleSet: "Iniciar");
// ...
}
With this implementation properties of the main class are validated fine but I need also to validate each property of the "Medicamento" class within the collection.
Could I be missing something here?. Should this be validated using the RuleForEach clause available?
Any help will be appreciated.
It appears the RuleSet setting is applying to the child validator as well as the primary one.
I tested your code in an xUnit.net test, and confirmed it.
If you change your rulesets to execute you should find it works as expected:
CasoValidator validator = new CasoValidator();
FluentValidation.Results.ValidationResult validate = validator.Validate(c, ruleSet: "default,Iniciar");
The 'default' ruleset will work on the MedicamentoValidator rules.
I didn't find this in the documentation, only through testing.
This is the sample unit test:
[Fact]
public void Test1()
{
Caso c = new Caso()
{
Id = 1,
Descripcion = "none",
Medicamentos = new List<Medicamento>()
};
c.Medicamentos.Add(new Medicamento()
{
Id = 0,
Nombre= null
});
CasoValidator validator = new CasoValidator();
FluentValidation.Results.ValidationResult validate = validator.Validate(c, ruleSet: "default,Iniciar");
Assert.NotEmpty(validate.Errors);
}
Update: i found a reference by Jeremy Skinner for exactly this behavior:
http://fluentvalidation.codeplex.com/discussions/266920
Rulesets cascade to any child validators, so whichever ruleset is
selected for use by the top-level validator will also be used by the
child validator.
So if you ran the "Minimal" ruleset on the CreateProfileModelValidator, then only rules in the "Minimal" ruleset
will be run on both the CreateProfileModelValidator and the
ProfileValidator.
as a complementary:
a collection named GroupMemberIds should have AdminMemebrId:
RuleFor(r => new { r.GroupMemberIds, r.AdminMemberId }).Must(a => a.GroupMemberIds.Contains(a.AdminMemberId));
Scenario :-
I am developing MVC 4 application, the website will run in several languages and will be hosted on Azure.
For localizing we are relying on the database rather than resource bundle approach.
Problem :-
I want to customize error messages at runtime, I want to localize the messages through the database.
I have tried to change attribute values through reflection but it had not worked.
Code :-
//Model
public class Home
{
[Required(ErrorMessage = "Hard coded error msg")]
public string LogoutLabel { get; set; }
}
//On controller
public ActionResult Index()
{
Home homeData = new Home();
foreach (PropertyInfo prop in homeData.GetType().GetProperties())
{
foreach (Attribute attribute in prop.GetCustomAttributes(false))
{
RequiredAttribute rerd = attribute as RequiredAttribute;
if (rerd != null)
{
rerd.ErrorMessage = "dynamic message";
}
}
}
return View(homeData);
}
On client side when validation takes place it shows me old message "Hard Coded error msg".
Please suggest how this can be customised if we donot want to use Resource bundle approach
You would better create and register your own DataAnnotationsModelMetadataProvider where you can just override the error messages. For more detail please see the answers to the similar question here MVC Validation Error Messages not hardcoded in Attributes
are you intended to implement this nested loop to localize validation message of all you'r entities ? i think no.a better solution is using Validator attribute.
for you'r class :
[Validator(typeof(HomeValidator))]
public class Home
{
public string LogoutLabel { get; set; }
}
now lets implement HomeValidator :
public class HomeValidator : AbstractValidator<Home>
{
public HomeValidator()
{
RuleFor(x => x.LogoutLabel ).NotEmpty().WithMessage("your localized message");
}
}
Why don't DataAnnotations work on public fields? Example:
namespace Models
{
public class Product
{
[Display(Name = "Name")]
public string Title; // { get; set; }
}
}
public ActionResult Test()
{
return View(new Models.Product() { Title = "why no love?" });
}
#Html.LabelFor(m => m.Title) // will return 'Title' if field, or 'Name' if property
#Html.DisplayFor(m => m.Title)
If Title is a field, then the Display attribute seems to have no effect. If Title is changed to a property, it works as expected as displays "Name".
It would seem easy in this example to just change to a property, but I am trying to use the types from F# where they get compiled to a class with fields and not properties.
This was tested in ASP.NET 4 and MVC RC 3.
The reason why DataAnnotations do not work with fields is because the reflection-like mechanism that is used to retrieve the attributes (TypeDescriptor) only supports properties.
While it would not be easy, we could look into making this work with fields if there is enough demand.
I am using server side validation like
public IEnumerable<RuleViolation> GetRuleViolations()
{
if (String.IsNullOrEmpty(Name))
yield return new RuleViolation("Name is Required", "Name");
if (Price == 0)
yield return new RuleViolation("Price is Required", "Price");
yield break;
}
When I left Price as blank, Then It takes 0 as a value.
So I check it with 0.
In my Database Price cannot be null; and I am using LINQ-to-SQL class.
Now my problem is when I left Price blank it gives me two messages.e.g.
A value is required.
Price is Required.
So How do I put custom validation without showing first error message?
Relpy to comment
I am reffering book code of Professional Asp.net MVC 1.0 here.
HTML pages of Book are Here.
usefull page.
public class RuleViolation
{
public string ErrorMessage { get; private set; }
public string PropertyName { get; private set; }
public RuleViolation(string errorMessage)
{
ErrorMessage = errorMessage;
}
public RuleViolation(string errorMessage, string propertyName)
{
ErrorMessage= errorMessage;
PropertyName = propertyName;
}
}
I think you get the first message "A value is required" automatically from the framework because your Price property is a value type, which can never be null.
So when you post a blank field, the framework will usually try to assign null to this property, which is not possible in this case.
If you change the type to nullable:
public double? Price { get; set; }
That particular message should disappear. Then you could change your validation to:
if (Price == null)
yield return new RuleViolation("Price is required", "Price");
The fact that the database field does not allow nulls should not interfere with your viewmodels.
To make what Thomas Eyde wrote above work (without messing with the code), you can...
Open the corresponding .dbml file
Click the "Price" property
In the Visual Studio Properties window, change the Nullable value from False to True
Save the file!
You can now go into your class and add the if statement and VS should not complain.
That's because the Default Model Binder adds that error. You can write your own model binder for that particular object and work directly with the form collection to get more control over validation.
There appears to be something of a hole in the way DataAnnotations works in that a user entering in some text into a field that will go into an int will never reach the DataAnnotations code. It kicks off a model binding error and displays the error to the user "The value 'a' is not valid for the XXXX field."
Anyway, it's all very nice that it automatically handles this situation, but I actually want to display an error message indicating the problem eg. "The value 'a' is not numeric. Please enter in a numeric value for the XXXX field".
I have tried the solutions set out How to replace the default ModelState error message in Asp.net MVC 2? and ASP.NET MVC - Custom validation message for value types, but I can't get them to work.
It appears that my resource file is not being read at all, since here (http://msdn.microsoft.com/en-us/library/system.web.mvc.defaultmodelbinder.resourceclasskey.aspx) it states "If the property is set to an invalid class key (such as a resource file that does not exist), MVC throws an exception." and even if I change the line to DefaultModelBinder.ResourceClassKey = "asdfasdhfk" there is no exception.
Anyone have any ideas?
EDIT: Here is some code. All of it is working minus my Messages.resx file's messages are not being used. The code for Messages.resx is auto generated so I won't include it.
So entering "a" into ProcessOrder results in a generic message rather than what I have entered into Messages.resx for PropertyValueInvalid (and InvalidPropertyValue for good measure).
Application_Start method
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.DefaultBinder = new Microsoft.Web.Mvc.DataAnnotations.DataAnnotationsModelBinder(); //set dataanooations to be used
DefaultModelBinder.ResourceClassKey = "Messages"; //set data annotations to look in messages.resx for the default messages
ValidationExtensions.ResourceClassKey = "Messages";
}
Entity Class
[MetadataType(typeof(GLMetaData))]
public partial class GL
{
}
public class GLMetaData
{
public int TransRefId { get; set; }
[DisplayName("Process Order")]
public int? ProcessOrder { get; set; }
[DisplayName("Trans Type")]
[StringLength(50)]
public string TransType { get; set; }
[StringLength(100)]
public string Description { get; set; }
[DisplayName("GL Code")]
[StringLength(20)]
public string GLCode { get; set; }
[DisplayName("Agents Credit No")]
[StringLength(50)]
public string AgentsCreditNo { get; set; }
[Required]
public bool Active { get; set; }
}
Controller Action:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(GL glToBeUpdated)
{
try
{
if (!ModelState.IsValid)
return View(glToBeUpdated);
//set auto properties
glToBeUpdated.UpdateDate = DateTime.Now;
glToBeUpdated.UpdateUser = this.CurrentUser;
glDataLayer.update(glToBeUpdated);
glDataLayer.submitChanges();
return RedirectToAction("Index");
}
catch
{
glDataLayer.abortChanges();
throw;
}
}
What I did to combat a similar issue was to clear the model state, validate against ModelState["XXXX"].Value.AttemptedValue instead of against the nulled value caused by an trying to put an invalid value into the Model's property, populating the error messages and resetting the Model values.
That way I can have the error messages I want and if necessary offer more than one ("a value is required" or "the value must be numeric").
I have battled this for most of the day on MVC4 RC. No matter what i set
DefaultModelBinder.ResourceClassKey
to it never seemed to work. It also never threw an exception when I assigned junk.
This is what I was using to assign the value (to no avail):
DefaultModelBinder.ResourceClassKey = typeof(App_GlobalResources.ValidationMessages).Name;
In the end I decided to tackle this error message on the client side and override the data attribute that jQuery uses to display the message.
#Html.TextBoxFor(m => m.Amount, new Dictionary<string,object>(){{"data-val-number","Invalid Number"}})
this is working how I need it to.
Ironically this works too:
#Html.TextBoxFor(m => m.Amount, new Dictionary<string, object>() {{ "data-val-number", HttpContext.GetGlobalResourceObject("ValidationMessages", "PropertyValueInvalid") } })
Here I have taken Contact number field as string but with Range Attribute so can provide numeric validatioin to use if from your Resource file .
[Required(ErrorMessageResourceType = typeof(Global), ErrorMessageResourceName = "ContactNumberRequired")]
[Range(0, int.MaxValue, ErrorMessageResourceType = typeof(Global), ErrorMessageResourceName = "ValidContactNumber")]
[Display(Name = "Contact Number")]
public string ContactNumber { get; set; }
So now here provided ErrorMessageResourceName as key . You can customize and use it also in Multi Language