Getting the Child Collection for Parent Model HttpPost Update - c#

This question might be a reiteration of a previous question, if it is please post the link. Either way I'll still go through with this post.
I have this model:
public class Employee {
//omitted for brevity
public virtual ICollection<ProfessionalExperience> ProfessionalExperiences { get; set; }
public virtual ICollection<EducationalHistory> EducationalHistories { get; set; }
}
public class ProfessionalExperience {
// omitted for brevity
}
public class EducationalHistory {
// omitted for brevity
}
I'm displaying on my View with this Action:
[HttpGet]
public ActionResult Edit(int id) {
using(var context = new EPMSContext()) {
var employees = context.Employees.Include("ProfessionalExperiences").Include("EducationalHistories");
var employee = (from item in employees
where item.EmployeeId == id && item.IsDeleted == false
select item).FirstOrDefault();
return View(employee);
}
}
Here is my View:
#using(Html.BeginForm()) {
<div class="editor-label">First Name:</div>
<div class="editor-field">#Html.TextBoxFor(x => x.FirstName)</div>
<div class="editor-label">Middle Name:</div>
<div class="editor-field">#Html.TextBoxFor(x => x.MiddleName)</div>
#foreach(var item in Model.ProfessionalExperiences) {
Html.RenderPartial("ProfExpPartial", item);
}
#foreach(var item in Model.EducationalHistories) {
Html.RenderPartial("EducHistPartial", item);
}
<input type="submit" value="Save" />
}
I display the child collection on the view using a foreach and using a partial view for each collection.
When calling my Post Edit Action the employee model has the child collections set to null.
[HttpPost]
public ActionResult Edit(Employee employee) {
using(var context = new EPMSContext()) {
}
return View();
}
What am I missing to get the child collections to correctly?
Thank you!

I think the problem is related to the way MVC expects collection elements to be built (their names in the html).
Take a look at this SO answer: https://stackoverflow.com/a/6212877/1373170, especially the link to Scott Hanselman's post.
Your problem lies in the fact that if you manually iterate and do individual RenderPartial() calls, the input fields will not have indexes, and the DefaultModelBinder will not know how to construct your collection.
I would personally create Editor Templates for your two ViewModel types, and use #Html.EditorFor(model => model.EducationalHistories) and #Html.EditorFor(model => model.ProfessionalExperiences).

Related

Linking PartialView to View returns System.Web.Mvc.WebViewPage<TModel>.Model.get returned null

I looked up everywhere and tried multiple solutions but none of them would work. In my MVC website, I have a cart section and a checkout section. I want to make it so inside my checkout section, i have a small cart section to show the cart.
This is my partial view _CartItems.cshtml
#model IEnumerable<ArrowDefenseSystems.Models.Cart>
#{
ViewBag.Title = "Your Cart";
}
#if (Model != null)
{
foreach (var item in Model)
{
<div class="itemInfo row">
<img class="col-md-3" src="#Url.Content("~/Content/" + #Html.DisplayFor(modelItem => item.productImage))" height="100px">
<div class="CartItemText col-md-9">
<h3>#Html.DisplayFor(modelItem => item.productName)</h3>
<i>$#Html.DisplayFor(modelItem => item.productPrice)</i><br>
Quantity : #Html.DisplayFor(modelItem => item.quantityChosen)<br>
</div>
</div>
<hr />
}
}
When I launch the Partial View by itself, it shows the items fine and everything runs correctly.
This is how I'm linking the partial view to the view Checkout.cshtml
#model ArrowDefenseSystems.Models.ParentViewModel
...
...
#Html.Partial("_CartItems", Model.Cart)
When I run this code i get the following error on the code above:
System.NullReferenceException: 'Object reference not set to an instance of an object.'
System.Web.Mvc.WebViewPage<TModel>.Model.get returned null.
ParentViewModel:
public class ParentViewModel
{
public Checkout Checkout { get; set; }
public Cart Cart { get; set; }
}
Checkout Controller (theres more but its unnecessary):
[HttpGet]
public ActionResult _CartItems()
{
return PartialView(db.Carts.ToList());
}
public ActionResult Checkout()
{
return View();
}
I've tried many solutions but all return the same error. What am I missing?
I think there is a fundamental misunderstanding here: #Html.Partial("_CartItems", Model.Cart) will not call your _CartItems() action. Instead, it will create a new instance of the _CartItems.cshtml partial view, and use your Model.Cart parameter as it's Model.
In this case, there are two problems:
_CartItems.cshtml is expecting a model with a type of IEnumerable<ArrowDefenseSystems.Models.Cart>, whereas you are passing it a model with a type of Cart (via the Model.Cart parameter)
Model.Cart is not being instantiated in your Checkout action
So, to fix this, you should first change your ParentViewModel to be:
public class ParentViewModel
{
public Checkout Checkout { get; set; }
public IEnumerable<Cart> Carts { get; set; }
}
Then, update your Checkout action to be:
public ActionResult Checkout()
{
var viewModel = new ParentViewModel
{
Carts = db.Carts.ToList()
}
return View(viewModel);
}
And change your usage of Model.Cart to Model.Carts (due to the rename above)
After that, you can remove your _CartItems action as it is no longer used.

Hidden fields have the wrong value after first POST

I have a view on which the user can log time spent on an Activity using an HTML form. So that view loops through a list of all Activities and generates a log time form (contained in the _LogTime partial view) for each one. The only piece of information passed to the partial view from the Index view is the ActivityId, which is placed in a hidden form. The rest of the required information is provided via the from by the user.
The problem I'm having is that once I submit one of the forms, the hidden field for all of the forms is set to the ActivityId of the form I just submitted. It's worth noting that when the page first loads (before I submit any forms), the hidden fields are correct, and when I submit a form for the first time, the correct Activity gets time logged to it (and none of the others erroneously get time logged). But any form submissions after that will only log time to the Activity I first submitted the form for.
Any idea what's going on here? Why are all of the hidden fields being set to the same ActivityId? And why only after the first POST? Let me know if you need any clarification of the problem.
Models:
public class Activity
{
public int ActivityId { get; set; }
public string Name { get; set; }
}
public class UserActivity
{
public int UserId { get; set; }
public int ActivityId { get; set; }
public int Duration { get; set; }
public DateTime Date { get; set; }
}
Views:
// Index View
#foreach (Activity activity in Model)
{
#Html.Partial("_LogTime", new UserActivity(activity.ActivityId))
}
// _LogTime Partial View
#using (Html.BeginForm())
{
<fieldset>
#Html.HiddenFor(model => model.ActivityId)
#Html.EditorFor(model => model.Duration)
#Html.EditorFor(model => model.Date)
<input type="submit" value="LOG TIME" />
</fieldset>
}
Controller:
public class ActivityController : Controller
{
private readonly DbContext _db = new DbContext();
public ActionResult Index()
{
return View(_db.Activities.ToList());
}
[HttpPost]
public ActionResult Index(UserActivity activity)
{
if (ModelState.IsValid)
{
_db.UserActivities.Add(activity);
_db.SaveChanges();
}
return View(_db.Activities.ToList());
}
}
What you are experiencing is due to the fact that the html helper methods automatically update form elements with post variables of the same name. The values are stored in ModelState. One way to fix this is to remove the offending entry from ModelState.
Another possible fix is to do a redirect instead.
[HttpPost]
public ActionResult Index(UserActivity activity)
{
if (ModelState.IsValid)
{
_db.UserActivities.Add(activity);
_db.SaveChanges();
}
// Remove the ActivityId from your ModelState before returning the View.
ModelState.Remove("ActivityId")
return View(_db.Activities.ToList());
}
As witnessed by the comments below, use of the Remove method can indicate a deeper issue with the flow of your application. I do agree with Erik on that point. As he points out, redesigning the flow of an application can be a time consuming task.
When encountering the behavior indicated by the question, if there is a way to solve the problem without modifying ModelState, that would be a preferred solution. A case in point might be where more than a single element were affected by this issue.
For completeness, here is an alternate solution:
[HttpPost]
public ActionResult Index(UserActivity activity)
{
if (ModelState.IsValid)
{
_db.UserActivities.Add(activity);
_db.SaveChanges();
}
return RedirectToAction("Index");
}
Towards the end of silencing my critic, here is the rewrite that he could not come up with.
// Index View
#using (Html.BeginForm())
{
#for (var i = 0; i < Model.Count; i++)
{
<div>
#Html.HiddenFor(model => model[i].ActivityId)
#Html.EditorFor(model => model[i].Duration)
#Html.EditorFor(model => model[i].Date)
</div>
}
<input type="submit" value="LOG TIME ENTRIES" />
}
// Controller Post Method
[HttpPost]
public ActionResult Index(List<UserActivity> activities)
{
if (ModelState.IsValid)
{
foreach( var activity in activities )
{
var first = _db.UserActivities
.FirstOrDefault(row => row.ActivityId == activity.ActivityId );
if ( first == null ) {
_db.UserActivities.Add(activity);
} else {
first.Duration = activity.Duration;
first.Date = activity.Date;
}
}
_db.SaveChanges();
return RedirectToAction("index");
}
// when the ModelState is invalid, we want to
// retain posted values and display errors.
return View(_db.Activities.ToList());
}
I never use global variables in my Controller.
I rather put all my hidden values, also those in the foreach partial view, in the form.
That way, you pass the entire list and add one after that.
Now I think that you pass an empty row and add the last one to that.
To be sure, you can put a breakpoint in the post function.
#using (Html.BeginForm())
{
// Index View
#foreach (Activity activity in Model)
{
#Html.Partial("_LogTime", new UserActivity(activity.ActivityId))
}
// _LogTime Partial View
<fieldset>
#Html.HiddenFor(model => model.ActivityId)
#Html.EditorFor(model => model.Duration)
#Html.EditorFor(model => model.Date)
<input type="submit" value="LOG TIME" />
</fieldset>
}

How do I keep the clicked state of a checkbox in asp.net

I'm really having problems with keeping the state of my checkbox in my mvc4 application. I'm trying to send its value down to my controller logic, and refresh a list in my model based on the given value, before I send the model back up to the view with the new values. Given that my checkbox is a "show disabled elements in list" type function, I need it to be able to switch on and off. I've seen so many different solutions to this, but I can't seem to get them to work :(
Here's a part of my view:
#model MyProject.Models.HomeViewModel
<div class="row-fluid">
<div class="span12">
<div class="k-block">
<form action="~/Home/Index" name="refreshForm" method="POST">
<p>Include disabled units: #Html.CheckBoxFor(m => m.Refresh)</p>
<input type="submit" class="k-button" value="Refresh" />
#* KendoUI Grid code *#
</div>
</div>
HomeViewModel:
public class HomeViewModel
{
public List<UnitService.UnitType> UnitTypes { get; set; }
public bool Refresh { get; set; }
}
The HomeViewController will need some refactoring, but that will be a new task
[HttpPost]
public ActionResult Index(FormCollection formCollection, HomeViewModel model)
{
bool showDisabled = model.Refresh;
FilteredList = new List<UnitType>();
Model = new HomeViewModel();
var client = new UnitServiceClient();
var listOfUnitsFromService = client.GetListOfUnits(showDisabled);
if (!showDisabled)
{
FilteredList = listOfUnitsFromService.Where(unit => !unit.Disabled).ToList();
Model.UnitTypes = FilteredList;
return View(Model);
}
FilteredList = listOfUnitsFromService.ToList();
Model.UnitTypes = FilteredList;
return View(Model);
}
You return your Model to your view, so your Model properties will be populated, but your checkbox value is not part of your model! The solution is to do away with the FormCollection entirely and add the checkbox to your view model:
public class HomeViewModel
{
... // HomeViewModel's current properties go here
public bool Refresh { get; set; }
}
In your view:
#Html.CheckBoxFor(m => m.Refresh)
In your controller:
[HttpPost]
public ActionResult Index(HomeViewModel model)
{
/* Some logic here about model.Refresh */
return View(model);
}
As an aside, I can't see any reason why you'd want to add this value to the session as you do now (unless there's something that isn't evident in the code you've posted.

Handling MVC form submission to database

So I'm loosely following the Music Store tutorial. I created the StoreManagerController on pg. 54ish. And it created a view with the Create, Deleted, Edit, etc. It's saving some stuff to the database, namely my EditFor controls, but nothing else.
I have multiple DropDownListFor controls, populated by both tables in the database and also Active Directory user data. I'm not sure how to get these to save. Here is my abridged code. Thanks for the help.
View:
<div class="createTopInner">
<div class="editor-label">
#Html.LabelFor(model => model.test.Category)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.CategoryId, Model.CategoryItems, "")
#Html.ValidationMessageFor(model => model.test.Category)
</div>
</div>
Controller:
public ActionResult Create()
{
// These four lines get active directory users
ActiveDirectoryModel ads = new ActiveDirectoryModel();
ViewBag.assignedto = ads.FetchContacts();
ViewBag.coassignedto = ads.FetchContacts();
ViewBag.notifyto = ads.FetchContacts();
var model = Populate();
return View(model);
}
[HttpPost]
public ActionResult Create(CreateViewModel model)
{
if (ModelState.IsValid)
{
db.TestItems.AddObject(model.test);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(model);
}
public CreateViewModel Populate()
{
var model = new CreateViewModel
{
CategoryItems =
from c in new IntraEntities().CategoryItems.ToList()
select new SelectListItem
{
Text = c.Name,
Value = c.ID.ToString()
}
};
return model;
}
Model:
public class CreateViewModel
{
public Intra.Models.TestItem test{ get; set; }
public int CategoryId { get; set; }
public IEnumerable<SelectListItem> CategoryItems { get; set; }
}
The problem seems to be that, while most of your inputs map to properties on test, the CategoryId doesn't. Not knowing anything about your entity models, it's difficult to say, but I'd hazard a guess that you need to retrieve the corresponding Category from the database and add that to your TestItem instance before you persist it. If you do have a CategoryId property on your TestItem instance, you could just set it, but I'm guessing that you don't because otherwise you would have used it directly (as you do for the Category label) instead of adding a property to the view model.
If you have access to it / know much about stored procedures, it is much better to use Store procedures inside the database and then call them within Entity. It's much more loosely coupled and easier to make changes to without recompiling code.

ASP.NET MVC Multiple Checkboxes

I have a List of about 20 items I want to display to the user with a checkbox beside each one (a Available property on my ViewModel).
When the form is submitted, I want to be able to pass the value of each checkbox that is checked back to my controller method via the Selections property on my ViewModel.
How would I go about doing this using the Form Helper class in MVC? Is this even possible?
PS: I don't want a listbox where the user can just highlight multiple items.
Model:
public class MyViewModel
{
public int Id { get; set; }
public bool Available { get; set; }
}
Controller:
public class HomeController: Controller
{
public ActionResult Index()
{
var model = Enumerable.Range(1, 20).Select(x => new MyViewModel
{
Id = x
});
return View(model);
}
[HttpPost]
public ActionResult Index(IEnumerable<MyViewModel> model)
{
...
}
}
View ~/Views/Home/Index.cshtml:
#model IEnumerable<AppName.Models.MyViewModel>
#using (Html.BeginForm())
{
#Html.EditorForModel()
<input type="submit" value="OK" />
}
Editor template ~/Views/Home/EditorTemplates/MyViewModel.cshtml:
#model AppName.Models.MyViewModel
#Html.HiddenFor(x => x.Id)
#Html.CheckBoxFor(x => x.Available)
The best thing to do would be to create a template that can be reused. I have some code at home that I can post later tonight.
Maybe check SO for similar posts in the mean time.
Dynamic list of checkboxes and model binding
This blog post also could help;
http://tugberkugurlu.com/archive/how-to-handle-multiple-checkboxes-from-controller-in-asp-net-mvc-sample-app-with-new-stuff-after-mvc-3-tools-update

Categories