MVC Ajax Form inside a For Loop - asp.net-mvc

I am using an Ajax.BeginForm inside a For Loop with allows me to post to the controller for each item in a the for loop. Initially the for loop renders each item correctly on page load.
When the controller has handled the incoming View Model which is populated correctly from the Ajax.BeginForm, the method sends back a newly generated view model back to the View, however each item in my For loop is now duplicated, and all the textboxes now have the value of the submitted View Model.
Very confused how to structure this code correctly, I need the submission to work based on ajax and partial view. I did look at using JQuery to submit, but was concerned I may lose the AntiForgeryToken and that the Ajax.BeginForm was a more elegant approach. Any help greatly appreciated. Happy to provide more information also.
VIEW
#model Namespace.Models.MyParentViewModel
#for (int i = 0; i < Model.Items.Count; i++)
{
using (Ajax.BeginForm("SaveItem", "Controller", new AjaxOptions { HttpMethod = "POST", UpdateTargetId = "pensions" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary()
<div>
#Html.Hidden("ID", Model.Items[i].ID)
#Html.TextBox("TheName", Model.Items[i].TheName, new { #class = "form-control", #id = "item-" + i})
<input type="submit" value="Save" class="btn save" name="Command" />
</div>
}
}
CONTROLLER
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("SaveItem")]
public ActionResult SaveItem(MyItemViewModel mivm, string Command)
{
if (ModelState.IsValid)
{
#do some logic
}
// Return a newly populated MyViewModel with updated mivm.
var model = PopulateMyParentViewModel();
return PartialView("_MyPartialView", model);
}
private MyParentViewModel PopulateMyParentViewModel()
{
List<MyItemsViewModel> lstItems = new List<MyItemsViewModel>();
foreach (var item in enity.items.OrderBy(p => p.ID).ToList())
{
var ExistingItem = new MyItemViewModel
{
ID = item.ID,
TheName = item.TheName
};
lstItems.Add(MyItemViewModel);
}
MyParentViewModel.Items = lstItems;
return MyParentViewModel
}

I am not sure what is "pensions" in your code, I guess it is a div surrounding every form generated by the loop? If so your problem is the following:
First, if none of the items are interrelated (that is, triggering some update on an item causes another item to be updated as well), you shouldn't waste resources to send the whole list back to the browser - consider just sending back the updated display of the affected "record" and just update the corresponding item with the response.
What causes items to be duplicated: the AjaxOptions class has a property InsertionMode, and as I can recall, its default value is InsertionMode = InsertionMode.InsertAfter, which causes the duplicates to appear. Why? You send an Ajax request to the server, which sends back an HTML fragment populated by the entire list, instead of just one item, then the browser appends that fragment to the existing records.
Solution
If you remake your project to just send back one record instead of all of them, the just add an uniquely identified (= has an unique id attribute) <div> element around the using(...) block, set InsertionMode to InsertionMode=InsertionMode.Replace and set the UpdateTargetId property to the id of that <div>, like so:
<div id="record-#i">
#using (Ajax.BeginForm("SaveItem", "Controller", new AjaxOptions { HttpMethod = "POST", UpdateTargetId = "record-" + i.ToString(), InsertionMode = InsertionMode.Replace }))
{
// ...
}
</div>
That will cause the Ajax response to replace the contents of the container from which the request is sent (by contents I mean that the wrapping element itself is not replaced! You would need to use InsertionMode.ReplaceWith for that behavior). If you mistake this, you end up dumping crazy amounts of nested elements inside the host container, possibly breaking scripts and/or styles which use very specific selectors.
If you stick to sending a whole collection of items back, then just wrap the for loop with a <div> tag, give it an id, set InsertionMode to InsertionMode=InsertionMode.Replace, and the UpdateTargetId property to the id of that <div>.
--------- Update -----------
To answer your comment:
Assume you have a view which - for the sake of simplicity - displays records in a tabular format. Then, the result looks like this:
Name | Age | Salary | Hide
------------------------------------
Peter | 32 | $15k | Hide button
Eva | 28 | $12k | Hide button
Assume what you change with an Ajax request is that whenever you hide a record, you send back a single table row with a button that now displays 'show' instead of 'hide' and the same data about the person which belongs to the button that triggered the request.
Then, when you make a direct (=non-ajax, when you directly navigate to the page) request, you do this:
#model PeopleCollection
...
<table>
<thead>
<tr>
<td>Name</td>
<td>Age</td>
<td>Salary</td>
<td>Hide</td>
</tr>
</thead>
<tbody>
#foreach (Person record in Model) {
#Html.Partial("TableRow_Partial",record)
}
</tbody>
The "TableRow_Partial" view looks like this:
#model Person
#{
AjaxOptions opts = new AjaxOptions() {
UpdateTargetId = "person-row-" + Model.Id.ToString(),
InsertionMode = InsertionMode.ReplaceWith
// etc
};
}
...
<tr id="person-row-#Model.Id">
<td>#Model.Name</td>
<td>#Model.Age</td>
<td>#Model.Salary</td>
<td>
<!-- configure the BeginForm(..) call with the action name and a routeValue anonymous object as "new { id = Model.Id}" -->
#using (Ajax.BeginForm(...)) {
<input type="submit" value="#(Model.IsHidden ? "Show" : "Hide")" />
}
</td>
</tr>
This will simply extract the construction of loops into a separated partial view. The action method procession your Ajax-request will simply return this view, filled with the one single record you have updated, like so:
public ActionResult UpdateRecord(Int64 id)
{
var _record = Repository.Get(id);
// logic to hide and update record
return PartialView("TableRow_Partial",_record);
}
For this to work, you have to set InsertionMode to InsertionMode.ReplaceWith. All this does is that when you send an ajax request to do something with just one record, you only refresh one item - the one that triggered the ajax call, and then you simply return the new row (One row!) and replace the old one with this new one.
If you really want to stick to sending the whole collection of items back to the client every time you do something with the records, just send the entire rendered table to the client and replace the old one with the new one in a similar manner.

Related

Need to upgrade a simple page post back form to use some row "inpage" postback mechanism?

I am using MVC3, Razor and C#.
I have implemented a simple and robust inline editing solution for a grid.
Basically I use Razor to build my form which encloses the grid, and then the row that matches the item id gets opened up as the editable row which is coded as a partial View.
The Grid View (part):
#using (Html.BeginForm("Edit", "GridTest"))
{
<table>
<tr>
<th>col1</th>
<th>Col2</th>
</tr>
#{
foreach (var item in Model)
{
<tr>
#{
if ((Model.ItemId == item.Id))
{
Html.RenderPartial("_EditRow", item);
}
else
{
Html.RenderPartial("_DisplayRow", item);
}
}
</tr>
}
</table
}
EditRow.cshtml
#Html.TextBoxFor(p=>p.Name)
Save
Cancel
#Html.HiddenFor(p=>p.Id)
DisplayRow.cshtml
<td>
#Model.Name
</td>
<td>
#Html.ActionLink("Edit", "Edit", "GridTest", new {id = Model.Id}, null)
</td>
GridTest/Edit Action
public ActionResult Edit(int id)
{
var myRecord = db.Orders.First(p => p.Id == id);
return View("Index",myRecord);
}
GridTest/Edit Post Action
[HttpPost]
public ActionResult Edit(Order myRecord, string btn="")
{
if (btn == "Save")
{
if (ModelState.IsValid)
{
Order myCurrentRecord = db.Order.First(p => p.Id == myRecord.Id);
myCurrentRecord.Name = myRecord.Name;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(myRecord);
}
else
{
return RedirectToAction("Index");
}
The above code shows the design. It works greats and is simple. However it causes a postback of the complete page, and I would like to stop this "flashiness". So I suspect I need to somehow tweak the above code such that the "EditRow" posts inline, without refreshing the entire page. I suspect I am looking at using Ajax?
So how can the above code be simply upgraded to prevent complete page refresh, but rather row refresh?
Many thanks in advance.
At its simplest, you can probably just capture the submit event of the form and serialize that form to an AJAX POST. Something like this:
$('form').submit(function () {
$.post('#Url.Action("Edit", "GridTest")', $('form').serialize())
.done(function(data) {
// AJAX call is complete
// do something on the page?
});
return false;
});
This will use the same controller action in the same way that the form does, just via AJAX. The controller action will also respond the same way, which probably isn't what you want with an AJAX request. You might instead want to return some JSON data to indicate success, error conditions, results of server-side processing, etc. Something as simple as this can just indicate success from the controller action:
return Json(true);
You can, of course, return any structured data by passing it to Json(), which will serialize it to JSON data as the data value in the JavaScript done handler.
Edit: If you want to replace a piece of the client-side content wholesale with a partial view, you can still return that partial view in the AJAX request. The controller action can do something like this:
return PartialView("_DisplayRow", myRecord);
(Assuming myRecord is the type that is bound to that partial view.)
Then you'd have something for the done handler in the client-side code. Maybe something like this:
$('tr.someClass').html(data);
The idea here is that the tr element which is the "edit row" should be uniquely identified in some way (I'm using a class in my selector at the moment, but you can use whatever works for you), and its contents should be replaced with the contents of the partial view being returned from the server-side code.

chaining the items of Razor DropDownLists

I have 3 DropDownLists; when I select an item in #1, I want the items in #2 to be filled according the value of #1. And when I select an item in #2, the items in #3 should be filled according the selection in #2. They all are located in a form named GetItemsForLeve1. I've started by using onchange of the drop-down.
<% using (Html.BeginForm("GetItemsForLeve1", "Index"))
{ %>
Main Group: <%:Html.DropDownList("Level1", (SelectList)ViewBag.Level1, "Select One", new { onchange = "$this.form.submit()" })%>
<%:Html.DropDownList("Level2", (SelectList)ViewBag.Level2, "-", new { onchange = "$this.form.submit()" })%>
<%:Html.DropDownList("Level3", (SelectList)ViewBag.Level3, "-", new { onchange = "$this.form.submit()" })%>
<input type="submit" value="Filter" />
<%}%>
Is it possible to fill the level 2 and level 3 drop-down lists without sending the page back to the server?
How can I tell which drop-down list has been clicked in the GetItemsForLevel action?
I am completely new to MVC, so I appreciate telling me in a simple way?
Thank you
As far as I know, there's not really a component that does this for you. But you can use the Ajax.BeginForm helper to build it.
the first ajax form should contain the first select list, and post back to an action that returns a partial view
the partial view in 1. should return the second ajax form with a second select list
and so forth
So the main Razor view should contain something like this:
#using (Ajax.BeginForm("SecondAjaxForm", new AjaxOptions() { UpdateTargetId = "secondFormDiv" })
{
#Html.DropDownList("selectList1", Model.FirstSelectList, new { onchange = "this.form.submit()")
}
<!-- the second AJAX form + drop-down list will get populated here
<div id="secondFormDiv"></div>
And the SecondAjaxForm action:
public ActionResult SecondAjaxForm(string selectList1)
{
SelectList secondSelectList;
// populate the second select list here
return PartialView("SecondAjaxForm", secondSelectList);
}
The partial view SecondAjaxForm should be basically the same as the first form above.
#model SelectList
#using (Ajax.BeginForm("ThirdAjaxForm", new AjaxOptions() { UpdateTargetId = "thirdFormDiv" })
{
#Html.DropDownList("selectList2", Model, new { onchange = "this.form.submit()")
}
<!-- the third AJAX form + drop-down list (if any) will get populated here
<div id="thirdFormDiv"></div>

Dropdownlist post in ASP.NET MVC3 and Entity Framework Model

I have 3 tables:
RateProfile
RateProfileID
ProfileName
Rate
RateID
RateProfileID
PanelID
Other stuff to update
Panel
PanelID
PanelName
I have models for each of these. I have an edit page using the RateProfile model. I display the information for RateProfile and also all of the Rates associated with it. This works fine and I can update it fine. However, I also added a dropdown so that I can filter Rates by PanelID. I need it to post back on change so that it can display the filtered rates.
I'm using
#Html.DropDownList("PanelID", (SelectList)ViewData["PanelDropDown"], new { onchange = "$('#RateForm').submit()" })
for my dropdownlist. Whenever it posts back to my HttpPost Edit method though, it seems to be missing all information about the Rates navigation property. It's weird because I thought it would do exactly what the input/submit button that I have in the form does (which actually passes the entire model back to my HttpPost Edit action and does what I want it to do). The panelID is properly being passed to my HttpPost Edit method and on to the next view, but when I try to query the Model.Rates navigation property is null (only when the post comes from the dropdown. Everything works fine when the post comes from my submit input).
Get Edit:
public ActionResult Edit(int id, int panelID = 1)
{
RateProfile rateprofile = db.RateProfiles.Single(r => r.RateProfileID == id);
var panels = db.Panels;
ViewData["PanelDropDown"] = new SelectList(panels, "PanelID", "PanelName", panelID);
ViewBag.PanelID = panelID;
return View(rateprofile);
}
HttpPost Edit:
[HttpPost]
public ActionResult Edit(RateProfile rateprofile, int panelID)
{
var panels = db.Panels;
ViewData["PanelDropDown"] = new SelectList(panels, "PanelID", "PanelName", panelID);
ViewBag.PanelID = panelID;
if (ModelState.IsValid)
{
db.Entry(rateprofile).State = EntityState.Modified;
foreach (Rate dimerate in rateprofile.Rates)
{
db.Entry(dimerate).State = EntityState.Modified;
}
db.SaveChanges();
return View(rateprofile);
}
return View(rateprofile);
}
View:
#model PDR.Models.RateProfile
#using (Html.BeginForm(null,null,FormMethod.Post, new {id="RateForm"}))
{
<div>
#Html.Label("Panel")
#Html.DropDownList("PanelID", (SelectList)ViewData["PanelDropDown"], new { onchange = "$('#RateForm').submit()" })
</div>
#{var rates= Model.Rates.Where(a => a.PanelID == ViewBag.PanelID).OrderBy(a => a.minCount).ToList();}
#for (int i = 0; i < rates.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(modelItem => rates[i].RateProfileID)
#Html.HiddenFor(modelItem => rates[i].RateID)
#Html.HiddenFor(modelItem => rates[i].PanelID)
#Html.EditorFor(modelItem => rates[i].minCount)
#Html.ValidationMessageFor(model => rates[i].minCount)
</td>
<td>
#Html.EditorFor(modelItem => rates[i].maxCount)
#Html.ValidationMessageFor(model => rates[i].maxCount)
</td>
<td>
#Html.EditorFor(modelItem => rates[i].Amount)
#Html.ValidationMessageFor(model => rates[i].Amount)
</td>
</tr>
}
<input type="submit" value="Save" />
}
To summarize my problem, the below query in my view only works when the post comes from the submit button and not when it comes from my dropdownlist.
#{var rates= Model.Rates.Where(a => a.PanelID == ViewBag.PanelID).OrderBy(a => a.minCount).ToList();}
How does it look in the rendered page? You may have two elements being rendered with the same name/id:
#Html.DropDownList("PanelID", (SelectList)ViewData["PanelDropDown"], new { onchange = "$('#RateForm').submit()"
and
#Html.HiddenFor(modelItem => rates[i].PanelID)
I suggest you use Firefox/Firebug to examine the actual request that it is being made. I can't imagine that the form will post differently if triggered via the submit vs. the button, but I suppose it's possible.
On a larger note, I would make a couple of comments about your design.
I would use ViewBag properties for your selections rather than key-based access to ViewData. This will be much more readable in both your controller and your view.
Consider having a separate action that populates the information filtered by the dropdown list. Render that action in your view based on the PanelID and call that action via AJAX to get the new HTML rather than doing a full form post.
Avoid applying your handlers in your markup. Rather, add an optional Scripts section to your master page and, when needed, add scripts to your page that apply your behaviors through that section. This allows you to control via the master where view-specific scripts are placed in the document and keeps your behaviors separate from your document structure, a best practice for readability.
I found the problem. It looks like because I was only looping through a subset of my navigation property (Rates filtered by panelID), the model that was returned from the view only had that subset of navigation properties available to it. After saving the changes, I just redefined my model (called the record from the database again) and it looks good now.
Ie. There were supposed to be 140 records in my navigation property, filtering by panelID == 1 narrowed it down to 28 records. For some reason the entity framework decided that it didn't feel like maintaining the relationship to the other 112 records, so when I changed the dropdown to filter by panelID == 2, the only records available all had panelID == 1 and returned null.

how to render a full view using Ajax.BeginForm

I have a partial view which has a Ajax.BeginForm, with a UpdateTargetID set. When the validation on the form fails the update target id is replaced with the validation errors, but when there are no validation errors users should be redirected to a new page.
The code in my Partial view is
<div id="div_UID">
<% using (Ajax.BeginForm("FindChildByUID", new AjaxOptions { UpdateTargetId = "div_UID" } ))
{%>
<p>
<label>UID:</label>
<%= Html.TextBox("UID") %>
</p>
<input type="submit" value="Continue" />
<% } %>
</div>
</pre>
The code in my controller is as follows
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult FindChildByUID(Student student)
{
Student matchingStudent = _studentService.FindChildByUID(student.UID);
if (matchingStudent == null)
{
ModelState.AddModelError("UID", String.Format("No matching child found for the entered UID: {0}", student.UID));
return PartialView();
}
else
{
// full view
return RedirectToAction("ConfirmChildDetails", matchingStudent);
}
}
So, for I have been unsuccessful to display the full view on it's own, as it always seems to dipslay the full view in the UpdateTargetID div specfied in the Ajax.BeginForm.
Any suggestions on how I can get this to work?
Thanks
What your AJAX post is doing is making a request and waiting on a response that contains html to input onto the page. The configuration is such that whatever html is returned will be injected into the div you've named "div_UID".
I typically avoid scenarios like this and use traditional posting if a redirect is required upon a successful outcome of the POST.
I imagine you could do it like this using jQuery to submit rather than the Ajax.BeginForm (or just set a callback function for your Ajax.BeginForm):
function SubmitForm(form) {
$(form).ajaxSubmit({ target: "#div_to_update", success: CheckValidity });
}
function CheckValidity(responseText) {
var value = $("#did_process_succeed").val();
if (value == "True") {
window.location.replace("url_of_new_action_here");
}
}
You just have to have a hidden field in your partial view called "did_process_succeed" and set the value of True or False based on some logic in your controller.
There are likely other ways as well. Perhaps someone else will chime in. I hope this helps for now.

Append row to <TABLE> using Ajax.BeginForm()

I have an HTML <TABLE> displaying a list of items in the rows of the table. To add a new item to the list of items I have a form which submits the data to my controller via AJAX using Ajax.BeginForm. Once the action on the controller has finished it returns a partial view containing the markup for a new row to append to my table (eg. <TR><TD>.......</TD></TR>). My question is how do I add the new row my existing table?
I have create an at the top of the as my header with the id "userrightsgridheader" and specified my Ajax.BeginForm as follows:
<% using (Ajax.BeginForm(
"CreateUserRight",
new { workstationId = Model.Id },
new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.InsertAfter,
UpdateTargetId = "userrightsgridheader"
}
))
{ %>
The problem is that this does not work. Does anyone have any ideas on how to achieve this?
Thanks!
You can add the following AjaxOption, this executes 'jsfunction' when the Ajax functionality executed successfully:
new AjaxOptions { OnSuccess = "jsfunction" };
You can add the tablerow in the jsfunction.
update
you can define jsfunction as follows:
function jsfunction(ajaxContext) {
//ajaxContext contains the responseText
}
AjaxContext is defined as follows:
AjaxContext ajaxContext = new AjaxContext(request, updateElement, loadingElement, ajaxOptions.InsertionMode);

Resources