ASP.NET vNext Identity Part 6 – Refectoring for Areas

Over the past 5 days I’ve implemented a fairly reasonable account login system that allows for logging in and out, handling registrations and handling forgotten passwords. I do have a major problem architecturally with this codebase though. My views, view models and controller for this logic is intermingled with my main application code. It’s not really a problem within this application since the actual application isn’t doing anything, but it can be a major pain in bigger applications. A similar case could be made for management and configuration code vs. the main application. I’d rather have the views, controller and view models all in their own separate area.

ASP.NET MVC has had a concept called Areas for this in the past. It’s configured a little bit differently than in the old ASP.NET but is the same concept. I get to put the pieces that make up the view and controller system in its own separate area.

This article is about doing this for the account login system I’ve been working on for the last five days. Explicitly, I want:

  1. A login and logout controller that just do that
  2. A registration controller that just does that
  3. A forgotten password controller that just does that

This way each of my workflows are separated from the other workflows with their own views and controllers. Let’s get started.

Configuring Areas

To configure the application to handle areas, I need to change the route mapping in Startup.cs. This is the new code:

            // Configure ASP.NET MVC6
            app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "areaDefault",
                        template: "{area:exists}/{controller}/{action=Index}/{id?}");

                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}"
                    );
                });

The default route was already there from my prior work. The areaDefault is new. You can see that it isn’t much different. It just has that {area:exists} in the front. If I create an area called Account, then a controller called Login and an action method called Index, it can be accessed via /Account/Login/Index or /Account/Login since Index is the default action. Note that the ASP.NET Identity Framework re-directs to /Account/Login when authentication is needed, so this seems like a perfect solution.

Creating an Area

All my areas live in a new folder called Areas. My new area is called Account. Within the Account area are folders for Controllers, ViewModels and Views. Once I’ve done this, my folder layout looks like the below:

blog-code-0407-1

Creating the Login Controller and Views

Since I am splitting my AccountController into three pieces, I will want three controllers. I am only showing you one of the three controllers in this article. You can refactor the AccountController to do the other two yourself or look at the resulting code at the end on my GitHub Repository.

The LoginController.cs file lives in Areas\Account\Controllers and looks like this:

using System.Threading.Tasks;
using AspNetIdentity.Areas.Account.ViewModels;
using AspNetIdentity.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Mvc;

namespace AspNetIdentity.Areas.Account.Controllers
{
    [Area("Account")]
    public class LoginController : Controller
    {
        public LoginController(
            UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager)
        {
            UserManager = userManager;
            SignInManager = signInManager;
        }

        public UserManager<ApplicationUser> UserManager
        {
            get;
            private set;
        }

        public SignInManager<ApplicationUser> SignInManager
        {
            get;
            private set;
        }

        // GET: /Account/Login/Index
        [HttpGet]
        [AllowAnonymous]
        public IActionResult Index()
        {
            return View();
        }

        // POST: /Account/Login/Index
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Index(LoginViewModel model, string returnUrl = null)
        {
            ViewBag.ReturnUrl = returnUrl;
            if (ModelState.IsValid)
            {
                var result = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false);
                if (result.Succeeded)
                {
                    if (Url.IsLocalUrl(returnUrl))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index", "Home");
                    }
                }
                if (result.IsLockedOut)
                {
                    ModelState.AddModelError("", "Locked Out");
                }
                else if (result.IsNotAllowed)
                {
                    ModelState.AddModelError("", "Not Allowed");
                }
                else if (result.RequiresTwoFactor)
                {
                    ModelState.AddModelError("", "Requires Two-Factor Authentication");
                }
                else
                {
                    ModelState.AddModelError("", "Invalid username or password.");
                }
                return View(model);
            }

            // If we got this far, something failed - redisplay the form
            return View(model);
        }

        // (GET|POST): /Account/Login/Logout
        public IActionResult Logout()
        {
            SignInManager.SignOut();
            return RedirectToAction("Index", "Home");
        }

    }
}

You might be saying “Wait a minute – that’s all the same code as before!” You would be right. There are a couple of differences. Firstly, I’ve added an [Area("Account")] decorator to the class. This indicates to the MVC framework that this is an area and needs to go through the routing that we’ve just established. I’ve also renamed the methods to Index so this can be accessed through /Account/Login. This does have a ViewModel called LoginViewModel that is needed. This is placed in the Areas/Account/ViewModels directory and looks exactly the same:

using System.ComponentModel.DataAnnotations;

namespace AspNetIdentity.Areas.Account.ViewModels
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }

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

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

The only thing that has changed here is the namespace – it’s now in the Areas namespace rather than the top level namespace.

As I move along this process I am removing the code from the AccountController and the classes I no longer need from their locations. This prevents confusion as to where I am in the refactoring process.

For the view, I’ve created a Login folder underneath Areas\Account\Views since I’m working with the LoginController. I’ve copied over the Views\Layouts\LoginPage.cshtml and placed it in Areas\Account\Views\Layout.cshtml I’ve also created a ViewStart.cshtml file. I did some cosmetic moving around and updating of the paths. Here is the _ViewStart.cshtml file:

@{ Layout = "~/Areas/Account/Views/Layout.cshtml"; }

Note that each area can have it’s own _ViewStart.cshtml file, so it can have its own dedicated layout. In the prior 5 articles, I had to specify the “login code layout” everywhere. Now I don’t have to do that – I can specify it centrally.

And here is the Layout.cshtml file:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="~/lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
    <link rel="stylesheet" href="~/Style/Login.css">
    <title>@ViewBag.Title</title>
</head>
<body>
  <div class="flex-container">
    <div class="login-outer-form">
      @RenderBody()
    </div>
  </div>
  <section id="scripts" style="visibility:hidden;">
    @RenderSection("scripts", required: false)
  </section>
</body>
</html>

The only real difference here is cosmetic. Every single one of my views was wrapped inside of the flex-container and login-outer-form. Rather than put them inside of each view, I’ve promoted that to the layout. The Index.cshtml file looks almost like a copy of the old Login.cshtml; like this:

@model AspNetIdentity.Areas.Account.ViewModels.LoginViewModel
@{ ViewBag.Title = "Log In"; }

<div class="login-type-selector">
    <h4 id="localLoginSelector" class="active">Local</h4>
    <h4 id="socialLoginSelector">Social</h4>
</div>
<div class="login-form-area">
    <div id="localLogin">
        @using (Html.BeginForm("Index", "Login", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { id = "login-form", area = "Account" }))
        {
            @Html.AntiForgeryToken()
            @Html.ValidationSummary(true)
            <div class="form-group">
                <div class="control-label"><span class="fa fa-envelope"></span></div>
                <div class="form-control">
                    @Html.TextBoxFor(m => m.UserName)
                    @Html.ValidationMessageFor(m => m.UserName)
                </div>
            </div>
            <div class="form-group">
                <div class="control-label"><span class="fa fa-key"></span></div>
                <div class="form-control">
                    @Html.PasswordFor(m => m.Password)
                    @Html.ValidationMessageFor(m => m.Password)
                </div>
            </div>
            <div class="form-group">
                <div class="checkbox">
                    @Html.CheckBoxFor(m => m.RememberMe)
                    @Html.LabelFor(m => m.RememberMe)
                </div>
            </div>
            <div class="submit-btn" id="login-btn">Login</div>
        }
        <div class="login-form-options">
            <div class="register-link">
                <a href="/Account/Register">Register<br>Account</a>
            </div>
            <div class="forgot-link">
                <a href="/Account/Forgot">Forgot<br>Password</a>
            </div>
        </div>
    </div>

    <div id="socialLogin">
        <h4>COMING SOON!</h4>
    </div>
</div>


@section scripts
{
    <script src="~/lib/jquery/dist/jquery.min.js"></script>

    <script>
        $(document).ready(function () {

            function activate(on) {
                var off = (on === "local") ? "social" : "local",
                    onSelector = $("#" + on + "LoginSelector"),
                    offSelector = $("#" + off + "LoginSelector"),
                    onBlock = $("#" + on + "Login"),
                    offBlock = $("#" + off + "Login");

                offSelector.removeClass("active");
                onSelector.addClass("active");
                offBlock.hide();
                onBlock.show();
            }

            activate("local");
            $("#socialLoginSelector").click(function () { activate("social"); });
            $("#localLoginSelector").click(function () { activate("local"); });
            $("#login-btn").click(function () { $("#login-form").submit(); });
        });
    </script>
}

The changes here are minor. I’ve removed the flex-container and login-form-outer wrapping. I’ve changed the namespace of the ViewModel at the top so that it points to the new location. I’ve removed the Layout as it is now handled for me. The important change is in the Html.BeginForm I’ve isolated that change below:

@using (Html.BeginForm("Index", "Login", 
    new { ReturnUrl = ViewBag.ReturnUrl }, 
    FormMethod.Post, 
    new { id = "login-form", area = "Account" }))

Firstly, the first two parameters have been reflected to the new controller and action names. Secondly, the area has been specified in the options area.

I also had to do a small change as a result of the HTML movement between view and layout. Specifically, there are two rules wrapped in a #login – I removed the #login wrapper.

Change your other (dependent) views

In the Views\Layout\MainSite.cshtml, there is a link to log out. I’ve changed that link to /Account/Login/Logout since it’s in an area now. I need to make a similar change to the Home view for it to work. In my case, I converted the form to an ActionLink – it’s in the middle of the new MainSite.cshtml file:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-theme.min.css">
    <link rel="stylesheet" href="~/Style/StyleSheet.css">
    <title>@ViewBag.Title</title>

    <!-- Polyfills -->
    <script src="~/lib/webcomponentsjs/webcomponents.min.js"></script>
</head>
<body>
    <div class="container">
        <div class="navbar-header">
            <ul class="nav navbar-nav navbar-right">
                <li>@Html.ActionLink("Log Out", "Logout", "Login", new { area = "Account" })</li>
            </ul>
        </div>
    </div>
    <section id="body">
        @RenderBody()
    </section>
    <section id="scripts">
        <script src="~/lib/requirejs/require.js" data-main="Scripts/application"></script>
    </section>
</body>
</html>

Wrapping Up

I love Areas. They assist in code organization and keep the views, view models and controllers for associated content together, rather than spread across the project. I highly recommend their use.

Check out the code after I’ve done all the re-factoring at my GitHub Repository.