Doug.Instance

Improving the world one post at a time
posts - 22 , comments - 46 , trackbacks - 0

Friday, August 5, 2011

Redirecting from a Partial View the "Right" Way in ASP.Net MVC 2

I am a firm believer that, regardless of the framework you use, if the framework is good it provides you all of the things you need.  I believe ASP.Net MVC 2 is good and therefore provides me everything I need.  This means I don't need to write a significant amount of custom UI code (jQuery, MVC Ajax, etc.) to do things that should be considered "standard".  One of these pretty standard things submitting a form via Ajax.  MVC provides for this very well with Ajax.BeginForm.  The contents of the form are submitted using the normal MVC process so I can use the standard patterns and behaviors like attribute-based validation, default model binding, etc.  One thing that isn't very obvious is how to conditionally trigger UI events depending on the result of the ajax call.

First, let's take a look at a typical scenario.  You created a new MVC 2 project in Visual Studio and it created a LogOn view which uses Html.BeginForm().  Wanting a better user experience, you create a partial view for the contents of your LogOn form and call it LogOnDetails which then looks something like this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<LogOnModel>" %>
<% using (Ajax.BeginForm("LogOn", new AjaxOptions { UpdateTargetId = "logOnDetails" }))
   { %>
<%: Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.") %>
<fieldset>
<legend>Account Informations</legend>
<divclass="editor-label"><%: Html.LabelFor(m => m.UserName) %></div>
<divclass="editor-field"><%: Html.TextBoxFor(m => m.UserName) %><%: Html.ValidationMessageFor(m => m.UserName) %></div>
<divclass="editor-label"><%: Html.LabelFor(m => m.Password) %></div>
<divclass="editor-field"><%: Html.PasswordFor(m => m.Password) %><%: Html.ValidationMessageFor(m => m.Password) %></div>
<divclass="editor-label"><%: Html.CheckBoxFor(m => m.RememberMe) %><%: Html.LabelFor(m => m.RememberMe) %></div>
<p>inputtype="submit"value="Log On"/></p>
</fieldset>
<% } %>

 

Then you change your LogOn.aspx to use Ajax.BeginForm and this partial view:

<asp:Content ID="loginTitle" ContentPlaceHolderID="TitleContent" runat="server">
    Log On
</asp:Content>
<asp:Content ID="loginContent" ContentPlaceHolderID="MainContent" runat="server">
<h2>Log On<h2>
<p> Please enter your username and password.
<%: Html.ActionLink("Register", "Register") %> if you don't have an account. </p>
<divid="logOnDetails">
<% Html.RenderPartial("LogOnDetails", Model); %>
</div>
</asp:Content>

Next you change your controller so it returns the partial view if there is an error.

1:  [HttpPost]
2: public ActionResult LogOn(LogOnModel model, string returnUrl)
3: {
4: if (ModelState.IsValid)
5: {
6: if (MembershipService.ValidateUser(model.UserName, model.Password))
7: {
8: FormsService.SignIn(model.UserName, model.RememberMe);
9: if (!String.IsNullOrEmpty(returnUrl))
10: {
11: return Redirect(returnUrl);
12: }
13: else
14: {
15: return RedirectToAction("Index", "Home");
16: }
17: }
18: else
19: {
20: ModelState.AddModelError("", "The user name or password provided is incorrect.");
21: }
22: }
23:
24: // If we got this far, something failed, redisplay form
25: return PartialView("LogOnDetails", model);
26: }

Voila! Right? Not quite. As you probably already know if you are reading this, Redirect and RedirectToAction do not work the way you want.  The entire page is rendered inside the target element of the ajax call instead of actually redirecting the browser to the new page.

One commonly used solution to this problem is to simply brute force the browser to redirect by returning a javascript result.  That code would look something like this:

return JavaScript( string .Format( "document.location = '{0}';" , returnUrl));

So what is wrong with this approach?  You are putting UI code inside your controller so you are mixing your "V" with your "C" from an MVC perspective.  So how can we do this better?

One option is to use the JsonResult and serialize an object containing the result data which can then be handled by a javascript function set in the OnSuccess parameter of the ajax call. However, even this can be problematic since there have been some changes between MVC 2 and MVC 3 so you can have compatability problems if you need to be able to go back and forth between versions.

There is a simpler option which might seem like a hack at first, but I think it is a very natural use of the available options in MVC and generates a very clean result. First, let's change our LogOnModel class to add a few things to pass back to the UI.

1:  public class LogOnModel
2: {
3: [Required]
4: [DisplayName("User name")]
5: public string UserName { get; set; }
6:
7: [Required]
8: [DataType(DataType.Password)]
9: [DisplayName("Password")]
10: public string Password { get; set; }
11:
12: [DisplayName("Remember me?")]
13: public bool RememberMe { get; set; }
14:
15: [ReadOnly(true)]
16: public bool? IsLoggedIn { get; set; }
17:
18: [ReadOnly(true)]
19: public string ReturnUrl { get; set; }
20: }

Now we simply return this back to our partial view. Then all the magic happens in the UI (as you might say that it should).

						1:  [HttpPost]
2: public ActionResult LogOn(LogOnModel model, string returnUrl)
3: {
4: if (ModelState.IsValid)
5: {
6: if (MembershipService.ValidateUser(model.UserName, model.Password))
7: {
8: FormsService.SignIn(model.UserName, model.RememberMe);
9: model.IsLoggedIn = true;
10: model.ReturnUrl = returnUrl;
11: if (string.IsNullOrEmpty(model.ReturnUrl))
12: {
13: model.ReturnUrl = Url.Action("Index", "Home");
14: }
15: }
16: else
17: {
18: ModelState.AddModelError("", "The user name or password provided is incorrect.");
19: }
20: }
21:
22:
23: return PartialView("LogOnDetails", model);
24: }

Notice that the controller action is more compact. Also, notice that we added some additional logic to set the redirect URL to the home page if none was provided and that this is done in the controller where it belongs.

Next we will output the IsLoggedIn and ReturnUrl values in the partial view. This can go anywhere in the partial view. (Note: This is why we put the ReadOnly attribute on the properties - so we can update the model in our controller and not have the modelstate undo the changes).

<%= Html.HiddenFor(m => m.IsLoggedIn)%>
<%= Html.HiddenFor(m => m.ReturnUrl)%>
		
Next, we will write a very simple bit of jQuery to redirect after the user has logged in.
1:  function logInComplete() {
2: if ($("#IsLoggedIn").val() == 'true' && $("#ReturnUrl").val() != '') {
3: document.location = $("#ReturnUrl").val();
4: }
5: }

And finally, modify the Ajax.BeginForm call to call this function after the response is returned.

<% using (Ajax.BeginForm("LogOn", new AjaxOptions { UpdateTargetId = "logOnDetails", OnSuccess = "logInComplete" }))
    { %>
		

So there are some other advantages of this method. You can for example very easily display a message with a link to redirect the user instead of using javasccript to redirect. Also, since you are passing an indicator of whether the current user is logged in or not to the controller, you can change your partial view that renders the form so it only renders the form if the user is not logged in and otherwise displays a message that the user is logged in.

Voila! For real this time.

]]>
]]>
			
			

Posted On Friday, August 5, 2011 6:52 PM | Comments (6) | Filed Under [ MVC ]

Powered by: