Handle Ajax request with expired forms authentication, in MVC ajax.form

There is a situation where I display a screen dynamically load a partial view using ajax.

And when the authentication expires, user initiates an ajax call, then the action is being called without authorization.

It actually returns a login view inside the DOM object on the page rather than returns a expected content with partial view.

The users will have bad UI experience and get confused.

Here I demonstrate the scenario to show you what exactly happens and what we can do about it.

1. Create an ajax.form which simply returns a partial view.


@using (Ajax.BeginForm("AjaxTestNoHandle", "Home", new AjaxOptions { UpdateTargetId = "testcontainer2"}, new { id="test-form"}))
{ 
    
}

<div id="testcontainer2">
</div>

2. Create the action method with “Authorize” attribute and return a partial view “TestPartial”

  [Authorize]
        public ActionResult AjaxTestNoHandle()
        {
            return PartialView("TestPartial");
        }

3. Run the application and click button “No Redirection Handling”. Because the user is not authenticated and it returns a partial view with the whole “Login” page. (I made login page with black background for displaying purpose)

image

Now let’s create  a customized authorize attribute to handle this situation.

1.  Create a CustomizedAuthorizeAttribute to handle login redirection if authentication is expired.  I only override the action HandleUnauthorizedRequest to detect if request is ajax, return a json result with a redirect url.

    public class CustomizedAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            string url = string.Format("{0}?returnUrl={1}",System.Web.Security.FormsAuthentication.LoginUrl,
                filterContext.HttpContext.Server.UrlEncode(filterContext.HttpContext.Request.RawUrl));
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                var redirectResult = filterContext.Result as RedirectResult;
                if (filterContext.Result is RedirectResult)
                {
                    // It was a RedirectResult => we need to calculate the url
                    var result = filterContext.Result as RedirectResult;
                    url = UrlHelper.GenerateContentUrl(result.Url, filterContext.HttpContext);
                }
                else if (filterContext.Result is RedirectToRouteResult)
                {
                    // It was a RedirectToRouteResult => we need to calculate
                    // the target url
                    var result = filterContext.Result as RedirectToRouteResult;
                    url = UrlHelper.GenerateUrl(result.RouteName, null, null, result.RouteValues, RouteTable.Routes, filterContext.RequestContext, false);
                }
                filterContext.Result = new JsonResult
                {
                    Data = new { Redirect = url },
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
                };
            }
            else
            {
                //non-ajax request
                base.HandleUnauthorizedRequest(filterContext);
            }
            
        }

2. Create controller action method with CustomizedAuthorize

        [CustomizedAuthorize]
        public ActionResult AjaxTest()
        {
            return PartialView("TestPartial");
        }

3. Create ajax.form post to “AjaxTest” method.


@using (Ajax.BeginForm("AjaxTest", "Home", new AjaxOptions { UpdateTargetId = "testcontainer" }, new { id="test-form"}))
{ 
    
}

<div id="testcontainer">
</div>

4. Click “Redirection Handling” button on the page without login. In FireBug – Console, you can see a json is returned

{"Redirect":"/Account/Login?returnUrl=%2fHome%2fAjaxTest%3fLength%3d4"}

5. Now let’s add a javascript function to handle the json and redirect to the returned url. Add a OnSuccess event in “AjaxOptions” call “validate”


@using (Ajax.BeginForm("AjaxTest", "Home", new AjaxOptions { UpdateTargetId = "testcontainer", OnSuccess="validate" }, new { id="test-form"}))
{ 
    
}

<div id="testcontainer">
</div>

6. Create javascript function validate.


    function validate(data) {
        if (data.Redirect)
            window.location = data.Redirect;
    }

7. Here you go, all done! When you click the “Redirection Handling” button, it will detect the forms authentication login url and return to the login page inside the ajax request!

Cools!

To download the demo project, go to codeplex https://ajaxrequestauthentic.codeplex.com/

Advertisements

Validation Summary is not cleared after Ajax.Form successfully posted, MVC 3

My scenario is after the invalid form values are posted through ajax.form, and the validation summary is displaying on the view. After I filled in all the required fields and posted the form with valid values, the post back data is correctly displaying, but the validation summary is remaining on the page.

Here are some code snippets,

The view contains two date picker (textbox) which are required fields.

    @model ClosedTicketByProductReportViewModel
    @using (Ajax.BeginForm("ClosedTicketByProductReport", "Report", new AjaxOptions { UpdateTargetId = "closedTicketByProductGrid", LoadingElementId = "loadingcontainer" }))
    { 
        @Html.ValidationSummary()
                
        <div class="control-group">
            Date From
            <div class="controls">
                @Html.TextBoxFor(p =&gt; p.DateFromForClosedProduct, new Dictionary { { "class", "small" }, { "type", "date" }, { "data-datepicker", "datepicker" } })
            </div>
        </div>
        <div class="control-group">
            Date To
            <div class="controls">
                @Html.TextBoxFor(p =&gt; p.DateToForClosedProduct, new Dictionary { { "class", "small" }, { "type", "date" }, { "data-datepicker", "datepicker" } })
            </div>
        </div>

        Generate Report
        
    }

Here is the view model I created for the view,

    public class ClosedTicketByProductReportViewModel
    {  

        [Required(ErrorMessage = "Please select Date From")]
        public DateTime? DateFromForClosedProduct { get; set; }

        [Required(ErrorMessage = "Please select Date To")]
        public DateTime? DateToForClosedProduct { get; set; }

        //report data
        public List Cases { get; set; }

    }

After I click submit without entering any dates, it displays the error messages as expected.

image

Then I enter the dates, and click submit again. The post back data is retrieved from controller action, but the validation summary remains on the page. As you can see the validation errors on the textbox itself are gone. Ajax.Form is only lack of support on the validation summaries.

image

Here is my fix, I don’t think it is great, but it is a work around.

    @using (Ajax.BeginForm("ClosedTicketByProductReport", "Report", new AjaxOptions { OnSuccess = "HideValidationSummary", UpdateTargetId = "closedTicketByProductGrid", LoadingElementId = "loadingcontainer" }))
    { }

I created a javascript function and call it on OnSuccess = "HideValidationSummary, the javascript function just hide the validation summary div.

function HideValidationSummary() {
    $(".validation-summary-errors").hide();
}

If anyone come across my blog post and has a better solution, please leave me a message. Much appreciated.