Login Controller, or posting forms in MVC

Vote:
 

Hi,

 

I recently started to develop for EPiServer 7 MVC. I have some knowledge about Asp.Net MVC since a few other non-episerver project - it's a great framework. But now I'm confused how this integrates with EPiServer.

Adopting the code from Alloy MVC template, I'm now trying to build a simple login page. I have created a LoginPage (inheriting SitePageData), I have created a view model LoginPageModel (inheriting PageViewModel<LoginPage>) that contains a UserName and Password property, and lastly I have built a view (that uses the LoginPageModel) that renders a simple login form that should POST data back to my LoginPageController.

Now, how would would the LoginPageController look like in terms of action methods? In a "normal" MVC project, I would have setup two actions: 

public ActionResult Index()
{
return View();
}

[HttpPost]
public ActionResult Index(LoginModel model)
{
  // Perform authentication logic
  // ...
}

 

But in EPiServer, to start with, I just have a method "public ActionResult Index(LoginPage currentPage)". When I add another action method, e.g. "[HttpPost] public ActionResultIndex Foo(LoginPageModel model)", and try to POST to that it responds with a redirect (301 Moved Permanently) and tries to go to the corresponding  GET action, which is not found since there I only allow POST (as indicated [HttpPost]).

Why does redirect occur and how do I setup up the controller actions? :o

Thank you!

 

 




#74216
Aug 22, 2013 14:12
Vote:
 

ActionResult Index(LoginPage currentPage){} - you can also have an empty method without parameters and get the currentPage object like this instead: var currentPage = PageContext.Page as LoginPage; (cleaner imho).

When it comes to posting a form to your controller, how does your view look? (basically your Html.BeginForm method).

Frederik

#74218
Aug 22, 2013 14:33
Vote:
 

Thanks, didn't know that - it looks cleaner to me as well. Now my login controller have the following action methods defined:

[HttpGet]
        public ActionResult Index()
        {
            var model = new LoginPageModel(PageContext.Page as LoginPage);
            return View(model);
        }

        [HttpPost]
        public ActionResult Foo(LoginPageModel model)
        {
            // Authentication logic
        }

    

My view looks like following (I have removed irrelevant html markup):

@using (Html.BeginForm("Foo", "Login", FormMethod.Post))
{
    <div class="control-group">
        @Html.TextBoxFor(m => m.UserName)
        @Html.ValidationMessageFor(m => m.UserName)
    </div>

    <div class="control-group">
        @Html.PasswordFor(m => m.Password)
        @Html.ValidationMessageFor(m => m.Password)
    </div>
        
    <input type="submit" value="Login">
}

    

When submitting, I see two "requests" happening:

Foo (/Login) POST - 301 Moved Permanently
Foo/ (/Login) GET - 404 Not Found

#74221
Aug 22, 2013 14:51
Vote:
 

What is the value of your forms action attribute (when rendered)? And what's the URL if the login page?

Frederik

#74222
Aug 22, 2013 14:59
Vote:
 

When rendered, the form looks like this: <form action="/Login/Foo" method="post">

The URL of the login page is http://localhost/login

I'm wondering what is making that redirect...

#74223
Edited, Aug 22, 2013 15:10
Vote:
 

That looks correct to me, try and turn on logging to see what's causing the redirect.

Frederik

#74224
Aug 22, 2013 15:13
Vote:
 

Yes, looks correct to me as well :)

Hm, stupid question, but do we have any built-in logging for EPiServer/MVC that covers this?

#74225
Aug 22, 2013 15:16
Vote:
 

Yes, EPiServer is using log4net. There should be a EPiServerLog.config file in your web's root folder (if not copy from the demo site). By default almost at the bottom you have a commented out appender for errorFileLogAppender which logs to a text file under APp_Data\EPiServerErrors.log. You can also change the logging level (default is error, but you also have All, Debug and Info).

Frederik

#74226
Aug 22, 2013 15:19
Vote:
 

Another thing you could try is rename your action method to Index and make sure it posts the form to /login/.

#74227
Aug 22, 2013 15:21
Vote:
 

Right, now I did setup the logger to level All for errorFileLogAppender, and it produces some logs (search/indexing warnings etc). I renamed the action method to Index and Html.BeginForm("Index", "Login", FormMethod.Post), which renders as <form action="/Login/Index" method="post">. The same thing happens now:

Index (/Login) POST - 301 Moved permanently
Index/ (/Login) GET - 200 OK 


Nothing is catched in the logs regarding this. Gah.

#74229
Aug 22, 2013 15:35
Vote:
 

If you have Fiddler or similar, what happens if you submit the form to /Login/ (notice the ending slash, without Index) instead?

#74230
Edited, Aug 22, 2013 15:37
Vote:
 

Interesting - I manually rendered the markup for the form tag to "/Login/" and run the debugger. I didn't get as far as into the correct action method, but an another exception occured:

[MissingMethodException: No parameterless constructor defined for this object.]
   System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) +0
   System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +113
   System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +232
   System.Activator.CreateInstance(Type type, Boolean nonPublic) +83
   System.Activator.CreateInstance(Type type) +6
   System.Web.Mvc.DefaultModelBinder.CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) +183
   System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) +564
   System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) +416
   System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor) +317

    
Do you happen to know what might be causing this? Obviously it fails somewhere when it tries to create my complex parameter (LoginViewModel).

UPDATE: 

The reason for above exception is obviously that my ViewModel has the following and only constructor:

public LoginPageModel(LoginPage currentPage) : base(currentPage)
{
}

    

The problem is how do I change this so that I can add a parameterless ctor, but yet is able to get hold of the currentPage instance? (It is needed in the base class as well as in accessing properties in the view).



Also, why is this not trigger when it renders the action as /Login/Index ? Big thanks for helping out!

#74232
Edited, Aug 22, 2013 15:52
Vote:
 

Usually I have a separate DTO object for posting data back, but you should be able to add a parameterless constructor to your ViewModel base class and then later set it in the method instead using PageContext.Page as LoginPage. You could also add a custom model binder for this.

Frederik

#74248
Aug 22, 2013 18:10
Vote:
 

Or override OnActionExecuting() and set it there.

#74249
Aug 22, 2013 18:13
Vote:
 

Hm, yes separate DTOs for incoming data might be the right way to go. I'm just worried that I'm building to much "infrastructure" - first the PageType model, then the ViewModel for modelling the view and then an additional DTO model for posting back data. But it might be worth it and perhaps there is some performance gain also (less overhead compared to creating a fully populated ViewModel)?

PageContext.Page - is that only available in the context of a controller, or is there a way to fetch the current page from within the ViewModel class?

I will have a look at how to implement a custom model binder or overriding OnActionExecuting(). Thank you!

 

 

 

#74251
Edited, Aug 22, 2013 20:20
Vote:
 

You can also use EPiServer.ServiceLocation.ServiceLocator.Current.GetInstance<PageRouteHelper>().Page as LoginPage.

Frederik

#74252
Aug 22, 2013 20:56
Vote:
 

Aha, thank you, that's good to know! I have not been able to find such "tips and tricks" or best practices for EPiServer 7/MVC yet.

I'm leaning towards separate DTOs though, prefer not to have logic in the models! 

#74261
Aug 23, 2013 8:46
Vote:
 

Aha, thank you, that's good to know! I have not been able to find such "tips and tricks" or best practices for EPiServer 7/MVC yet.

I'm leaning towards separate DTOs though, prefer not to have logic in the models! 

#74262
Aug 23, 2013 8:46
Vote:
 

Yes, EPiServer and MVC is still a little new so not that much information out there. I have Google Docs document with notes that I've taken while developing with EPiServer and MVC that I hope to post soon as a blog post (need to work a little more with EPiServer and MVC first).

Joel Abrahamasson just started writing a book on EPiServer Development with MVC that I recommend checking out: EPiServer 7 CMS Development.

Frederik

#74264
Aug 23, 2013 8:53
Vote:
 

Great to hear!

Btw - do you know why my Html.BeginForm("Index", "Login", FormMethod.Post) renders as "/Login/Index"? If I manually add the trailing slash it works, otherwise it doesn't. Also I didn't expect it would add "Index" to the URL at all. Is this related to how EPiServer sets up the default routes?

#74288
Aug 23, 2013 11:59
Vote:
 

Also remember, if you are using multilingual site (more than one language) use:

@using (Html.BeginForm(null, null, new { language = ContentLanguage.PreferredCulture.Name }))

    

This will make sure that action Url is "/{language}/{controller}/{action}/"

#74325
Edited, Aug 23, 2013 14:58
Vote:
 

Thank you, seems to work.

#74335
Aug 23, 2013 17:01
This thread is locked and should be used for reference only. Please use the Episerver CMS 7 and earlier versions forum to open new discussions.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.