ASP.NET MVC View Best Practices – Save time creating views with MvcContrib.FluentHtml
TweetThis isn’t strictly a best practice post, but it is now I am currently creating html in my ASP.NET MVC views.
It must be a good approach because Microsoft are implementing Templated Helpers to do what I’m showing you today in ASP.NET MVC 2. See Ben Scheirmans’ post A First Look at ASP.NET MVC 2 for more information.
In this post I want to show you two view implementations based on adapted versions of the Account Controller and LogOn view which are created whenever you do File > New > Project > ASP.NET MVC Web Application.
The slimmed down AccountController looks like this
using System;
using System.Web.Mvc;
namespace AspNetMvcViewBestPractices.Controllers
{
public class AccountController : Controller
{
public ActionResult LogOn()
{
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string UserName, string Password, bool RememberMe)
{
if (!ValidateLogOn(UserName, Password))
{
return View();
}
return RedirectToAction("Index", "Product");
}
private bool ValidateLogOn(string userName, string password)
{
if (String.IsNullOrEmpty(userName))
{
ModelState.AddModelError("UserName", "You must specify a username.");
}
if (String.IsNullOrEmpty(password))
{
ModelState.AddModelError("Password", "You must specify a password.");
}
if (!(userName == "admin" && password == "password"))
{
ModelState.AddModelError("_FORM", "The username or password provided is incorrect.");
}
return ModelState.IsValid;
}
}
}
The LogOn view is shown below
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="loginContent" ContentPlaceHolderID="MainContent" runat="server">
<h2>Log On</h2>
<%= Html.ValidationSummary("Login was unsuccessful. Please correct the errors and try again.") %>
<% using (Html.BeginForm()) { %>
<div>
<fieldset>
<legend>Account Information</legend>
<p>
<label for="UserName">UserName</label>
<%= Html.TextBox("UserName") %>
<%= Html.ValidationMessage("UserName") %>
</p>
<p>
<label for="Password">Password</label>
<%= Html.Password("Password") %>
<%= Html.ValidationMessage("Password") %>
</p>
<p>
<%= Html.CheckBox("RememberMe") %> <label class="inline" for="RememberMe">Remember me?</label>
</p>
<p>
<input type="submit" value="Log On" />
</p>
</fieldset>
</div>
<% } %>
</asp:Content>
There are two problems I have this view. The first is that everything is tightly coupled using what are known as magic strings. For example if you change the line
<%= Html.TextBox("UserName") %>
to
<%= Html.TextBox("Username") %>
the UserName argument in the LogOn form post action no longer gets populated when the form is submitted. This is because the name for the id in the form and name in the method signature need to be the same.
The other is the amount of html you have to manually write. Sometimes Visual Studio can tell you if have made a typo in your html mark up, but not always. For example it can’t detect an error like this
<input type="sumbit" />
As I mentioned in my previous post unless you go through the laborious process of creating front end tests, the first time you will notice is when you view the website in a browser or one of your customers tell you.
How I would create the LogOn view
1. Create a LogOnForm data transfer object (DTO)
namespace AspNetMvcViewBestPractices.Models
{
public class LogOnForm
{
public string UserName { get; set; }
public string Password { get; set; }
public bool RememberMe { get; set; }
}
}
2. Change the signature of the Account controllers’ LogOn post action to take in the LogOnForm DTO. This model will be populated by the ASP.NET MVC model binders using the form values.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(LogOnForm logOnForm)
{
if (!ValidateLogOn(logOnForm.UserName, logOnForm.Password))
{
return View(logOnForm);
}
return RedirectToAction("Index", "Product");
}
3. Add a reference to the MvcContrib.FluentHtml assembly.
4. Change the view so it inherits from ‘MvcContrib.FluentHtml.ModelViewPage
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="MvcContrib.FluentHtml.ModelViewPage<LogOnForm>" %>
5. Use the MvcContrib.FluentHtml assembly to create the html in the view
<fieldset>
<legend>Account Information</legend>
<p>
<%=this.TextBox(m => m.UserName).Label("UserName")%>
<%=this.ValidationMessage(m => m.UserName)%>
</p>
<p>
<%=this.TextBox(m => m.Password).Label("Password")%>
<%=this.ValidationMessage(m => m.Password )%>
</p>
<p>
<%=this.CheckBox(c => c.RememberMe).LabelAfter(" Remember me?","inline")%>
</p>
<p>
<%=this.SubmitButton("Log On") %>
</p>
</fieldset>
Now there is less chance of any html typos in the view and we no longer have magic strings for the ids of the input fields. The MvcContrib.FluentHtml assembly can generate the ids from the property names in the view model for you automatically.
This means you no longer have to search for strings in views if you change the name of a property.
If you’re at all worried about the html generated by the MvcContrib.FluentHtml assembly don’t be, it’s as good if you had written it yourself.
For more information about the MvcContrib.FluentHtml assembly check out blog posts below:
- MvcFluentHtml – Fluent HTML Interface For MS MVC
- Editing a Variable Length List Of Items With MvcContrib.FluentHtml – Take 2
- Eliminate Magic Strings In Javascript With MvcContrib.FluentHtml
What about ASP.NET MVC 2?
In ASP.NET MVC 2 the DisplayFor html helper can be used to create the html for the model like this
<%= Html.DisplayFor(m => m.Password) %>
What it can’t do (at the time of writing) is any chaining for things like labels e.g.
<%=this.TextBox(m => m.Password).Label("Password")%>
Jag Reehal’s Final Thought on ‘ASP.NET MVC View Best Practices – Save time creating views with MvcContrib.FluentHtml’
in this post I wanted to demonstrate the why at this moment in time I use the MvcContrib.FluentHtml assembly for creating html in my views.
It will definitely save you time and you will be more productive because you’re no longer going to write as much html plumbing code.
- Tracy Phillips
- Chester