9

This is probably again a newbie question.

When I create an ASP.NET MVC2 application, an Account Controller with an Action LogIn is created like this:

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
   if (ModelState.IsValid)
   {
      if (MembershipService.ValidateUser(model.UserName, model.Password))
      {
         FormsService.SignIn(model.UserName, model.RememberMe);
         if (!String.IsNullOrEmpty(returnUrl))
         {
            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);
    }

Now, I don't want to have a login page, I want to have login controls as part of a bigger page. So, I changed Login.aspx to Login.ascx and I am integrating it in my main view either with Html.RenderPartial or Html.RenderAction.

Both works like a charm if the login is successfull. If it is not, the

return View(model)

is killing me. What I want is to go back to my main page (call it Home/Index) but with the error information of the partial view.

return RedirectToAction("Index", "Home")

Obviously doesn't work.

Hints?

Ikke
  • 99,403
  • 23
  • 97
  • 120
Sparhawk
  • 1,517
  • 1
  • 14
  • 28

5 Answers5

3

This is certainly not a newbie question and I have combed up and down the web for an answer to this problem and so far the best solution I've found was kind of hidden in this tutorial here. This is what Darin Dimitrov was suggesting with the Ajax refresh. I'll summarize the important parts of that link and why this isn't easily fixed : /

Ajax refresh based on weird lover

The solution with the ajax refresh pretty much hinges on the following function (weird lover uses ControllerContext but it didn't exist for me so I have ControllerExtension):

ControllerExtension.RenderPartialViewToString(this,"mypartial", (object)model)

This function is what takes your model + modelstate and rerenders your partial view into an html string. You can then take that string and send it back in a json object to some javascript to refresh the view. I used jquery and it looks like this,

$(document).ready(function () {
    var partialViewUpdate = function (e) {
            e.preventDefault(); //no postback
            var partialDiv = $(this).parent(".partial");
            $.post($(this).attr("action"),
                   $(this).serialize(),
                   function (json) {
                   if (json.StatusCode != 0) {
                       // invalid model, return partial 
                       partialDiv.replaceWith(json.Content);
                   }
                   else if (json.Content != null && json.Content != "") {
                       window.location.replace(data.Content);
                   };
           });

    $(".partial").find("form")
                 .unbind('submit')
                 .live("submit", partialViewUpdate);
};

Jquery explanation:

  1. Look up the div that contains my partial (class="partial") and find the form within that div
  2. Unbind any other "submit" events with that form (I got some strange double submit bug until I did that unbind).
  3. use "live" so that once the content is replaced it rebinds again
  4. Once we enter the function partialViewUpdate...
  5. Prevent the form from finishing the submit so that it can all be handled by ajax.
  6. fetch the div that contains my partial (will use this later)
  7. Setup the jquery post url by taking it from the form, $(this).attr("action")
  8. Take the form (i.e. our model) and serialize it for the controller function, $(this).serialize()
  9. Create the function that will handle the ajax return value.
  10. I use my own personal json object, where a StatusCode 1 is bad. So if it's bad then I take what's in Content, this is the string that RenderPartialViewToString gave me, and I just replace the contents of the div that contains my partial.

Why it doesn't "just work" normally

So the reason the partials' don't just work with modelstate validation is that you can't return View(model) with the POST because MVC will resolve that to the route address of the partial view (login.ascx) instead of where the partial is embedded (index.aspx).

You also can't use RedirectAction() because that will send it to (index.aspx) controller function, which is the equivalent of clearing everything and refreshing the index.aspx page. However if you use that ActionFilter suggested by Chino and Thabaza then when your page is refreshed and the login.ascx controller function is fired off again it will pick up that tempdata. This however doesn't work if refreshing the page causes a hassle with client-side code such as popup modals (i.e. if you refresh your popup is gone).

Say it aint so

I would prefer if it "just worked" so if anyone knows the correct/better way of doing this pleaaaase share it! I still feel that the Ajax refresh and ActionFilter solutions are not a clean way of doing it because it almost makes it look like partial views with forms are impossible to use without some sort of "trick".

Thirlan
  • 712
  • 1
  • 8
  • 26
1

Yeah redirecttoaction but provide an error message with it in the tempdata so you should do something like this

TempData["errorMsg"] = "incorrect values provided";
return RedirectToAction("Index", "Home")

Of course in the main index you should have a div that displays the message

<%= html.Encode(TempData["errorMsg"]) %>

EDIT I see you want to maintain the modelstate that might be a problem but what you could do is pass the modelstate in the index action or pass the modelstate object in the tempdata. What you can do then is check if a there are modelstate errors in the object and if there are check the field and add the error to the right field.

Chino
  • 821
  • 6
  • 13
  • Hm, probably I could put the whole LogOnModel in the TempData. But to me this seems to be a hack. What would be the clean solution here? – Sparhawk Jun 21 '10 at 06:08
  • Yes it is a hack the clean solution is to write a filter for it. Look at this question and my answer for an example http://stackoverflow.com/questions/2503032/where-to-use-controller-httpcontext/2503085#2503085 In the filter you check if the modelstate is valid if not re add the values of course it would be better to put this in a custom attribute because I do not think you need this in all actions of a controller. You can then use the attribute on actions in which you want to maintain the modelstate. – Chino Jun 24 '10 at 22:57
  • Meh. This is what TempData is for. My guess is you don't like the magic strings and lack of strongly typed data. That's fair, but its a style choice, not a hack. – a7drew Jun 29 '10 at 23:05
0

You could explicitly specify the view to return:

return View("~/Views/Home/Index.aspx", model);

This way error information will be preserved and correct view rendered.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • If I did this, I would pass the LogOnModel to the Home/Index-View. That probably wouldn't work. Perhaps the question is not so newbie after all :) – Sparhawk Jun 21 '10 at 06:05
  • 1
    @Sparhawk, you are correct. What if you returned the model of the Home/Index view? The LogOn view model should be a subset of the Home/Index view model. As an alternative you could ajaxify the LogOn form and invoke this action asynchronously which in case of success the redirect would be performed on the client (window.location.href) and in case of error it would return a `PartialView` and the script calling this action would refresh the `div` containing this form. – Darin Dimitrov Jun 21 '10 at 06:20
  • 1
    Currently it is not a subset of the Home/Index view model. I have a portal and each page (like Home/Index) has a number of controls on it. I am rendering the controls with RenderAction into the page. So, why should the Home/Index view now anything about a LogonModel? Sure, I could use ajax. But that would feel like circumventing the issue. Is there no clean solution for partial views with validation logic? – Sparhawk Jun 21 '10 at 09:56
0

Take a look at practice #13 on this blog. This method works well to pass modelstate information when you're coding in the PRG (Post-Redirect-Get) style. You'll just need to create a couple of action filters and apply them to your get and post actions as appropriate.

Tahbaza
  • 9,486
  • 2
  • 26
  • 39
0

I had the same issue when used Ajax.BeginForm, where i needed to return a partial view, but all the modelstate errors were gone. what does the trick is to isolate the Ajax.BeginForm part to a separate View, and RenderPartial this view within the UpdateTargetId div in another, containing view.

This way you can return the viewmodel with the model errors when you have them, or else just show some success message of your choice (if any). here is a great, detailed explanation: http://xhalent.wordpress.com/2011/02/05/using-unobtrusive-ajax-forms-in-asp-net-mvc3/

Eitan Rousso
  • 181
  • 1
  • 5