Showing posts with label caching. Show all posts
Showing posts with label caching. Show all posts

Detecting and fixing Ajax.BeginForm and Partial View stale data

This blog post describes a technical hurdle and how to solve it with some (actually 2) clever lines of code.

In a recent ASP.NET MVC application called Vizion Logs, me and my buddy Mario Brenes from IT Vizion were facing a difficult situation: our Save action method returns a PartialView but the Model value passed to this partial view was not being displayed correctly under the covers, that is, the View was keeping the previous/stale Model value. This was completely ruining our business logic on the Controller side. This was a nasty problem because the first time the view/form is rendered the Model has an ID property set to -1. After post backing the Model and saving it we then pass this same Model to the partial view but now it has the ID property set to the ID automatically generated by an Oracle sequence defined in the database side. We count on this ID property value for subsequent save operations in which case this specific Model will be updated and not saved again as a new row in the database.

It’s worth mentioning that we use that Save action method to handle both Saving and Updating the log objects.

Here you can see the form being constructed with an Ajax.BeginForm on the View side:

@using(Ajax.BeginForm("Save", Model.Field,
    new AjaxOptions
    {
        HttpMethod = "POST",
        UpdateTargetId = "logs",
        InsertionMode = InsertionMode.Replace,
        OnFailure = "handleFailure",
        OnSuccess = "setupForm();parseFormForUnobtrusiveValidation();",
    }))

We did a Google Hangout to investigate what was going on… two minds working together to detect a problem is better than just one. It was Pair Programming (remotely) at its best. These hangouts with Mario are helping me to exercise my English too – WOOT! It’s a double gain or why not say a bargain!?
During this investigation Mario kept asking me questions that led us to the correct places in the code path. This allowed us to solve the problem in less than 30 minutes…

At first we tested the condition that was leading to the buggy path: saving the logs for the first time worked great but then hitting the save button a second time was causing duplicate entries on the database instead of updating the log entries already created.

As our Save method counts on the log’s ID property for subsequent saves (updates to be technically correct) it was fair enough to check the model that was being passed to the partial view right after hitting the save button for the first time. We saw that it had the ID property set correctly to the ID generated by the database. So this was the expected behavior. On the view side we use @Html.HiddenFor(l => l.ID) to keep the ID property as part of the post back values. This is important because with this we know when inside the Save action method that that specific log entry must be updated and not saved. A log entry must be created when it has an ID = -1 and it must be updated when ID != -1. Now what was left for us to check was see if the correct ID was present in the HTML generated by the partial view. Bingo: the ID property was still -1. Houston we have a problem. Now let us look at the jquery.unobtrusive-ajax.js file that handles the Ajax.BeginForm. What interest us on this JavaScript file is the asyncOnSuccess method:

function asyncOnSuccess(element, data, contentType) {
    var mode;

    if (contentType.indexOf("application/x-javascript") !== -1) {  // jQuery already executes JavaScript for us
        return;
    }

    mode = (element.getAttribute("data-ajax-mode") || "").toUpperCase();
$(element.getAttribute(
"data-ajax-update")).each(function (i, update) { var top; switch (mode) { case "BEFORE": top = update.firstChild; $("<div />").html(data).contents().each(function () { update.insertBefore(this, top); }); break; case "AFTER": $("<div />").html(data).contents().each(function () { update.appendChild(this); }); break; default: $(update).html(data); break; } }); }

We placed a breakpoint in this method and analyzed the data parameter that contains the HTML that’s going to be rendered by the partial view. To our surprise we saw that the ID property was still –1 despite it being 10 for example on the Controller side right before passing the model to the PartialView.

Jesus Christ… this is crazy! Confused smile How can that be the case?!

Well I thought… this sounds like a cache problem. This was the first time I was using the Ajax.BeginForm construct. Before using it I was used to use plain @Html.BeginForm and jquery $.ajax calls and as such I had never experienced this let’s say “bug” before. With this caching hint on mind I Googled about the problem and AS ALWAYS StackOverflow is our best friend. I found this question:

MVC Partial View Model Not Refreshing

jgauffin tells us to use the OutputCacheAttribute to disable cache for that specific action method and also clear the ModelState object. Something like this:

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult Save(LogFormViewModel model)
{   
    // Custom Save/Update logic here...
    
    ModelState.Clear();
    
    return PartialView(model);
}

After decorating our Save action method with the OutputCache attribute and calling ModelState.Clear() we retested the form and to our surprise it just worked and we told each other: that’s great buddy!

We just solved the problem with some clever lines of code thanks to our collaborative thinking and to StackOverflow that never ceases to amaze me. For 99% of questions I have I can be sure that they’ve already been answered there – it’s just a matter of using the correct wording in a Google Search, but hold on: before searching you must first know why and what you need to search for. I think this is one lesson learned with this post.

That’s it. Hope this post is useful to the next soul/spirit/human being that might stumble on this.