Having fitted the theme generated for the Contoso Karate Web Sites (see the previous posts in this series) it’s time to start adding functionality to the template.

If you’re new to ASP.NET MVC, let me take this opportunity to encourage you to stick with it. Much is different and sometimes this makes things especially frustrating because not only do we have to learn new things, but the new things work quite differently than we expect based on our previous experience.

Be tenacious, it’s worth it.

Here is our first feature scenario. My Contoso Karate site will have a “portal” feel to it. The default ASP.NET MVC Internet Application template has a separate page for the user to use to authenticate (Log In) but I want the user to be able to log in from any page in my website.

It’s very common for web sites to have a “Log In” widget in the side bar of their theme, like this:

I want the user to be able to Log In by entering their User Name and Password right here on the Home page, or any page in the site that uses the default page template.

To do this in a functionally complete way I need to provide for several behavioral considerations.

  • The user does not yet have an account on the site and needs to create one.
  • The user submits the form without entering the required User Name and Password.
  • The user fills out the form but the user name and password to not match any in the authentication database.
  • The user entered a valid user name and password and is authenticated when the user clicks the submit button.

In the last case, it doesn’t make sense to continue to show a Log-In feature after the user successfully logs in so we should conditionally display a Log-Out option is the current user is authenticated.

ASP.NET WebForms developers have traditionally been bound by the restriction of a single HTML form to each .ASPX page. (Though technically there are ways around this issue and the server-side execution model for user interface element events made this more or less irrelevant.)

While ASP.NET WebForms abstracts the “nature” of World Wide Web applications, ASP.NET MVC embraces it.

An ASP.NET MVC View can contain any number of html forms and each form’s action attribute can submit to any controller and action (and http method) that we need.

So to create the Log-In feature in the right column of our template (_Layout.cshtml) we will insert an html form object. We will do this using the @Html helper object.


using (Html.BeginForm('LogOn', 'Account', FormMethod.Post))
{
   <div class='editor-label'>
      @Html.LabelFor(m => m.UserName)
   </div>
   <div class='editor-field'>
      @Html.TextBoxFor(m => m.UserName)
      @Html.ValidationMessageFor(m => m.UserName)
   </div>
   <div class='editor-label'>
      @Html.LabelFor(m => m.Password)
   </div>
   <div class='editor-field'>
      @Html.PasswordFor(m => m.Password)
      @Html.ValidationMessageFor(m => m.Password)
   </div>
   <div class='editor-label'>
      @Html.CheckBoxFor(m => m.RememberMe)
      @Html.LabelFor(m => m.RememberMe)
   </div>   
   <span class='style-button-wrapper'>
      <span class='style-button-l'> </span>
      <span class='style-button-r'> </span>
      <input type='submit' name='Submit' class='style-button' 
         value='Login' />
   </span>
   <hr />  
   <ul>
      <li>
    @Html.ActionLink('Create an Account.', 'Register', 'Account')
      </li>
   </ul>
}
         

Notice that the form labels and fields are referencing a data model (m.UserName). The model is the LogonModel that we get by virtue of starting our project from the default ASP.NET MVC Internet template.

We will need to include the model at the top of our template page.


@model ContosoKarateMVC.Models.LogOnModel

Note the ValidationMessageFor entries for the Username and Password fields.

Note also the little bit of ASP.NET MVC magic in the form declaration.


Html.BeginForm('LogOn', 'Account', FormMethod.Post)

This says that the form will be submitted to the LogOn action of the Account controller using the HTTP POST verb.

Let’s explore the LogOn Action source code.



[HttpPost]

public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
	if (Membership.ValidateUser(model.UserName, model.Password))
	{
		FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
		if (Url.IsLocalUrl(returnUrl) && returnUrl.Length &gt; 1 
									  && returnUrl.StartsWith('/')
			&& !returnUrl.StartsWith('//') && !returnUrl.StartsWith('/\\'))
		{
			return Redirect(returnUrl);
		}
		else
		{
			return RedirectToAction('Index', 'Home');
		}
	}
	else
	{
		ModelState.AddModelError('', 'The user name or password 
									  provided is incorrect.');
	}
}

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

Note the [HttpPost] attribute.

This bit of code is only executed if the HTTP verb is POST. If te HTTP verb is GET then a different execution thread is followed.

Early in the code we test to see if the model is valid (see line #4)

If the model fails validation, in this case meaning a required field (User Name or Password) was not provided we fall to line 22 where we add a model error and then we fall to line 28 which results in the default LogIn View being displayed. (LogIn.cshtml)

The validation error will be displayed and the user will have the opportunity to correct the input errors.

The validation error that I identified as #1 is created by the following line of code in the LogOn.cshtml view file.


@Html.ValidationSummary(true, 'Login was unsuccessful. 
                               Please correct the errors and try again.')

The two validation messages that I have labeled #2 come from the ValidationMessages defined in the HTML form (see this first listing in this post).

As I said above, once the user has successfully logged in, I want to change the display and offer the user a Log-Out option. Not only does it not make sense to display a Log-In form after the user has successfully logged in but it’s confusing to the user. If the Log-In form is still presented the user is apt to think that their Log-In attempt failed.

In order to do this we will need to determine whether or not the current user has been authenticated and then conditionally display either a “Log-In” or a “Log-Out” option depending on that authenticated state.

Here is the complete form markup.


@if (!User.Identity.IsAuthenticated)
    {
        using (Html.BeginForm('LogOn', 'Account', FormMethod.Post))
        {
           <div class='editor-label'>
              @Html.LabelFor(m => m.UserName)
           </div>
           <div class='editor-field'>
              @Html.TextBoxFor(m => m.UserName)
              @Html.ValidationMessageFor(m => m.UserName)
           </div>
           <div class='editor-label'>
              @Html.LabelFor(m => m.Password)
           </div>
           <div class='editor-field'>
              @Html.PasswordFor(m => m.Password)
              @Html.ValidationMessageFor(m => m.Password)
           </div>
           <div class='editor-label'>
              @Html.CheckBoxFor(m => m.RememberMe)
              @Html.LabelFor(m => m.RememberMe)
           </div>   
           <span class='style-button-wrapper'>
              <span class='style-button-l'> </span>
              <span class='style-button-r'> </span>
              <input type='submit' name='Submit' class='style-button' 
			                       value='Login' />
           </span>
           <hr />  
           <ul>
              <li>
			  @Html.ActionLink('Create an Account.', 'Register', 'Account')
			  </li>
           </ul>
        }
    }
else
    {
        using (Html.BeginForm('LogOff', 'Account', FormMethod.Post))
        { 
           <span class='style-button-wrapper'>
              <span class='style-button-l'> </span>
              <span class='style-button-r'> </span>
              <input type='submit' name='Submit' class='style-button' 
			         value='Logoff' />
           </span>                                              
        }
    }                                    

Our authentication state test happens at line #1.

I think the rest of the form implementation is pretty straight forward but let’s revisit the LogOn action code to understand why we get the desired behavior.



[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (Membership.ValidateUser(model.UserName, model.Password))
        {
            FormsAuthentication.SetAuthCookie(model.UserName, 
			                                  model.RememberMe);
            if (Url.IsLocalUrl(returnUrl) && returnUrl.Length &gt; 1 
                                          && returnUrl.StartsWith('/')
                && !returnUrl.StartsWith('//') && 
				   !returnUrl.StartsWith('/\\'))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction('Index', 'Home');
            }
        }
        else
        {
            ModelState.AddModelError('', 'The user name or password 
                                          provided is incorrect.');
        }
    }

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

Note line #6. If the form was valid, we get to line #6 and we try to validate the user with the User Name and Password that the user supplied.

If the credentials fail, we fall to the “else” statement and add a validation error then fall through to return the default view for the LogOn action.

If, however, the credentials are valid, we set an Authorization cookie and then check to see if our validation request came from a valid url inside our application.

If not we simple return the default View for the Home Controller.

In our case though, we validated from our web site’s default page so the user will be returned to that view, but the view will now render differently (with the LogOut option displayed instead of the LogIn option) because we set the Authentication cookie.

If the user does not yet have an account for my application they can create on by clicking on the link created by the following code:


<li>@Html.ActionLink('Create an Account.', 'Register', 'Account')</li>                    

Of course this doesn’t appear if the user is logged in and I’m displaying this as an unordered list because I intend to add additional links in the future, like password retrieval.


You can download the code with this feature implemented [ HERE ].