ASP.NET MVC – Display visual hints for the required fields in your model
Problem
- You want to display visual hints in the view to signal that certain fields are required.
- You want the visual hints to be dynamically added when the RequiredAttribute is added to a model property.
- You want to use a familiar way of displaying the visual hints.
Solution
Let’s analyze each of the requests above:
1. You want to display visual hints in the view to signal that certain fields are required.
The most common way to do this is to add a red star (*) near the field or to color the field label in red.
2. You want the visual hints to be dynamically added when the RequiredAttribute is added to a model property.
The best way will be to create a html helper, read the IsRequired property from ModelMetadata and output a red star (<span style=”color: red;”>*<span> ).
Unfortunately for the default ModelMetadataProvider the IsRequired property is always true for all non-nullable properties with or without the RequiredAttribute applied.
One way to overcome this is to create our own ModelMetadataProvider and set the IsRequired property to true only if the RequiredAttribute is present but there is an alternative solution using reflection. So instead of using the IsRequired property we need to use reflection to see if the RequiredAttribute is present in the custom attributes collection for the current property (see the code bellow).
We will end with something like this: Html.RequiredVisualHintFor( m => m.FirstName). Now we have the visual hint displayed automatically but we need to add a new html helper besides the LabelFor, TextBoxFor (or OtherInputForhelper) and DisplayMessageFor).
3. You want to use a familiar way of displaying the visual hints.
The solution at step 2 solves our problem but introduces a new helper and thus adding more code in our view. We already have the Label html control so why not to “extend” it by adding some overloads?
MVC2 (relevant code only):
internal static MvcHtmlString LabelHelper( HtmlHelper html , ModelMetadata metadata , string htmlFieldName , string cssFieldId , string cssClassName ) { string labelText = metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split( '.' ).Last( ); if ( String.IsNullOrEmpty( labelText ) ) { return MvcHtmlString.Empty; } TagBuilder tag = new TagBuilder( "label" ); tag.Attributes.Add( "for" , html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId( htmlFieldName ) ); tag.SetInnerText( labelText ); //The Required attribute has allow multiple false bool isRequired = metadata.ContainerType.GetProperty( metadata.PropertyName ) .GetCustomAttributes( typeof( RequiredAttribute ) , false ) .Length == 1; if ( isRequired ) { if ( cssFieldId.Equals( "self" , StringComparison.OrdinalIgnoreCase ) ) { tag.AddCssClass( cssClassName ); } else { if ( cssFieldId.StartsWith( "#" ) ) { cssFieldId = cssFieldId.Remove( 0 , 1 ); } StringBuilder jQueryString = new StringBuilder( ) .Append( tag.ToString( TagRenderMode.Normal ) ) .Append( "<script type='text/javascript'>" ) .Append( "$(document).ready(function () {" ) .Append( "$('#" ).Append( cssFieldId ).Append( "').addClass('" ) .Append( cssClassName ).Append( "\')" ) .Append( "});" ) .Append( "</script> " ); return MvcHtmlString.Create( jQueryString.ToString( ) ); } } return MvcHtmlString.Create( tag.ToString( TagRenderMode.Normal ) ); }
MVC3 (relevant code only):
internal static MvcHtmlString LabelHelper( HtmlHelper html , ModelMetadata metadata , string htmlFieldName , string labelText = null , string cssFieldId = "self" , string cssClassName = "field-required" ) { string resolvedLabelText = labelText ?? metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split( '.' ).Last( ); if ( String.IsNullOrEmpty( resolvedLabelText ) ) { return MvcHtmlString.Empty; } TagBuilder tag = new TagBuilder( "label" ); tag.Attributes.Add( "for" , TagBuilder.CreateSanitizedId( html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName( htmlFieldName ) ) ); tag.SetInnerText( resolvedLabelText ); //The Required attribute has allow multiple false bool isRequired = metadata.ContainerType.GetProperty( metadata.PropertyName ) .GetCustomAttributes( typeof( RequiredAttribute ) , false ) .Length == 1; if ( isRequired ) { if ( cssFieldId.Equals( "self" , StringComparison.OrdinalIgnoreCase ) ) { tag.AddCssClass( cssClassName ); } else { if ( cssFieldId.StartsWith( "#" ) ) { cssFieldId = cssFieldId.Remove( 0 , 1 ); } StringBuilder jQueryString = new StringBuilder( ) .Append( tag.ToString( TagRenderMode.Normal ) ) .Append( "<script type='text/javascript'>" ) .Append( "$(document).ready(function () {" ) .Append( "$('#" ).Append( cssFieldId ).Append( "').addClass('" ) .Append( cssClassName ).Append( "\')" ) .Append( "});" ) .Append( "</script> " ); return MvcHtmlString.Create( jQueryString.ToString( ) ); } } return MvcHtmlString.Create( tag.ToString( TagRenderMode.Normal ) ); }
How it works
As you can see above the whole magic is in identifying if the RequiredAttribute is present, If it is then we add a class to the tag (if the “self” value is present) or we output a jQuery snippet if we need to add the class to another control. After that is all about adding a CSS style for the respective class.
See it in action
Let’s consider the following model:
public class Person { public string Prefix { get; set; } [Required( ErrorMessage = "The first name is required" )] public string FirstName { get; set; } [Required( ErrorMessage = "The middle name is required" )] public string MiddleName { get; set; } [Required( ErrorMessage = "The last name is required" )] public string LastName { get; set; } public string PrefferedName { get; set; } public string Suffix { get; set; } public bool Sex { get; set; } }
And the Edit view for it
MVC2 (Edit.aspx)
<% Html.EnableClientValidation(); %> <% using ( Html.BeginForm( "Edit" , "Person" ) ) {%> <%: Html.ValidationSummary(true) %> <fieldset> <legend>Fields</legend> <div class="editor-label"> <%: Html.LabelFor(model => model.Prefix, "self") %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.Prefix) %> <%: Html.ValidationMessageFor(model => model.Prefix) %> </div> <div class="editor-label"> <%: Html.LabelFor(model => model.FirstName, "self") %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.FirstName) %> <%: Html.ValidationMessageFor(model => model.FirstName) %> </div> <div id="divRequired"> <div class="editor-label"> <%: Html.LabelFor( model => model.MiddleName , "#divRequired" , "field-required3" )%> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.MiddleName) %> <%: Html.ValidationMessageFor(model => model.MiddleName) %> </div> </div> <div class="editor-label"> <%: Html.LabelFor(model => model.LastName, "self", "field-required2") %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.LastName) %> <%: Html.ValidationMessageFor(model => model.LastName) %> </div> <div class="editor-label"> <%: Html.LabelFor(model => model.PrefferedName) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.PrefferedName) %> <%: Html.ValidationMessageFor(model => model.PrefferedName) %> </div> <div class="editor-label"> <%: Html.LabelFor(model => model.Suffix) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.Suffix) %> <%: Html.ValidationMessageFor(model => model.Suffix) %> </div> <div class="editor-label"> <%: Html.LabelFor( model => model.Sex , "self" )%> </div> <div class="editor-field"> <%: Html.CheckBoxFor(model => model.Sex) %> <%: Html.ValidationMessageFor(model => model.Sex) %> </div> <p> <input type="submit" value="Create" /> </p> </fieldset> <% } %>
MVC3 (Edit.cshtml)
@using ( Html.BeginForm( ) ) { @Html.ValidationSummary( true ) <fieldset> <legend>Person</legend> <div class="editor-label"> @Html.LabelFor( model => model.Prefix , null , null, null ) </div> <div class="editor-field"> @Html.EditorFor( model => model.Prefix ) @Html.ValidationMessageFor( model => model.Prefix ) </div> <div class="editor-label"> @Html.LabelFor( model => model.FirstName , null , null , null ) </div> <div class="editor-field"> @Html.EditorFor( model => model.FirstName ) @Html.ValidationMessageFor( model => model.FirstName ) </div> <div id="divRequired"> <div class="editor-label"> @Html.LabelFor( model => model.MiddleName , "divRequired" , "field-required3" ) </div> <div class="editor-field"> @Html.EditorFor( model => model.MiddleName ) @Html.ValidationMessageFor( model => model.MiddleName ) </div> </div> <div class="editor-label"> @Html.LabelFor( model => model.LastName , "self" , "field-required2" ) </div> <div class="editor-field"> @Html.EditorFor( model => model.LastName ) @Html.ValidationMessageFor( model => model.LastName ) </div> <div class="editor-label"> @Html.LabelFor( model => model.PrefferedName ) </div> <div class="editor-field"> @Html.EditorFor( model => model.PrefferedName ) @Html.ValidationMessageFor( model => model.PrefferedName ) </div> <div class="editor-label"> @Html.LabelFor( model => model.Suffix ) </div> <div class="editor-field"> @Html.EditorFor( model => model.Suffix ) @Html.ValidationMessageFor( model => model.Suffix ) </div> <div class="editor-label"> @Html.LabelFor( model => model.Sex , null , null , null ) </div> <div class="editor-field"> @Html.EditorFor( model => model.Sex ) @Html.ValidationMessageFor( model => model.Sex ) </div> <p> <input type="submit" value="Create" /> </p> </fieldset> }
The styles:
/* Styles for editor and display hints ----------------------------------------------------------*/ .field-required:before { color: red; content: "*"; } .field-required2:after { font-style: italic; font-weight: bold; color: red; content: " (Required)"; } .field-required3 { border-color: #666666; border-style: dashed; width: 220px; padding: 5px; } .field-required3 label { color: red; font-style: italic; font-weight: bold; }
The result: