This article summarizes the possible solution to several issues that appeared during an MVC3 project.
Synopsis
In my recent MVC3 project, I had to manage to let user edit a two-level structure, more precisely the form contained a table of several fields, and the user had the possibility to add and remove rows dynamically. And all this with unobstructive validation.
This was not straightforward - during this project, I encountered several problems. This article and the sample project summarizes my findings and, of course possible solutions to those problems. I had to admit, I have performed exhaustive Googling and I was inspired by some sources found. I will mention these sources later on.
The sample project is as minimalist as possible, emphasizing only what’s regarding the topic. We will start from the optimistic assumption, that everything is working as expected, but we will encounter the problems I have encountered. We will investigate the problems, look for solutions, and implement one of them.
Please note that this article is based on ASP.NET MVC3 and original jquery and plugin versions, thus might not be fully applicable to other versions.
The Sample Project
The sample project is an MVC3 application for HR personnel, where they can enter employee name, select job position from a list and add skills. A skill consists of title and level. The level can be selected from predefined values.
The application contains one single controller, one view with a form - and that’s all. Actually no persistence or anything else is in place.
If you run the project, the interface looks like this:
First Version
The above domain is represented by the input model below:
namespace UDTID.InputModels
{
public class Employee
{
[Required(ErrorMessage = "Enter employee name!")]
[Display(Name="Employee name")]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.",
MinimumLength = 6)]
public string Name { get; set; }
[Required(ErrorMessage="Job position is required!")]
[Display(Name = "Job position")]
public int JobPosition { get; set; }
public List<Skill> Skills { get; set; }
public Employee()
{
Skills = new List<Skill>();
}
}
public class Skill
{
[Required(ErrorMessage = "Describe skill!")]
public string Title { get; set; }
[Required(ErrorMessage = "Select skill level!")]
public string Level { get; set; }
}
}
As we can see, the model is quite simple, and has several validation related annotations on it. Since we want dropdowns for job position and skill level, we add two additional model classes, let’s call them meta-models. Both will have a static
property that will return the list of values to be displayed with the dropdown lists. I won’t waste more time on them, since they have nothing special.
The controller is even simpler:
namespace UDTID.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var employee = new Employee();
employee.Skills.Insert(0, new Skill());
return View(employee);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(Employee employee)
{
return View(employee);
}
}
}
We create an empty entity, add an empty skill to be filled, show the view. The entity is shown after postback just as it was posted, thus user can edit it.
Let’s take a look at an interesting part of the view:
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<fieldset>
<legend>
Please enter employee data and skills:
</legend>
<div class="flow-row">
<div class="flow-editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="flow-editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="flow-row">
<div class="flow-editor-label">
@Html.LabelFor(model => model.JobPosition)
</div>
<div class="flow-editor-field">
@Html.DropDownListFor(
model => model.JobPosition,
new SelectList(UDTID.MetaModels.JobPosition.GetJobPositions(),
"Code", "Position"),
"-- Select --",
new { @class = "skill-level" })
@Html.ValidationMessageFor(model => model.JobPosition)
</div>
</div>
<table id="skills-table">
<thead>
<tr>
<th style="width:20px;"> </th>
<th style="width:160px;">Skill</th>
<th style="width:150px;">Level</th>
<th style="width:32px;"> </th>
</tr>
</thead>
<tbody>
@for (var j = 0; j < Model.Skills.Count; j++)
{
<tr valign="top">
<th><span class="rownumber"></span></th>
<td>
@Html.TextBoxFor(model => model.Skills[j].Title,
new { @class = "skill-title" })
@Html.ValidationMessageFor(model => model.Skills[j].Title)
</td>
<td>
@Html.DropDownListFor(
model => model.Skills[j].Level,
new SelectList(UDTID.MetaModels.SkillLevel.GetSkillLevels(),
"Code", "Description"),
"-- Select --",
new {@class = "skill-level"}
)
@Html.ValidationMessageFor(model => model.Skills[j].Level)
</td>
<td>
@if (j < Model.Skills.Count - 1)
{
<button type="button" class="remove-row" title="Delete row">
</button>
}
else
{
<button type="button" class="new-row" title="New row">
</button>
}
</td>
</tr>
}
</tbody>
</table>
</fieldset>
<p>
<button type="submit" id="submit">Submit</button>
</p>
}
You are right, there is nothing dynamic in it for now, but let’s try it to see if this part is working or not.
Now we ensure to have all that’s needed for unobstructive client side validation, thus we set the settings in web.config, and add all necessary client side scripts to the layout file.
Problem #1: Missing Validation Message
Let’s run the project, and without entering any data, try to submit. We expect to see validation error messages below every field.
But, no! The empty dropdown corresponding to the skill level has no error message below it. Let’s look at the generated HTML code and see the difference between the two SELECT
elements.
This is the code of the job position dropdown:
<select class="skill-level" data-val="true"
data-val-number="The field Job position must be a number."
data-val-required="Job position is required!" id="JobPosition" name="JobPosition">
and this for the skill level:
<select class="skill-level" id="Skills_0__Level" name="Skills[0].Level">
And there it is: all data-val-*
attributes are missing! It seems that the extension method implemented in SelectExtensions.cs (see original source) is missing the feature to properly retrieve metadata and generate unobstructive validation attributes for complex models.
What we could do is to add necessary attributes by hand. Since these attributes contain dashes, we have to switch from anonymous inline object to dictionary
:
@Html.DropDownListFor(
model => model.Skills[j].Level,
new SelectList(UDTID.MetaModels.SkillLevel.GetSkillLevels(), "Code", "Description"),
"-- Select --",
new Dictionary<string,object>()
{
{ "class", "skill-level" },
{ "data-val", "true" },
{ "data-val-required", "Select skill level!" }
})
Well, this is great, and it is working for sure. This is quite straightforward until we decide to add more constraints to the property, or we have a model with tens of dropdowns. Good news, that a guy shared with us and implemented the missing features (see original source of the helper). The interesting part of it is the following:
public static MvcHtmlString DdUovFor<TModel, TProperty>
(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList, string optionLabel,
IDictionary<string, object> htmlAttributes)
{
ModelMetadata metadata =
ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
IDictionary<string, object> validationAttributes =
htmlHelper.GetUnobtrusiveValidationAttributes
(ExpressionHelper.GetExpressionText(expression), metadata);
}
The original code is using the name of the property to get validation metadata, but that is not working in all situations. This code is using the lambda expression representing the model property to get the metadata and to generate the validation attributes. Thank you counsellorben, whoever you are!
Problem #2: How to Make It Dynamic?
Now that we have unobstructive validation working on all fields, we have to make the tabular input dynamic as we originally intended. There are some approaches like using some template, but let’s take an other path: cloning the last row. Since we have jquery, it is not complicated on its own:
function addTableRow(table) {
var $ttc = $(table).find("tbody tr:last");
var $tr = $ttc.clone();
$(table).find("tbody tr:last").after($tr);
};
Looks so simple – but won’t work, because this way we clone everything in the row – including all fields with all their attributes. Let’s see how this part of the view is rendered:
<tr valign="top">
<th><span class="rownumber"></span></th>
<td>
<input class="skill-title" data-val="true"
data-val-required="Describe skill!" id="Skills_0__Title" name="Skills[0].Title"
type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="Skills[0].Title"
data-valmsg-replace="true"></span>
</td>
<td>
<select class="skill-level" data-val="true"
data-val-required="Select skill level!" id="Skills_0__Level" name="Skills[0].Level">
<option value="">-- Select --</option>
<option value="0">Beginner</option>
<option value="1">Intermediate</option>
<option value="2">Expert</option>
<option value="3">Wizard</option>
</select>
<span class="field-validation-valid" data-valmsg-for="Skills[0].Level"
data-valmsg-replace="true"></span>
</td>
<td>
<button type="button" class="new-row" title="New row"> </button>
</td>
</tr>
It is obvious, that we have to handle somehow the id
and the name
attribute of the input and select element respectively. We have to increment the index during cloning. I found a post on the web (see source) about a similar but simpler scenario. The basic idea is using regular expressions to extract the index in the id
and the name
, increment it, build the new attribute and give it to the newly created elements. Is this all? No, since we have to alter the validation message SPAN
element also. And we have to change the function of the button too. Let’s see the JavaScript code with some comments:
function addTableRow(table) {
var $ttc = $(table).find("tbody tr:last");
var $tr = $ttc.clone();
$tr.find("input,select").attr("name", function () {
var parts = this.id.match(/(\D+)_(\d+)__(\D+)$/);
return parts[1] + "[" + ++parts[2] + "]." + parts[3];
}).attr("id", function () {
var parts = this.id.match(/(\D+)_(\d+)__(\D+)$/);
return parts[1] + "_" + ++parts[2] + "__" + parts[3];
});
$tr.find("span[data-valmsg-for]").attr
("data-valmsg-for", function () {
var parts = $(this).attr("data-valmsg-for").match
(/(\D+)\[(\d+)]\.(\D+)$/);
return parts[1] + "[" + ++parts[2] + "]." + parts[3];
})
$ttc.find(".new-row").attr("class", "remove-row").attr
("title", "Delete row").unbind("click").click(deleteRow);
$tr.find(".new-row").click(addRow);
$tr.find("select").val("");
$tr.find("input[type=text]").val("");
$(table).find("tbody tr:last").after($tr);
};
After we add a simple code for row deletion too, we can try it out:
Excellent, it is working like a charm. And now, let’s try to submit:
No, not again! There is no validation message in the new rows. Let’s check the generated code:
<tr vAlign="top">
<th>
<span class="rownumber"></span>
</th>
<td>
<input name="Skills[0].Title" class="skill-title" id="Skills_0__Title"
type="text" data-val-required="Describe skill!" data-val="true" value="" />
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Skills[0].Title"></span>
</td>
<td>
<select name="Skills[0].Level" class="skill-level" id="Skills_0__Level"
data-val-required="Select skill level!" data-val="true">…</select>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Skills[0].Level"></span>
</td>
<td>
<button title="Delete row" class="remove-row" type="button"> </button>
</td>
</tr>
<tr vAlign="top">
<th>
<span class="rownumber"></span>
</th>
<td>
<input name="Skills[1].Title" class="skill-title" id="Skills_1__Title"
type="text" data-val-required="Describe skill!" data-val="true" value="" />
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Skills[1].Title"></span>
</td>
<td>
<select name="Skills[1].Level" class="skill-level" id="Skills_1__Level"
data-val-required="Select skill level!" data-val="true">…</select>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Skills[1].Level"></span>
</td>
<td>
<button title="Delete row" class="remove-row" type="button"> </button>
</td>
</tr>
It looks like we did it right, the input and select element’s names and ids are correct, and even the SPAN’s data-valmsg-for
attributes are good. What’s the problem then? If we dig a little bit deeper, we find out that the unobstructive validation plugin is keeping track of the affected elements, thus our newly created ones won’t be taken into account. Now we have a new problem to solve:
Problem #3: Extending Validation
If we look carefully at the jquery.validate.unobtrusive.js file, at the very end, we see following code lines:
$(function () {
$jQval.unobtrusive.parse(document);
});
With a little jquery knowledge, we can figure out what it is doing: when the document is fully loaded, it will initiate the parse
method of the validator, which “parses all the HTML elements in the specified selector. It looks for input elements decorated with the [data-val=true
] attribute value and enables validation according to the data-val-*
attribute values” (this is the comment from the file itself).
It looks obvious to tell the validator to re-parse the document. But that’s not enough. Before doing this, we have to remove the whole form from its repository.
So this is the code we need to add at the end of the addTableRow
JavaScript function above:
var $form = $tr.closest("FORM");
$form.unbind();
$form.data("validator", null);
$.validator.unobtrusive.parse(document);
Let’s hope we solved extending the validation to the newly created rows. Let’s try it by adding some rows and submitting.
Excellent!
Now let’s fill some data in, and submit.
Oh yes, we got our input back, as expected! We are really happy and relieved.
But wait! Pascal was no biologist, let’s remove that row and submit again.
Sorry it is not English, but either way, we see a big fat exception: Modell.Skills is NULL. NULL!!! How on Earth can this happen?
Problem #4: Non-Continuous Indexes
We don’t give up, so let’s debug: we put a breakpoint in the post-handling action:
Let’s see what we have: the populated model has the Skills
property empty for real, while the request contains the missing parameters. What to do now? If we run some further attempts deleting other than the first row, we will see, that the property is populated with the rows that were before deleted one. What is the logic in this? Here it is: the built-in model binder is expecting the array to have continuous indexes starting from zero. If there is no zero-indexed element, it is totally ignored. This was our case.
What can we do? We could add some code on client side to reindex the fields on row deletion or before post. But there is a better option: let’s create a custom model binder.
The idea is to filter the request fields for the keys belonging to a field of the array. Than extracting all indexes and looping through these and the properties of the Skill
class, build a list property by property. We could make it hard-coded to that class and list, but let’s make it more general.
public class ListModelBinder<t> : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var form = controllerContext.HttpContext.Request.Form;
List<t> result = new List<t>();
Regex re = new Regex(string.Format(@"^{0}\[(\d+)]\.*",
bindingContext.ModelName), RegexOptions.IgnoreCase | RegexOptions.Compiled);
var candidates = form.AllKeys.Where(x => re.IsMatch(x));
var indices = candidates.Select
(x => int.Parse(re.Match(x).Groups[1].Value)).Distinct();
var PropInfo = typeof(T).GetProperties(BindingFlags.Public |
BindingFlags.Instance | BindingFlags.DeclaredOnly);
foreach (int i in indices)
{
T s = Activator.CreateInstance<t>();
foreach (var prop in PropInfo)
{
var value = form[string.Format("{0}[{1}].{2}",
bindingContext.ModelName, i, prop.Name)];
s.GetType().GetProperty(prop.Name).SetValue(s, value, null);
}
result.Add(s);
}
return result;
}
}
And finally, we have to add a code row to the global.asax.cs file:
ModelBinders.Binders.Add(typeof(List<Skill>), new ListModelBinder<Skill>());
It looks we made it. So let’s try it out. We add the rows, delete the first one, and submit.
Pfff… and a new problem arise…
Problem #5: Dropdown Not Showing Selected Item
Well, this is the most mysterious of all: there is no visible difference between the Skills
property bound by the built-in model binder and our custom binder. The value is there, we can even output it, but the DropDownList
is not taking it into consideration. This time we take the shortest path: since the SelectList
constructor has an additional parameter for the selected value, we simply pass the value to it.
@Html.MyDropDownListFor(model => model.Skills[j].Level,
new SelectList(UDTID.MetaModels.SkillLevel.GetSkillLevels(), "Code", "Description",
Model.Skills[j].Level), "-- Select --", new {@class = "skill-level"} )
And yes, we really made it this time.
Point of Interest
I am really curious if these bugs have been corrected in MVC4, so I will check it soon.
Conclusions
Actually, I haven’t drawn any conclusion – besides the one, that we can never be sure that something is flawless. But I am pretty sure that I will have the opportunity to take advantage about the knowledge gathered and synthesized in this article. And I hope that it will help other fellow developers too.
Updates
- 9th January, 2014 - Fellow selvan noticed a problem related to checkboxes. The
CheckBoxFor
is rendering two controls with the same name. One of them is always false
, so you get the model binder gets two values in a single input - which can not be parsed a boolean. I suggest using some hack instead - like hidden string input and/or manually rendered checkbox. - 10th March, 2014 - Fellows machallo and Piotr Machałowski have hound a bug in the model binder code, which hindered it to parse more than ten items.