tags:

views:

1801

answers:

5

Hi, I have a login box in my MasterPage. Whenever the login information is not correct, I valorize ViewData["loginError"] to show the error message to the user.

Login is an action of the UserController, so the form that contains the login has action = "/User/Login".

As a user can try to log in from any page, in case of success I redirect him to his personal page, but in case of error I want him to stay on the very same page where he tried to login. I've found that this works:

return Redirect(Request.UrlReferrer.ToString());

but it seems that, as I'm not returning a proper view, the data on ViewData is lost, so I cannot show the error message.

Any suggestion on how to solve this and similar problems?

Thanks

A: 

Why not handle the login via AJAX instead a full post? You could easily supply the status, a redirect URL, and any error messages via JSON.

public ActionResult Logon( string username, string password )
{
     ...

     // Handle master page login
     if (Request.IsAjaxRequest())
     {
          if (success)
          {
              return Json( new { Status = true, Url = Url.Action( "Index", "Home" ) } );
          }
          else
          {
              return Json( new { Status = false, Message = ... } );
          }
     }
     else // handle login page logon or no javascript
     {
          if (success)
          {
              return RedirectToAction( "Index", "Home" );
          }
          else
          {
              ViewData["error"] = ...
              return View("Logon");
          }
      }
  }

Client-side

  $(function() {
      $('#loginForm input[type=submit]').click( function() {
          $('#loginError').html('');
          $.ajax({
             url: '<%= Url.Action("Logon","Account") %>',
             dataType: 'json',
             type: 'post',
             data: function() { return $('#loginForm').serialize(); },
             success: function(data,status) {
                 if (data.Status) {
                     location.href = data.Url;
                 }
                 else {
                     $('#loginError').html( data.Message );
                 }
             }
          });
          return false;
      });
  });
tvanfosson
Hi,I know I can handle this via ajax and that's what I'm about to do, but I'd like to know how to solve the problem as is as there may be times when using a simple POST is more appropriate, or even simply required by a customer.
pistacchio
You could have the View that should be rendered on error set as a hidden parameter of the form being submitted. Unfortunately, you'd probably also need to serialize any model data contained in that view to pass along with the logon request since otherwise there's no way to persist it between requests. Doing it with AJAX is going to be a lot cleaner. Note that the code above will redirect to the logon page (not the original view, however) if javascript isn't available. That's how I would handle it -- drop the error into the logon page and let the user reauthenticate from there.
tvanfosson
A: 

Normally for most web sites, when user fail to authenticate (due to password or so), it will go to another page which help the user with (like password retrieve, or ask the user to sign up) which is rare to stay in the very same page. I think you can re-consider that you really need the navigation that you are using.

OK, one solution if you really want to stick to your model, is that you can attach the login error to the URL. For example, http://www.example.com/index.aspx?login_error=1 indicates that error occurs, and you can use BEGIN_REQUEST (or HTTP Module) to capture this, and tell the model state about the error:

ModelState.AddModelError(...);

BTW, add model error is actually a more proper way to inform the view about any error rather than using ViewState (this is similar to throwing exception vs returning an integer about the execution result in old days).

While using AJAX to login (as suggested by tvanfosson) is perfectly achievable and it sometimes excel in user experience, classic full-post is still inreplacable (consider some user will disable javascript, or even on my dump WM6 handset that doesn't support javascript).

xandy
While my code sample is not complete, I think the structure of it will handle the case where javascript is not available. In that case the logon form will do a full post and any errors get redirected back to the logon page as you describe.
tvanfosson
Agree on using both AJAX + form post if no JS, and for my projects I would use both as well. But pistacchio's problem still persist.
xandy
+1  A: 

I'm confused. Doesn't

return View();

just return the current page back to you?

So in your case, when login fails, set your viewdata and call return View();

i.e.

if (!FailedLogin) {
  //Go to success page
}else{
  //Add error to View Data or use ModelState to add error
  return View();
}

Are you using the [Authorize] decorator? The MVC login process auto prompts with the login page and then returns you to the controller action you were trying to execute. Cuts down on a lot of redirecting.

Rake36
+6  A: 

You probably want to use the TempData property, this will be persisted across to the next HTTP request.

roryf
+1 Just what I was thinking. There's also a set of handy action filters in MvcContrib that copies ModelState to and from TempState so you have the validation information available after the redirect.See item 13 in this article for an example http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx
Marnix van Valen
Remember TempData uses session state though - with all the implications.
UpTheCreek
A: 

The following example would hopefully help you out in resolving this issue:

View.aspx

<%= 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>
<% } %>

AccountController.cs

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 (!MembershipService.ValidateUser(userName, password)) { ModelState.AddModelError("_FORM", "The username or password provided is incorrect."); }

        return ModelState.IsValid;
    }

You won't be able capture the information added in ViewData after Redirect action. so the right approach is to return the same View() and use ModelState for errors as mentioned by "xandy" as well.

Hope this would give u a head start with form validation.

hunaid mushtaq