pwilcox pwilcox - 1 month ago 10
Ajax Question

ASP.NET MVC Partial View Post-Back, Inconsistent Updating and Bizzare Behavior

I'm developing an ASP.Net MVC application, and am running into a bizzare issue when trying to update data in my database via a partial postback. I'm still new when it comes to HTTP, AJAX, etc., so I'm hoping it's an obvious error.

Basically, when I try to update a table linking content areas to assessments, the update sometimes works, sometimes doesn't. What's bizzare is that after I post, I query the database directly from the MVC application just to make sure that the expected change was in fact made (that's what all the ViewBag.DebugInfo's are doing in the code below). In every case, the query returns what I hope to see. But then when I query the table via SSMS, I see that the change only sometimes goes through.

How is it that a direct query to a table from my MVC application is showing that an update went through, while I can clearly see that it didn't via SSMS? Is there a silent rollback or something? This is maddening and any help would be much appreciated.

A few bits of info:


  • When I run the "saveTheData" function from the assessmentContent class below outside of MVC, it is always successful.

  • The update is always successful on the first post.

  • The update is successful only about half the time on subsequent posts.

  • When the update is not successful, the the direct query checks from the MVC application nevertheless do seem to show that the update made it all the way to the table.

  • I can barely make out a pattern in the failures. Namely, it seems that whenever I try to update to a higher contentId value, it is successful, if I try to update to a lower contentId, it is not. For instance, updating from a value of 1 (Math) to 2 (Reading) will always go through, but the reverse will not. This pattern does not manifest if it's the first post from the parent view, or if it's updated via Linqpad.

  • I put insert, update, and delete triggers on the database table that write to a logging table to see if perhaps the changes were being rolled back. But no entries on the log table on failure. But I also don't know if rollbacks would undo the triggers as well.

  • I queried dbo.fn_dblog() filtered for Operation = 'LOP_ABORT_XACT', but nothing (though I don't have trained eyes for that difficult view).



Here's my class that fetches and updates the data:

public class assessmentContent {

public int? assessmentId { get; set; }
public List<short> baseline { get; set; } = new List<short>();
public List<short> comparison { get; set; } = new List<short>();

public assessmentContent() { if (assessmentId != null) refreshTheData(); }

public assessmentContent(int assessmentId) {
this.assessmentId = assessmentId;
refreshTheData();
}

public void saveTheData() {

List<short> upserts = comparison.Except(baseline).ToList();
List<short> deletes = baseline.Except(comparison).ToList();

foreach (var upsert in upserts)
reval.ach.addAssessmentContent(assessmentId, upsert);

foreach (var delete in deletes)
reval.ach.deleteAssessmentContent(assessmentId, delete);

refreshTheData();
}

void refreshTheData() {
baseline = reval.ach.assessmentContent(assessmentId).ToList();
comparison = reval.ach.assessmentContent(assessmentId).ToList();
}

}


The logic works fine when I use it outside of my MVC application. So, for instance, if I use it via linqpad, there are no issues. I should mention that assessmentContent() could be named 'getAssessmentContent()'.

Here's my Controller for the Partial View, and some related Code:

public class ContentsModel {
public int? assessmentId { get; set; }
public List<short> comparison { get; set; }
}

public class ContentsController : Controller {

public static string nl = System.Environment.NewLine;

public ActionResult ContentsView(int assessmentId) {

ViewBag.DebugInfo = new List<string>();

var vm = new ContentsModel();
vm.assessmentId = assessmentId;
vm.comparison = reval.ach.assessmentContent(assessmentId).ToList();

return View("~/Views/ach/Contents/ContentsView.cshtml", vm);
}

public ActionResult update(ContentsModel vm) {

ViewBag.DebugInfo = new List<string>();
sqlFetch();

ViewBag.DebugInfo.Add($"VM Pased In {vm.assessmentId} c{vm.comparison.intsJoin()}");
sqlFetch();

var crud = new crud.ach.assessmentContent((int)vm.assessmentId);
ViewBag.DebugInfo.Add($"newly fetched CRUD {crud.assessmentId} b{crud.baseline.intsJoin()} c{crud.comparison.intsJoin()}");
sqlFetch();

crud.comparison = vm.comparison;
ViewBag.DebugInfo.Add($"CRUD after crud_comparison = vm_comparison {crud.assessmentId} b{crud.baseline.intsJoin()} c{crud.comparison.intsJoin()}");
sqlFetch();

crud.saveTheData();
ViewBag.DebugInfo.Add($"CRUD after save {crud.assessmentId} b{crud.baseline.intsJoin()} c{crud.comparison.intsJoin()}");
sqlFetch();

vm.comparison = crud.comparison;
ViewBag.DebugInfo.Add($"VM after vm_comparison = crud_comparison {vm.assessmentId} c{vm.comparison.intsJoin()}");
sqlFetch();

return PartialView("~/Views/ach/Contents/ContentsView.cshtml", vm);
}

void sqlFetch() {
ViewBag.DebugInfo.Add(
"SQL Fetch " +
Sql.ExecuteOneColumn<short>("select contentId from ach.assessmentContent where assessmentId = 12", connections.research).intsJoin()
);
}

}

public static partial class extensions {

public static string intsJoin(this IEnumerable<short> ints) {

var strings = new List<string>();
foreach (int i in ints)
strings.Add(i.ToString());
return string.Join(",", strings);
}

}


I'm aware that I might not have the 3-tier architecture or the Model-View-Controller structure best implemented here.

You'll notice that, in my desperation, I put in a direct check to the database table at every point of change in models.

The Partial View:

@model reval.Views.ach.Contents.ContentsModel
@using reval
@{Layout = "";}

<div id="contentDiv">

<form id="contentForm">

@Html.HiddenFor(m => m.assessmentId)

@Html.ListBoxFor(
m => m.comparison,
new reval.ach.content()
.GetEnumInfo()
.toMultiSelectList(
v => v.Value,
d => d.DisplayName ?? d.Description ?? d.Name,
s => Model.comparison.Contains((short)s.Value)
),
new { id = "contentListBox" }
)

</form>

<br/>
@foreach(string di in ViewBag.DebugInfo) {
@Html.Label(di)
<br/>
}

</div>

<script>

$("#contentListBox").change(function () {

$.ajax({

url: "/Contents/update",
type: "get",
data: $("#contentForm").serialize(),
success: function (result) {
$("#contentDiv").html(result);
},
error: function (request, status, error) {
var wnd = window.open("about:blank", "", "_blank");
wnd.document.write(request.responseText);
}
});
})

</script>


And Finally, the call from the main View:

<div id="testDiv">
@if (Model.assessment != null && Model.assessment.assessmentId != null) {

Html.RenderAction("ContentsView", "Contents", new { assessmentId = Model.assessment.assessmentId });

}
</div>

Answer

I hate it when I solve an issue without exactly pinning down what I did to solve it. But after working on some of the code above, I got it working correctly and consistently.

I'm almost certain, however, that the issue had to do with a misunderstanding of how asp.net mvc works when passing data from server to client and back. Namely, I had an idea that my C# viewmodels and controllers on the server were still alive when their data was being sent to html/asp on the client. I had a hunch that the client data was not the same as the C# objects, but I did feel that ASP.Net MVC was updating the C# object for any changes on postback. Now it is clear to me that in fact the C# objects are fully discarded and completely instantiated (with the constructors called and all related consequences) and repopulated with data from client. And this is true even when no changes are made at the client.

I think that updates were in fact being made to the database. No rollback was occurring. But something was happening upon re-instantiation that was causing a second call to the database and resetting their values. This would explain why it was working perfectly outside of ASP.net MVC. It would explain why I solved the issue after this realization.

I would call this response accurate, but not precise. By that I mean that I have confidence that the guidance resolves the issue, even if it doesn't pin down the exact lines of offending code above. Due to the accuracy, I'm considering it fair game to mark it as an answer. Due to the imprecision, I'm open to marking someone else's response as the answer if they can be more precise. However, since the code above is no longer in use, it is all just for learning purposes.