I'm having trouble displaying and saving navigation properties.
I have 3 tables:
RateProfile
- RateProfileID
- ProfileName
Rate
- RateID
- RateProfileID
- PanelID
- amount
Panel
- PanelID
- PanelName
I have models for each of these and navigation properties for foreign keys (i.e. Rates is a navigation property of RateProfile). I have an edit page with RateProfile as the model. On this page, I have a dropdown for PanelID. The dropdown filters which navigation properties are shown (each panelID has 28 rates associated with it). Whenever the dropdown is changed, it posts to my Edit method (to save the existing data before moving on and to display the rates for the new panelID selected).
I pass only the needed rates records to my view a List via the ViewBag.
I was initially using a foreach loop
foreach(PDR.Models.Rate rate in ViewBag.Rates){
@Html.HiddenFor(modelItem => rate.RateProfileID)
@Html.HiddenFor(modelItem => rate.RateID)
@Html.HiddenFor(modelItem => rate.PanelID)
@Html.EditorFor(modelItem => rate.Amount)
@Html.ValidationMessageFor(modelItem => rate.Amount)
}
With a foreach loop, everything displayed properly when changing the dropdown for PanelID, ie it would display with the correct rates for each panel. However, none of my rates would get passed along with my Model back to my HTTPPost Edit method in my controller. The RateProfile passed to my controller would have 0 records in it's Rates navigation property (there were 28 in the view).
I was advised to change the foreach loop into a for(int i...) loop. Apparently foreach loops name all of the textboxes the same thing and a regular for loop names them based on the index.
Using a for(int i...) loop, the Rates navigation property did indeed have all of it's records when passed to the HTTPPost Edit method and I was able to update those records based on what I entered.
However... now with the for(int i...) loop, the page will not display the correct records based off of panelID (it always displays records for the default panelID even when changed). What's even crazier is that if I put break points in my view, I see that the correct Rate records are being passed to the View still. I see that the correct RateID/amount etc are all being put into the Html helpers, but when the page actually displays, it displays the data that was there before changing the dropdown rather than the correct data that I was watching it populate in debug mode.
Here's the view code that am I using since switching from a foreach loop to a normal for loop:
@{List<PDR.Models.Rate> rates = ViewBag.rates;
var count = rates.Count;
}
@for (int i = 0; i < count; i++)
{
@Html.HiddenFor(modelItem => rates[i].RateProfileID)
@Html.HiddenFor(modelItem => rates[i].RateID)
@Html.HiddenFor(modelItem => rates[i].PanelID)
@Html.EditorFor(modelItem => rates[i].Amount)
@Html.ValidationMessageFor(modelItem => rates[i].Amount)
}
From checking values in debug mode in my view, I know that the correct rates are being passed to ViewBag.rates based on panelID. The problem is that even though I can step through it in debug and watch it put the correct values for the RateID,PanelID, and Amount for each rate in the above loop, what actually displays on the page is always the values for panelID == 1, and then the values passed to my controller are always the values for the rates where panelID == 1 rather than the rates that I passed to the view.
Here's the code for the edit method. I'm pretty sure the problem is in the view though, because the rateprofile (which should be the model from my View) is never getting the correct navigation properties back in the first place.
[HttpPost]
public ActionResult Edit(RateProfile rateprofile, int panelID)
{
var panels = db.Panels;
ViewBag.PanelDropDown = new SelectList(panels, "PanelID", "PanelName", panelID);
ViewBag.PanelID = panelID;
if (ModelState.IsValid)
{
db.Entry(rateprofile).State = EntityState.Modified;
foreach (Rate rate in rateprofile.Rates)
{
db.Entry(rate).State = EntityState.Modified;
}
db.SaveChanges();
}
rateprofile = db.RateProfiles.Include(a => a.Rates).Where(a => a.RateProfileID == rateprofile.RateProfileID).Single(); //reloads the current rateprofile from the db
PopulatePanelDropDown(rateprofile, panelID); //populates the ViewBag.rates variable
return View(rateprofile);
}
In summary:
foreach loop over
Rates: displays everything properly based onpanelID, but will not pass values to controller.for(int i...) loop: Will only display values for
panelID == 1and will not update based onpanelID. Will save, but only the records associated withpanelID == 1.
forloop, you could use aforeach. The trick is getting the input name attributes rendered in a way that, when sent to the server via HTTP POST, can tell the default model binder how to construct your complex viewmodel collection (aforloop helps with this but is not entirely necessary). Have a look at these links: haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx blog.stevensanderson.com/2010/01/28/… github.com/danludwig/BeginCollectionItem