Introduction
This article shows how to use the Modal Dialog jQuery plugin of the Twitter Boostrap framework to create a smoother user login experience. This solution can be easily included into an ASP.NET MVC project created by a default Visual Studio 2013 ASP.NET MVC project template. Instead of redirecting to the login page when clicking the "Login" navigation item, mouse clicking on that link opens the login form in a modal dialog. Using Twitter Boostrap jQuery modal dialog plugin allows this dialog to be well integrated into the overall Boostrap design of the web site created by the ASP.NET MVC project template. This solution leads to a smoother user login experience. When the user has JavaScript disabled, the user is redirected to the login page and the application behaves normally.
Contributions
This project has arisen with contributions by:
- Implementing Ajax Login in ASP.NET MVC
- Forloop HtmlHelpers HtmlHelpers that help Partial Views to add script blocks or references to script files to the right place. Available as a NuGet package to install in your web project. Refer to the documentation how to add and use it in your project.
Background
The Visual Studio ASP.NET MVC project template comes with a login solution that redirects the user to a separate Login view when clicking on "Login" navigation item. Web users today expect a more smooth experience which is most realized by DHTML, AJAX and JavaScript. Many web sites realize the login experience as a small popup which opens in front of the calling page and redirects the user to the target page when the user successfully logged in.
This was the motivation of this article, to provide a smoother login experience, using the default functionality of ASP.NET MVC project template as much as possible and conform to default ASP.NET MVC design which is based on Twitter Bootstrap. Documentation about Twitter Bootstrap can be found on getbootstrap.com. Documentation about the Bootstrap Modal plugin can be found here.
The Modal dialog does not submit the login form as long as the user has not successfully logged in. The login itself is initiated via an AJAX call to an additional Action
method on Account controller and not via form submit. The result of success and failure is handled on client side and not as per default server side. Because the modal login is invisible when the page opens, the modal login form is closed when the page hosting the modal login form submits.
Using the Code
The code is included in the sample project. This project is based on the default Visual Studio 2013 ASP.NET MVC project template.
Preparation
Maybe you already have an MVC project and you want to try adding the modal login to your existing project. Alternatively, you can create a new ASP.NET MVC Website in Visual Studio and add the necessary code to this project. You can use the sample project as a starting point for your own web project. It is almost identical to the default project template of Visual Studio. As mentioned, the default ASP.NET MVC project template contains Twitter Bootstrap resources (script and styles) so you can start immediately with the steps below after you created an ASP.NET MVC project.
Before beginning to write or copy the code to your web project, it is recommended that you install the Forloop.HtmlHelpers
from NuGet to your project. This little helper aims you to add script or script references to the end of e.g. the <body>
tag. After installing the helpers, you need to add the following two lines of code to the existing code files: App_Start/BundleConfig.cs
Add the following line of code after all bundle configuration:
ScriptContext.ScriptPathResolver = System.Web.Optimization.Scripts.Render;
Views/Shared/_Layout.cshtml
Add a call to the Forloop HtmlHelper
method RenderScripts()
:
...
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@Scripts.Render("~/bundles/jqueryval")
@RenderSection("scripts", required: false)
@Html.RenderScripts()
</body>
</html>
Render the jQuery validation script references. The script bundle for that is already created in BundleConfig.cs. So, you need only a command to render a reference of that bundle to Views/Shared/_Layout.cshtml. This ensures client side validation and avoid submitting the form to validate the login form. Adding it to the layout view makes it possible to remove it from views based on this layout.
...
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@Scripts.Render("~/bundles/jqueryval")
@RenderSection("scripts", required: false)
@Html.RenderScripts()
</body>
</html>
Create the Modal Login View
The Twitter Bootstrap Modal plugin is based on a jQuery plugin. It is already integrated into the default bootstrap.js script resource that is delivered by Bootstrap and is delivered by the default Visual Studio 2013 ASP.NET MVC project template so you don't need to add any additional resources.
The Bootstrap Modal dialog is built up by a specific HTML structure with prescribed class names as is well known when creating web sites based on Twitter Bootstrap. JavaScript code that made up the Modal dialog appearance and functionality uses this structure. To setup the login as a modal dialog, we need to do five things.
- Create an MVC layout view for modal dialogs based on Twitter Bootstrap
- Separate out login form from Login.cshtml into a partial view so we can use it in modal dialog and the login page
- Create modal login view based on partial login view and modal layout created in the first step
- Add reference of partial login view into Login.cshtml view
- Add the Modal Login form to default page layout view
Create a MVC layout for modal dialogs
Create a "MVC 5 Layout Page" in the Views/Shared folder of your Visual Studio Project. Name it e.g. _LayoutModal.cshtml. This layout view contains the following code:
@* Layout Page for Twitter Bootstrap Modal Dialogs *@
<div class='modal fade' id='@ViewBag.ModalId' tabindex='-1' role='dialog'
aria-labelledby='@ViewBag.ModalId@Html.Raw(' label')' aria-hidden='true'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class='modal-header'>
<button type='button' class='close' data-dismiss='modal'
aria-label='Close'><span aria-hidden='true'>×</span></button>
<h4 class='modal-title' id='@ViewBag.ModalId@Html.Raw(' label')'>
@ViewBag.ModalTitle</h4>
</div>
<div class='modal-body'>
@RenderBody()
</div>
<div class='modal-footer'>
<button type='button' class='btn btn-default' data-dismiss='modal'>Close</button>
</div>
</div>
</div>
</div>
Separate out login form code into a partial view
To use the login form in modal dialog and default login page, we create a _LoginPartialForm.cshtml partial view in the Views/Shared folder. Select option "Create as a partial view" option when creating the view.
Next, cut the login form Login.cshtml and paste it into the new partial view _LoginPartialForm.cshtml. I used the unedited default Razor markup in Login.cshtml. This looks like:
@using Sample.Web.ModalLogin.Models
@model LoginViewModel
<div class='row'>
<div class='col-md-8'>
<section id='loginForm'>
@using (Html.BeginForm('Login', 'Account', new { ReturnUrl = ViewBag.ReturnUrl },
FormMethod.Post, new { @class = 'form-horizontal', role = 'form' }))
{
@Html.AntiForgeryToken()
<h4>Use a local account to log in.</h4>
<hr />
<div class='alert alert-danger' role='alert' style='display: none' id='alertBox'></div>
@Html.ValidationSummary(true, '', new { @class = 'text-danger' })
<div class='form-group'>
...
}
</section>
</div>
</div>
We added a Bootstrap Alert
element to show login errors and server side exceptions. We edited some CSS classes of the Bootstrap Grid so it looks better in the modal dialog.
Create modal login view
The next step is to create a view using the _LayoutModal.cshtml created in the first step and including the partial view created in step two.
For that, create a view under Views/Shared and name it e.g., LoginModal.cshtml. When creating the view, select option "Use a layout page" and select the _LayoutModal.cshtml layout view. Add partial _LoginPartialForm.cshtml into the view. Add the reference to the partial login form to the view. @Html.Partial("_LoginPartialForm")
e.g. at the end of the view.
The following code snippet shows the content of LoginModal.cshtml. It renders the _LoginPartialForm.cshtml login form as the body. There are two ViewBag
properties to set: the id
attribute of the modal dialog and a title
label of the modal dialog. This is to distinguish if more than one modal dialog is on the page. The lines of code at the beginning of the view ensures that the JavaScript file containing the client functionality of our modal login is placed at the end of the <body>
node. This is accomplished by the Forloop HtmlHelpers
library described ahead. The LoginModal.js file itself will be described below.
@using Forloop.HtmlHelpers
@using (Html.BeginScriptContext()){
Html.AddScriptFile("~/Scripts/app/views/LoginModal.js");
}
@{
Layout = "~/Views/Shared/_LayoutModal.cshtml";
ViewBag.ModalTitle = "Login";
ViewBag.ModalId = "ModalLogin";
}
@Html.Partial("_LoginPartialForm")
Add reference of partial login view into Login.cshtml
view
As with the LoginModal.cshtml view, you need to add a command to render the partial view _LoginPartialForm.cshtml in Login.cshtml by adding @Html.Partial("_LoginPartialForm")
to Login.cshtml.
Add the Modal Login form to default page layout
To open the modal login dialog, we need to add it to the _Layout.cshtml layout view.
<body>
@if (!Request.IsAuthenticated)
{
@Html.Partial("LoginModal")
}
...
The modal dialog needs to be topmost of the DOM tree beneath the <body>
tag. Of course, only when not authenticated, the modal login form and its script resources need to be rendered.
Bring the Modal Login Form to Work
After creating the modal login form itself, we need to edit the "Login" navigation item so that it displays the modal login form when JavaScript is enabled instead of redirecting to the default Login view. This is accomplished by manipulating the DOM of the "Login" navigation link via JavaScript when the page is loaded. The following code snippet can be found in the LoginModal.js file described below. The link is only edited when JavaScript is enabled. Otherwise, nothing happens and the "Login" link behaves as default and redirects the user to the login page.
$(document).ready(function () {
var loginLink = $("a[id='loginLink']");
loginLink.attr({ "href": "#", "data-toggle": "modal", "data-target": "#ModalLogin" })
});
Add login functionality to modal login form
As mentioned earlier, we must not submit the modal login form when logging in because in this case the modal login will be closed even if the login was not successful. A better user experience is to keep the login dialog open until the user has successfully logged in. Therefore, we handle login via AJAX call to a new Controller
method that returns a JsonResult
instead of an ActionResult
. This method is called via AJAX request when the user submits the form by clicking on "Login" button.
The following code shows this Action
method.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<jsonresult> LoginJson(string username, string password, bool rememberme)
{
var result = await SignInManager.PasswordSignInAsync
(username, password, rememberme, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return Json(true);
case SignInStatus.LockedOut:
case SignInStatus.RequiresVerification:
case SignInStatus.Failure:
return Json(false);
default:
break;
}
return Json(false);
}
This sample uses the simplest possible approach. It uses sign in without additional verification and returns simply true
when the user logged in successfully and false
when the login failed.
Now, we have to work on the client side part. For better separation of concerns (separate function from view specific code), we could split the functionality into two JavaScript files. For this simple example, I added the code in one file. The following shows the client side scripts which you can find in the file Scripts/app/LoginModal.js.
The first part contains view related functionality. We handle the submit event of the form, call the new Action
method LoginJson
on Account
controller via AJAX call and surpress the submit of the form. When successfully logged in, the user is redirected to the same page. When an error occurred or the login failed, e.g., - because wrong credentials are provided - an error is displayed but the form remains open.
Together with the credentials provided by the user, we have to post the anti forgery token (against CSRF attack) to the Action
method. In ASP.NET MVC views, this token value can be found in a hidden input field named __RequestVerificationToken
.
We handle the two Modal
events hidden.bs.modal
and shown.bs.modal
to reset the form when the user closes it and to focus the first input field when it is opened.
$(document).ready(function () {
var loginLink = $("a[id='loginLink']");
loginLink.attr({ "href": "#", "data-toggle": "modal", "data-target": "#ModalLogin" });
$("#loginform").submit(function (event) {
if ($("#loginform").valid()) {
var username = $("#Email").val();
var password = $("#Password").val();
var rememberme = $("#RememberMe").val();
var antiForgeryToken = Sample.Web.ModalLogin.Views.Common.getAntiForgeryValue();
Sample.Web.ModalLogin.Identity.LoginIntoStd
(username, password, rememberme, antiForgeryToken,
Sample.Web.ModalLogin.Views.LoginModal.loginSuccess,
Sample.Web.ModalLogin.Views.LoginModal.loginFailure);
}
return false;
});
$("#ModalLogin").on("hidden.bs.modal", function (e) {
Sample.Web.ModalLogin.Views.LoginModal.resetLoginForm();
});
$("#ModalLogin").on("shown.bs.modal", function (e) {
$("#Email").focus();
});
});
var Sample = Sample || {};
Sample.Web = Sample.Web || {};
Sample.Web.ModalLogin = Sample.Web.ModalLogin || {};
Sample.Web.ModalLogin.Views = Sample.Web.ModalLogin.Views || {}
Sample.Web.ModalLogin.Views.Common = {
getAntiForgeryValue: function () {
return $('input[name="__RequestVerificationToken"]').val();
}
}
Sample.Web.ModalLogin.Views.LoginModal = {
resetLoginForm: function () {
$("#loginform").get(0).reset();
$("#alertBox").css("display", "none");
},
loginFailure: function (message) {
var alertBox = $("#alertBox");
alertBox.html(message);
alertBox.css("display", "block");
},
loginSuccess: function () {
window.location.href = window.location.href;
}
}
Sample.Web.ModalLogin.Identity = {
LoginIntoStd: function (username, password, rememberme, antiForgeryToken,
successCallback, failureCallback) {
var data = { "__RequestVerificationToken": antiForgeryToken,
"username": username, "password": password, "rememberme": rememberme };
$.ajax({
url: "/Account/LoginJson",
type: "POST",
data: data
})
.done(function (loginSuccessful) {
if (loginSuccessful) {
successCallback();
}
else {
failureCallback("Invalid login attempt.");
}
})
.fail(function (jqxhr, textStatus, errorThrown) {
failureCallback(errorThrown);
});
}
}
Points of Interest
With the means provided by the Visual Studio 2013 project template for ASP.NET MVC applications, developers can enhance the login experience with the use of the integrated Twitter Bootstrap Modal dialog plugin.
History
- 16 May, 2015: Version 1.0
- 12 June, 2015: Version 1.1
- Sample Project contains referenced assemblies now. No reinstall via Package Manager Console needed anymore.