I am using a Dictionary<string, T> data structure to store phone numbers in a view model. The user can add or remove them client side before posting them back to the server.
Back story: I am using a Dictionary, because using a List<T> data structure with ASP.NET MVC 5 requires the names of form fields to contain sequential indexes starting with zero, and it becomes a real pain for JavaScript to add or remove those fields on screen without re-sequencing the index values. I found using a dictionary made it very easy. Now I'm doing a proof of concept task to enable dependency injection that allows us to use our NHibernate session to query the database during validations, and use the same session that the controllers and view models use instead of the "singleton" pattern that FluentValidation uses with MVC 5.
When using the [Validator(typeof(T))] attribute above view models, messages appear by the fields just fine, but the validator instances are singletons in the AppDomain, and the NHibernate session used by the validators is not the same one used by the controllers. This causes data to become out of sync during data validations. Validations that check the database start returning unexpected results, because NHibernate is caching so much data on the server, and it effectively has 2 separate caches.
Project setup
- ASP.NET MVC 5
- .NET Framework 4.5.1 (but we could upgrade)
- FluentValidation v8.5.0
- FluentValidation.Mvc5 v8.5.0
- FluentValidation.ValidatorAttribute v8.5.0
View models
public class PersonForm
{
public PhoneFieldsCollection Phones { get; set; }
}
public class PhoneFieldsCollection
{
public Dictionary<string, PhoneNumberFields> Items { get; set; }
}
public class PhoneNumberFields
{
[Display(Name="Country Code")]
[DataType(DataType.PhoneNumber)]
public string CountryCode { get; set; }
[Display(Name="Phone Number")]
[DataType(DataType.PhoneNumber)]
public string PhoneNumber { get; set; }
[DataType(DataType.PhoneNumber)]
public string Extension { get; set; }
[Display(Name="Type")]
public string TypeCode { get; set; }
}
View model validators
public class PersonFormValidator : AbstractValidator<PersonForm>
{
private readonly IPersonRepository repository;
public PersonFormValidator(IPersonRepository repository)
{
// Later on in proof of concept I will need to query the database
this.repository = repository;
RuleForEach(model => model.Phones)
.SetValidator(new PhoneNumberFieldsValidator());
}
}
public class PhoneNumberFieldsValidator : AbstractValidator<PhoneNumberFields>
{
public PhoneNumberFieldsValidator()
{
RuleFor(model => model.PhoneNumber)
.NotEmpty();
}
}
Controller code to validate the view models:
private bool IsModelStateValid(PersonForm model)
{
// The `repository` field is an IPersonRepository object from the DI container
var validator = new PersonFormValidator(repository);
var results = validator.Validate(model);
if (results.IsValid)
return true;
results.AddToModelState(ModelState, "");
return false;
}
Razor template code to render the page
Page level template
@model PersonForm
@Html.EditorFor(model => model.Phones)
PhoneFieldCollection editor template
@model PhoneFieldsCollection
<fieldset class="form-group form-group-phones">
<legend class="control-label col-md-3 required">
Phone Numbers:
</legend>
<div class="col-md-9">
@Html.ValidationMessageFor(model => model, "", new { role = "alert", @class = "alert alert-danger", @for = Html.IdFor(model => model) + "-addButton" })
<ol class="list-unstyled">
@foreach (var item in Model.Items)
{
if (item.Value.IsClientSideTemplate)
{
<script type="text/html">
@Html.EditorFor(model => model.Items[item.Key])
</script>
}
else
{
@Html.EditorFor(model => model.Items[item.Key])
}
}
</ol>
<hr />
<p>
<button type="button" class="btn btn-default" id="@Html.IdFor(model => model)-addButton"
data-dynamiclist-action="add"
data-dynamiclist="fieldset.form-group-phones ol">
<span class="glyphicon glyphicon-plus"></span>
Add another phone number
</button>
</p>
</div>
</fieldset>
PhoneNumberFields editor template
@model PhoneNumberFields
@Html.EditorFor(model => model.PhoneNumber)
@Html.ValidationMessageFor(model => model.PhoneNumber)
Required field message not showing up
When I POST the form back to the server with the phone number field empty, I get a validation summary message at the top of the page saying "Phone number field is required", which is what I expect. However, the call to ValidationMessageFor(model => model.PhoneNumber) in the editor template is not causing the validation message to appear by the form field.
When running the application in debug mode I get Phones[0].PhoneNumber for the name of the field that has the validation message, but the name of the field in the view model is Phones.Items[123].PhoneNumber (where 123 is a database Id, or a timestamp generated by new Date().getTime() in JavaScript).
So I know why the validation message isn't showing up next to the field. The challenge is, how can I do this?
How can I validate a Dictionary with FluentValidation so the error messages appear by the form fields when using **ValidationMessageFor(model => model.PhoneNumber) in the editor template?
Update: Looks like there is a GitHub issue from 2017 related to this: Support for IDictionary Validation. The person found a workaround, but the maintainer for FluentValidation basically said supporting this is a monster pain and would require a major refactoring job. I might try fiddling with this myself and posting an answer if I can get something to work.