Search code examples
c#asp.net-mvc-5asp.net-mvc-routing

MVC Login Form - Resource not found


I am trying to implement my own login framework which originated from my ASP.NET projects and is now about to migrate to ASP.NET MVC.

The Problem

Whenever I click the submit button in CommunityBar.cshtml I am redirected to: localhost/Home/Login?Length=4 and receive a resource not found error but having spent the last few hours on this problem, together with our handy friend, google, I have yet to find a solution. So I was hoping somebody here might be able to help.

Next a bunch of code:

RouteConfig.cs

// POST home/login
routes.MapRoute(
    name: "Login",
    url: "Home/Login/{model}/{returnUrl}",
    defaults: new { controller = "Home", action = "Login" }
    );

CommunityBar.cshtml

@using (Html.BeginForm("Login", "Home", new {ReturnUrl = ViewBag.ReturnUrl}, FormMethod.Post, new {role = "form"}))
{
     @Html.AntiForgeryToken()
     @Html.ValidationSummary(true, "", new {@class = "danger"})
     @Html.TextBoxFor(m => m.LoginUsername, new {@class = "form-input", @placeholder = "Username"})
     @Html.ValidationMessageFor(m => m.LoginUsername, "", new {@class = "danger"})
     @Html.TextBoxFor(m => m.LoginPassword, new {@class = "form-input", @placeholder = "Password"})
     @Html.ValidationMessageFor(m => m.LoginPassword, "", new {@class = "danger"})
     @Html.ActionLink("Sign In", "Login", "Home", new {@class = "form-btn"})
}

HomeController.cs

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(UserViewData model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    return Content("test");
    //return RedirectToLocal(returnUrl);
}

UserViewData Model

public class UserViewData
{

    [Required]
    [DataType(DataType.Text)]
    public string LoginUsername { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string LoginPassword { get; set; }

    public MembershipUser User { get; private set; }
    public bool HasMessages { get; private set; }
    public List<MembershipUserPrivateMessage> Messages { get; private set; }
    public bool HasNotifications { get; private set; }
    public List<Subscription> Subscriptions { get; private set; }

    public UserViewData(MembershipUser user)
    {
        UserService userService = new UserService();

        this.User = user;
        this.HasMessages = userService.CountUnreadPrivateMessages() > 0;
        this.Messages = userService.GetPrivateMessages(false).Where(p => !p.IsRead).Take(5).ToList();
        this.HasNotifications = false;
        this.Subscriptions = null;
    }
}

EDIT: One possible solution, is to avoid the POCO structure

RouteConfig.cs

// POST home/login
routes.MapRoute(
    name: "Login",
    url: "Home/Login/",
    defaults: new { controller = "Home", action = "Login" }
);

CommunityBar.cshtml

@using (Html.BeginForm("Login", "Home", FormMethod.Post))
{
    @Html.AntiForgeryToken()
    <input type="text" name="username" class="form-input" placeholder="Username"/>
    <input type="text" name="password" class="form-input" placeholder="Username"/>
    <input type="submit" value="Sign In"/>
}

HomeController.cs

// POST: /Home/Login
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(string username, string password)
{
    if (!ModelState.IsValid)
    {
        return Content("fail");
    }

    return Content("success" + username + password);
}

I personally don't like this, as this is really just a way to hide the problem than an actual solution. So I'm gonna keep this Question alive, if somebody figures our what the problem is with the original code. Or I do...


Solution

  • RouteConfig.cs is fine in this format.

    // POST home/login
    routes.MapRoute(
        name: "Login",
        url: "Home/Login/",
        defaults: new { controller = "Home", action = "Login" }
    );
    

    CommunityBar.cshtml has no way to submit the form as the action link in the OP renders an action tag that just links back to the page

    @using (Html.BeginForm("Login", "Home", new {ReturnUrl = ViewBag.ReturnUrl}, FormMethod.Post, new {role = "form"}))
    {
         @Html.AntiForgeryToken()
         @Html.ValidationSummary(true, "", new {@class = "danger"})
         @Html.TextBoxFor(m => m.LoginUsername, new {@class = "form-input", @placeholder = "Username"})
         @Html.ValidationMessageFor(m => m.LoginUsername, "", new {@class = "danger"})
         @Html.TextBoxFor(m => m.LoginPassword, new {@class = "form-input", @placeholder = "Password"})
         @Html.ValidationMessageFor(m => m.LoginPassword, "", new {@class = "danger"})
         <!-- Remove action link and add Submit button here -->
         <button class="form-btn" type="submit">Sign In</button> 
    }
    

    UserViewData Model needs a parameter less constructor to allow model binder to create model on post. currently it requires dependency. Also looks like this model has multiple roles. Use a simple POCO.

    public class LoginModel {    
        [Required]
        [DataType(DataType.Text)]
        public string LoginUsername { get; set; }
    
        [Required]
        [DataType(DataType.Password)]
        public string LoginPassword { get; set; }
    }
    

    HomeController.cs needs to make sure it is passing the model on the original get

    [HttpGet]
    [AllowAnonymous]
    public ActionResult Login() {
        var model = new LoginModel();//also why default constructor needed
        return View(model);
    }
    
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public ActionResult Login(LoginModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }
    
        //...login logic here
    
        //if reach this far then redirect
        if (!string.IsNullOrWhiteSpace(returnUrl)) {
            return RedirectToLocal(returnUrl);
        } else {
            return this.RedirectToAction("MyActionNameHere");
        }
    }