A common scenario in web development is the desire to have multiple forms on the same page, some with similar field names. Those coming from an ASP.Net background may not have had to give this scenario much thought before as the use of validation groups and ASP.Net’s rendering of control names and ids make it easy to ensure that the validation messages are associated with the expected elements. When using ASP.Net MVC there are a couple of simple tricks that can be used to ensure that validation errors highlight the correct fields and the appropriate ValidationSummary is used.
An example of a view with multiple forms is a page that displays both logon and registration forms on the same page: both forms may require user name and password fields, such as the one shown below:
The first stab at writing the mark-up might look something like:
<%using (Html.BeginForm("Logon", "Logon"))
{%>
<fieldset>
<legend>Logon</legend>
<%=Html.ValidationSummary() %>
<label for="UserName">User Name:</label>
<%=Html.TextBox("UserName") %>
<label for="Password">Password:</label>
<%=Html.Password("Password") %>
</fieldset>
<input type="submit" value="Logon" />
<%} %>
<%using (Html.BeginForm("Register", "Logon"))
{%>
<fieldset>
<legend>Register</legend>
<%=Html.ValidationSummary()%>
<label for="UserName">User Name:</label>
<%=Html.TextBox("UserName")%>
<label for="Password">Password:</label>
<%=Html.Password("Password")%>
<label for="Forename">Forename:</label>
<%=Html.Password("Forename")%>
<label for="Surname">Surname:</label>
<%=Html.Password("Surname")%>
<label for="EmailAddress">Email Address:</label>
<%=Html.Password("EmailAddress")%>
</fieldset>
<input type="submit" value="Register" />
<%} %>
The corresponding controller may be along the lines of:
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Logon(CredentialsModel model)
{
NameValueCollection validationErrors = _authenticationService.Logon(model);
return CredentialsAreValid(validationErrors) ?
View("Home") :
View(model);
}
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Register(CredentialsModel model)
{
NameValueCollection validationErrors = _authenticationService.Register(model);
return CredentialsAreValid(validationErrors) ?
View("Home") :
View("Logon", model);
}
private bool CredentialsAreValid(NameValueCollection validationErrors)
{
foreach (string fieldName in validationErrors)
{
ModelState.AddModelError(fieldName, HttpUtility.HtmlEncode(validationErrors[fieldName]));
}
return validationErrors.Count == 0;
}
Both calls to the _authenticationService return a NameValueCollection where the key is the name of the property (ie control) that failed validation and the value is the error message. By way of example, the Logon method on the AuthenticationService test stub always returns sample user name and password validation errors within a NameValueCollection.
public NameValueCollection Logon(CredentialsModel model)
{
return new NameValueCollection
{
{"UserName", "User name must be a minimum of 8 characters."},
{"Password", "The password must contain both numbers and letters."}
};
}
For completeness, the CredentialsModel is as follows:
public class CredentialsModel
{
public string UserName { get; set; }
public string Password { get; set; }
public string Forename { get; set; }
public string Surname { get; set; }
public string EmailAddress { get; set; }
}
Note that we want to have the same names for both logon and password elements in the view as we wish to use the same CredentialsModel as the parameter of both the Register and Logon actions.
When submitting valid data in either form, the correct data is passed to the correct action, and all appears well and good. The shortcomings of this code so far only become apparent if one of the fields fail validation. For example, if the user was to attempt to logon without entering a user name or password, the UserName and Password elements in both the logon and registration section are highlighted (as they have the same name) and both validation summaries display the error messages:
We have two problems:
- Multiple elements with the same name;
- Multiple validation summaries with no way of distinguishing between them.
View with multiple elements with the same name
We can deal with the problem of multiple elements with the same name (which we need to have so we can use the same model for both of our actions) by taking advantage of the way ASP.Net MVC’s DefaultModelBinder deals with prefixes, inferring the prefix from the method parameter name. First, we prefix the element names in each form with a different prefix: in this case adding the logon and registration prefix as appropriate. Now the UserName element in the logon form has the name logon.UserName, whilst the UserName element in the registration form is now registration.UserName. Our view now looks like this:
<%using (Html.BeginForm("Logon", "Logon"))
{%>
<fieldset>
<legend>Logon</legend>
<%=Html.ValidationSummary() %>
<label for="logon_UserName">User Name:</label>
<%=Html.TextBox("logon.UserName") %>
<label for="logon_Password">Password:</label>
<%=Html.Password("logon.Password")%>
</fieldset>
<input type="submit" value="Logon" />
<%} %>
<%using (Html.BeginForm("Register", "Logon"))
{%>
<fieldset>
<legend>Register</legend>
<%=Html.ValidationSummary()%>
<label for="registration_UserName">User Name:</label>
<%=Html.TextBox("registration.UserName")%>
<label for="registration_Password">Password:</label>
<%=Html.Password("registration.Password")%>
<label for="registration_Forename">Forename:</label>
<%=Html.Password("registration.Forename")%>
<label for="registration_Surname">Surname:</label>
<%=Html.Password("registration.Surname")%>
<label for="registration_EmailAddress">Email Address:</label>
<%=Html.Password("registration.EmailAddress")%>
</fieldset>
<input type="submit" value="Register" />
<%} %>
Note that whilst the name of the input elements (the TextBox and Password controls) use a full-stop (“.”) as the separator between the prefix and the rest of the name, the label needs to use an underscore (“_”) when identifying the id of the element with which the label is associated (via the for property), as this is the separator the ASP.Net MVC renders when it creates the id of the element. The Action’s model parameter binds to the element’s name, whilst the HTML label identifies the element using its id.
We also need to update the parameter names in our actions so that the match our prefix:
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Logon(CredentialsModel logon)
{
NameValueCollection validationErrors = _authenticationService.Logon(logon);
return CredentialsAreValid(validationErrors, "logon") ?
View("Home") :
View(logon);
}
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Register(CredentialsModel registration)
{
NameValueCollection validationErrors = _authenticationService.Register(registration);
return CredentialsAreValid(validationErrors, "registration") ?
View("Home") :
View("Logon", registration);
}
private bool CredentialsAreValid(NameValueCollection validationErrors, string prefix)
{
foreach (string fieldName in validationErrors)
{
//Concatenate the prefix with the model property name (as stored in the key) to identify
//the name of the HTML element on the page.
ModelState.AddModelError(prefix + "." + fieldName, HttpUtility.HtmlEncode(validationErrors[fieldName]));
}
return validationErrors.Count == 0;
}
}
Our Logon action now takes a CredentialsModel parameter called logon, whilst the Register action now has a parameter called registration. This on its own won’t solve our validation problem: we also updated our call to ModelState.AddModelError within the method CredentialsAreValid, concatenating the appropriate prefix to the field name, using the full-stop separator.
Should we wish to be more explicit, or wish to use a parameter name that differs from the prefix, we can identify the prefix in the Action’s signature using the Bind attribute:
public ViewResult Logon([Bind(Prefix="logon")]CredentialsModel model)
Now when we attempt to logon without supplying a user name or password, only the elements that have failed validation are highlighted:
However, we still have a problem with the ValidationSummary, which we will deal with next.
Multiple ValidationSummary controls contained within the same page
In order to host multiple validation summaries on the same form, and be able to distinguish between each one, we adapted the Html.MyValidationSummary extension described on stackoverflow. In essence, we use ViewData to identify which ValidationSummary we wish to use to display our error messages.
First, create an extension to HtmlHelper called NamedValidationSummary, which takes a string parameter called name:
public static class HtmlExtensions
{
public static readonly string FormNameKey = "FormName";
public static string NamedValidationSummary(this HtmlHelper html, string name)
{
if (html.ViewData[FormNameKey] != null
&& !string.IsNullOrEmpty(html.ViewData[FormNameKey].ToString())
&& (html.ViewData[FormNameKey].ToString() == name))
{
return html.ValidationSummary();
}
return "";
}
}
We then update our view to use the NamedValidationSummary, supplying the appropriate prefix for the associated form as the name parameter:
<%using (Html.BeginForm("Logon", "Logon"))
{%>
<fieldset>
<legend>Logon</legend>
<%=Html.NamedValidationSummary("logon") %>
<label for="logon_UserName">User Name:</label>
<%=Html.TextBox("logon.UserName") %>
<label for="logon_Password">Password:</label>
<%=Html.Password("logon.Password") %>
</fieldset>
<input type="submit" value="Logon" />
<%} %>
<%using (Html.BeginForm("Register", "Logon"))
{%>
<fieldset>
<legend>Register</legend>
<%=Html.NamedValidationSummary("registration")%>
<label for="registration_UserName">User Name:</label>
<%=Html.TextBox("registration.UserName")%>
<label for="registration_Password">Password:</label>
<%=Html.Password("registration.Password")%>
<label for="registration_Forename">Forename:</label>
<%=Html.Password("registration.Forename")%>
<label for="registration_Surname">Surname:</label>
<%=Html.Password("registration.Surname")%>
<label for="registration_EmailAddress">Email Address:</label>
<%=Html.Password("registration.EmailAddress")%>
</fieldset>
<input type="submit" value="Register" />
<%} %>
Finally, update the CredentialsAreValid method within the controller to set the ViewData item that is used by our NamedValidationSummary (in this case, the value of HtmlExtensions.FormNameKey which we defined as "FormName" within our HtmlExtensions class) to the name we gave the the relevant NamedValidationSummary in our view, which in this case is the same as the form’s prefix:
private bool CredentialsAreValid(NameValueCollection validationErrors, string prefix)
{
foreach (string fieldName in validationErrors)
{
//Concatenate the prefix with the model property name (as stored in the key) to identify
//the name of the HTML element on the page.
ModelState.AddModelError(prefix + "." + fieldName, HttpUtility.HtmlEncode(validationErrors[fieldName]));
}
if(validationErrors.Count>0)
{
//We gave our NamedValidationSummary the same id as the prefix to the fields of the
//form we are validating.
ViewData[HtmlExtensions.FormNameKey] = prefix;
}
return validationErrors.Count == 0;
}
We have now managed to limit the displayed validation to the form that has been submitted:
