Creating a Login form with Umbraco MVC SurfaceController

I really like the new support for MVC in Umbraco. It's certainly great to be able to use the standard Razor Layout system instead of old Masterpages. And together with the new strongly typed API, I think it's now quicker and nicer than ever to build the front end of an Umbraco site.

Some time ago I built a RazorLogin package for Umbraco, using "WebPages style" = "throw logic and view into the same file". It does still work in Umbraco 4.10. But, as Mr. Sebastiaan Janssen rightfully commented in a twitter dialogue about RazorLogin:

"Would recommend a REAL (Surface)Controller in 4.11 though. Views shouldn't really have logic."

Ok, I don't want to promote bad habits (well, not too often anyway), so here we go:

Note that some of the code is abbreviated to keep the article a bit shorter, see the Gist link below for the complete source code.

The Model

In MVC we use a Model to describe our data, a model is simply a class. For the login form we need strings for username and password and a bool for a "remember me" checkbox.

public class MemberLoginModel
{
    public string Username { get; set; }
    public string Password { get; set; }
    public bool RememberMe { get; set; }
}

The Controller

The C in MVC is as we know the Controller. It deals with user actions which returns a result, normally a view or a redirection. In our case we write one function to handle the initial get request, one to handle logout and one to handle a login form post from the user.

To make it work as a surface controller we simply inherit the class from SurfaceController:

public class MemberLoginSurfaceController : Umbraco.Web.Mvc.SurfaceController
{

    // The MemberLogin Action returns the view, which we will create later. It also instantiates a new, empty model for our view:

    [HttpGet]
    [ActionName("MemberLogin")]
    public ActionResult MemberLoginGet()
    {
        return PartialView("MemberLogin", new MemberLoginModel());
    }

    // The MemberLogout Action signs out the user and redirects to the site home page:

    [HttpGet]
    public ActionResult MemberLogout()
    {
        Session.Clear();
        FormsAuthentication.SignOut();
        return Redirect("/");
    }

    // The MemberLoginPost Action checks the entered credentials using the standard Asp Net membership provider and redirects the user to the same page. Either as logged in, or with a message set in the TempData dictionary:

    [HttpPost]
    [ActionName("MemberLogin")]
    public ActionResult MemberLoginPost(MemberLoginModel model)
    {
        if (Membership.ValidateUser(model.Username, model.Password))
        {
            FormsAuthentication.SetAuthCookie(model.Username, model.RememberMe);
            return RedirectToCurrentUmbracoPage();

        }
        else
        {
            TempData["Status"] = "Invalid username or password";
            return RedirectToCurrentUmbracoPage();
        }
    }
}

TempData is an UmbracoMVC thing that can be used to add data to a view without adding it to the view model. In standard MVC we're used to ViewData (or the dynamic ViewBag). (In standard MVC we would probably return the ModelState and the model to the view on failed login with a message. But I could not get that to work in Umbraco currently. See also below. TempData works nice though.)

The View

The last letter in our acronym is V for View. It is a Razor file, and as opposed to our class files, it is saved directly in the web site. In the /Views/Partials folder with the name MemberLogin.cshtml:

@model UmbracoLogin.MemberLoginModel
@if (User.Identity.IsAuthenticated)
{
    <p>Logged in: @User.Identity.Name</p>
    <p>@Html.ActionLink("Log out", "MemberLogout", "MemberLoginSurface")</p>
}
else
{
    using (Html.BeginUmbracoForm("MemberLogin", "MemberLoginSurface"))
    {
    @Html.EditorFor(x => Model)
    <input type="submit" />
    }
    
    <p>@TempData["Status"]</p>
}   

By defining a strongly typed model we get nice intellisense when we create our view.

We're having the form created for us automatically with the BeginUmbracoForm method together with the EditorFor MVC method. The latter adds a label and a suitable input field for each property on the model.

Placing the login form in an actual template

The final piece to the puzzle is to render our HTML in the right place in a suitable template. To do that, just add this line of code:

@Html.Action("MemberLogin","MemberLoginSurface")

And there you go - a working login form using MVC with SurfaceController!

Spice it up with DataAnnotations

It's really easy to add custom label texts, and validation just by adding some attributes to our model. Let's check that out. First add a reference to System.ComponentModel.DataAnnotations. Then add some data annotations to the model:

public class MemberLoginModel
{
    [Required, Display(Name = "Enter your user name")]
    public string Username { get; set; }

    [Required, Display(Name = "Password"), DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "Remember me")]
    public bool RememberMe { get; set; }

}

With these in place the password box will be rendered as an HTML password input, and the labels will be a bit nicer than simply the propertynames.

Add some validation

The DataAnnotations attributes also adds validation rules, such as "Required". To let MVC check those for us we need to add one line in our controller:

if (ModelState.IsValid)
{
    if (...

before the actual password validation. Unfortunately, like I mentioned, I could not make the ModelState work with the SurfaceController. Otherwise we would get validation messages back to the form automatically.

The good news is that client side validation works as expected by using default MVC unobtrusive validation (with jQuery validate).

Automatic client side validation

Add three JavaScript libraries to your layout:

<script src="/scripts/jquery-1.8.2.js"></script>
<script src="/scripts/jquery.validate.js"></script>
<script src="/scripts/jquery.validate.unobtrusive.js"></script>

Then enable client side validation just by adding these two lines in your web.config <appSettings>:

<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>

Thats it! With these in place the user will get nice messages if he/she forgot to enter text in either of the fields.

Customize your form

If you like to define your form manually it is also easily possible - just the standard MVC behaviour. You can use either or combine:

  • write the raw HTML (use the same names as the model properties):

<input type="text" name="Username"/>

  • define each tag individually using Html helpers: 

@Html.LabelFor(model => model.UserName, "Friendly Name")
@Html.TextBoxFor(model => model.UserName, new { style = "width: 500px;" })

  • create custom EditorTemplates (see references below).

 

Happy holidays everyone!

 

 

The full source for the login control :

https://gist.github.com/4336141 

References and further reeding :

About MVC in Umbraco 4.10+

http://umbraco.com/follow-us/blog-archive/2012/10/30/getting-started-with-mvc-in-umbraco-410.aspx

http://our.umbraco.org/documentation/Reference/Mvc/forms

http://our.umbraco.org/forum/developers/api-questions/36614-411-Using-SurfaceController-Child-Action-with-Post

About DataAnnotations

http://weblogs.asp.net/srkirkland/archive/2011/02/23/introducing-data-annotations-extensions.aspx

Custom EditorTemplates in MVC

http://coding-in.net/asp-net-mvc-3-how-to-use-editortemplates/

 

Jonas Eriksson

Jonas is on Twitter as