I am a complete novice at MVC, and can't seem to get my head around a very basic concept.
I have a parent object, that contains a collection of child objects. I want to create a new child object, and link it to the parent object, persisted in the database via EF4
public class Parent
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<Child> Children { get; set; }
}
So far, what happens in my very basic application is this. My user goes to a page displaying the details of a parent object, which includes a list of the current children. There is also a link on that page to add a new child. That link points to a Create action on the child Controller, passing the parent Id, which in turn displays a view to create a new child. And this is where I've got stuck. I don't know how to persist the supplied parent Id so that when I click Save, I can retrieve that parent object and add my new child object to its collection.
I'm probably approaching this in completely the wrong way, but I can't seem to find any basic tutorials on how to add new child items to a parent, which is odd considering how common a scenario it is.
Any help is really appreciated!
Many thanks
Gerry
[Update 1]
I have two CreateChild actions, initially generated by MVC and then modified by myself. I can see just by looking at them that they don't do what I need, but I'm not at all sure how they need to change. Specifically, I store the parent ID within the ViewBag but between the calls to the Create actions, it gets lost, and so is not available when the second Create is called to persist the new child item to the database.
public ActionResult Create(int parentId)
{
Parent parent = db.Parents.Find(parentId);
ViewBag.ParentId = parent.Id;
return View();
}
[HttpPost]
public ActionResult Create(Child child)
{
if (ModelState.IsValid)
{
Parent parent = db.Parents.Find(ViewBag.ParentId);
parent.Children.Add(child);
db.Children.Add(child);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(child);
}
Thanks again
Gerry
When you pass the ParentId to the add child action, it looks like you are doing it with a route parameter.
Instead of storing it in the ViewBag, write it as a hidden field in your child form. Then, when someone submits the form, the ParentId will be submitted to your HttpPost action method.
You can do this by making ParentId a property on your Child viewmodel.
public class Child
{
public int ParentId { get; set; }
}
public ActionResult Create(int parentId)
{
Parent parent = db.Parents.Find(parentId);
var model = new Child { ParentId = parent.Id };
return View(model);
}
In your view, render it like this:
#Html.HiddenFor(m => m.ParentId)
Then, during your HttpPost, Child will already contain ParentId -- the default model binder will get it from the hidden field on your form.
[HttpPost]
public ActionResult Create(Child child)
{
if (ModelState.IsValid)
{
Parent parent = db.Parents.Find(child.ParentId);
parent.Children.Add(child);
db.Children.Add(child); // don't think you need this line
db.SaveChanges();
return RedirectToAction("Index");
}
return View(child);
}
Update after posting answer
Looking at your HttpPost code, it may be incorrect to add the child to the db twice. If you are using EF 4.1 or 4.2, I am pretty sure this is incorrect, but I'm not sure about previous EF versions. Adding the child to the parent.Children collection should be enough -- I don't think you should also add it to the db.Children set.
ViewBag is not ViewState (ASP.NET MVC doesn't have any built-in equivalent to ASP.NET WebForms ViewState) - it will not keep ParentId between calls. It will just allow you passing ParentId to view (in your first action) so you can for example store it in hidden field.
Related
I'm quite new in MVC and can't get my head around a possible overposting threath. I have an "Event" model which contains an Id property. When a user, for example, wants to edit an existing "Event", I use this property for fetching the "Event" I need to update from a collection of "Events".
I tried to decorate the Id property with the [BindNever] or [Editable] attribute which results in an Id property of 0 as the property no longer binds after a post. This, of course, generates problems when I want to use this Id property for fetching an "Event" from the collection.
So I leave the property undecorated. But it feels unnatural as this property should not be editable by a user. Using a ViewModel does not solve my problem as an Id property would still be needed.
In all examples I find online, an Id property is always part of a binding model. Does this pose threats to possible overposting? I assume not as, when working with Entity Framework, for example, the Id property is not Editable as it is autoincremented. A user would still be able to change the Id in order to update another "Event" though but in the case of my application, this generates no issue as a user is able to edit any "event" he wants
Model:
public class Event
{
public int Id { get; set; }
public string Name { get; set; }
}
Controller:
[HttpGet]
public IActionResult EditEvent(int? id)
{
if (id == null)
{
return NotFound();
}
else
{
var eventToEdit = _events.GetEvent(id.Value);
return View(eventToEdit);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult EditEvent(Event postedEvent)
{
if (ModelState.IsValid)
{
if (postedEvent == null)
{
return NotFound();
}
else
{
var eventToUpdate = _events.GetEvent(postedEvent.Id);
eventToUpdate = _events.EditEvent(postedEvent, eventToUpdate);
return RedirectToAction(nameof(EventDetails), new { id = eventToUpdate.Id });
}
}
else
{
return View(postedEvent);
}
}
Does ID property in binding model pose a threat to overposting?
Yes, if you're using this id as a lookup for editing then you need to write a check that ensures that the user is allowed to perform the edit on the data.
public IActionResult EditEvent(Event postedEvent)
{
//Make sure the current user can edit the posted event
if(!CanUserEditEvent(postedEvent.Id, User.GetUserId()) return Forbid();
//User can edit this event so continue normally
if(ModelState.IsValid)
...
}
Furthermore, ensure that other "read-only" or unused properties of your model that are exposed on the view and action are not then used as a lookup or saved. A view model can help with this by allowing you to define a subset of your domain model properties.
So.... I have an action in my controller that basically copies the current model and returns a new view based on that copy. To inform the user that this is a copy I append a message via the viewbag stating that it's a copy. All seemed to be working until I noticed that it's not the copy that is being used when rendering the new view instead it's the original, but the viewbag on the other hand is updated so the message is shown.
Hmm, don't know if that's understandable so I'll try to show what i mean with some pseudo-code as well:
Model
public class Model{
[ScaffoldColumn(false)]
[HiddenInput(DisplayValue = false)]
public Guid Id { get; set; }
public string Name { get; set; }
}
View
<input type="submit" name="Copy" value="#_("Copy")"/>
Controller
public ActionResult Copy(model) {
ViewBag.Message = _("This is a copy.");
var clone = model.Clone();
return View("Index", clone);
}
I'm having a real hard time trying to wrap my head around this so any help/tips/pointers are really appreciated.
Oh, I've stepped through the code several times to ensure that the clone is really a clone. The only thing that differentiates them is the Id property and that is the new one in the controller but when the view renders it's back to the old one.
You need to clear the ModelState collection before returning the clone because the HtmlHelpers prefer to reuse the posted data:
public ActionResult Copy(model) {
ViewBag.Message = _("This is a copy.");
var clone = model.Clone();
ModelState.Clear();
return View("Index", clone);
}
You can read more about this feature in this artice: ASP.NET MVC Postbacks and HtmlHelper Controls ignoring Model Changes.
I have an action that creates a List and returns it to my view..
public ActionResult GetCustomers()
{
return PartialView("~/Views/Shared/DisplayTemplates/Customers.cshtml", UserQueries.GetCustomers(SiteInfo.Current.Id));
}
And in the "~/Views/Shared/DisplayTemplates/Customers.cshtml" view I have the following:
#model IEnumerable<FishEye.Models.CustomerModel>
#Html.DisplayForModel("Customer")
Then I have in the "~/Views/Shared/DisplayTemplates/Customer.cshtml" view:
#model FishEye.Models.CustomerModel
#Model.Profile.FirstName
I am getting the error:
The model item passed into the dictionary is of type System.Collections.Generic.List`1[Models.CustomerModel]', but this dictionary requires a model item of type 'Models.CustomerModel'.
Shouldn't it display the Customer.cshtml for every item in the collection in the Customers.cshtml?
Help!
I am not sure why you are calling a partial view like this. If it is a Customer Specific view, why not put it under Views/Customer folder ? Remember ASP.NET MVC is more of Conventions. so i would always stick with the conventions (unless abosultely necessary to configure myself) to keep it simple.
To handle this situation, i would do it in this way,
a Customer and CustomerList model/Videmodel
public class CustomerList
{
public List<Customer> Customers { get; set; }
//Other Properties as you wish also
}
public class Customer
{
public string Name { get; set; }
}
And in the action method, i would return an object of CustomerList class
CustomerList customerList = new CustomerList();
customerList.Customers = new List<Customer>();
customerList.Customers.Add(new Customer { Name = "Malibu" });
// you may replace the above manual adding with a db call.
return View("CustomerList", customerList);
Now there should be a view called CustomerList.cshtml under Views/YourControllerName/ folder. That view should look like this
#model CustomerList
<p>List of Customers</p>
#Html.DisplayFor(x=>x.Customers)
Have a view called Customer.cshtml under Views/Shared/DisplayTemplates with this content
#model Customer
<h2>#Model.Name</h2>
This will give you the desired output.
Your view is expecting a single model:
#model FishEye.Models.CustomerModel // <--- Just one of me
You're passing it an anonymous List:
... , UserQueries.GetCustomers(SiteInfo.Current.Id) // <--- Many of me
You should change your view to accept the List or determine which item in the list is supposed to be used before passing it into the View. Keep in mind, a list with 1 item is still a list and the View is not allowed to guess.
I'm using the Entity Framework version 4.2. There are two classes in my small test app:
public class TestParent
{
public int TestParentID { get; set; }
public string Name { get; set; }
public string Comment { get; set; }
public virtual ICollection<TestChild> TestChildren { get; set; }
}
public class TestChild
{
public int TestChildID { get; set; }
public int TestParentID { get; set; }
public string Name { get; set; }
public string Comment { get; set; }
public virtual TestParent TestParent { get; set; }
}
Populating objects with data from the database works well. So I can use testParent.TestChildren.OrderBy(tc => tc.Name).First().Name etc. in my code.
Then I built a standard edit form for my testParents. The controller look like this:
public class TestController : Controller
{
private EFDbTestParentRepository testParentRepository = new EFDbTestParentRepository();
private EFDbTestChildRepository testChildRepository = new EFDbTestChildRepository();
public ActionResult ListParents()
{
return View(testParentRepository.TestParents);
}
public ViewResult EditParent(int testParentID)
{
return View(testParentRepository.TestParents.First(tp => tp.TestParentID == testParentID));
}
[HttpPost]
public ActionResult EditParent(TestParent testParent)
{
if (ModelState.IsValid)
{
testParentRepository.SaveTestParent(testParent);
TempData["message"] = string.Format("Changes to test parents have been saved: {0} (ID = {1})",
testParent.Name,
testParent.TestParentID);
return RedirectToAction("ListParents");
}
// something wrong with the data values
return View(testParent);
}
}
When the form is posted back to the server the model binding appears to be working well - i.e. testParent looks okay (id, name and comment set as expected). However the navigation property TestChildren remains at NULL.
This I guess is not sooo surprising since the model binding merely extracts the form values as they were sent from the browser and pushes them into an object of the TestParent class. Populating testParent.TestChildren however requires an immediate roundtrip to the database which is the responsibility of the Entity Framework. And EF probably doesn't get involved in the binding process.
I was however expecting the lazy loading to kick in when I call testParent.TestChildren.First(). Instead that leads to an ArgumentNullException.
Is it necessary to tag an object in a special way after model binding so that the Entity Framework will do lazy loading? How can I achieve this?
Obviously I could manually retrieve the children with the second repository testChildRepository. But that (a) doesn't feel right and (b) leads to problems with the way my repositories are set up (each using their own DBContext - which is an issue that I haven't managed to come to terms with yet).
In order to get lazy loading for your child collection two requirements must be fulfilled:
The parent entity must be attached to an EF context
Your parent entity must be a lazy loading proxy
Both requirements are met if you load the parent entity from the database through a context (and your navigation properties are virtual to allow proxy creation).
If you don't load the entity from the database but create it manually you can achieve the same by using the appropriate EF functions:
var parent = context.TestParents.Create();
parent.TestParentID = 1;
context.TestParents.Attach(parent);
Using Create and not new is important here because it creates the required lazy loading proxy. You can then access the child collection and the children of parent with ID = 1 will be loaded lazily:
var children = parent.TestChildren; // no NullReferenceException
Now, the default modelbinder has no clue about those specific EF functions and will simply instantiate the parent with new and also doesn't attach it to any context. Both requirements are not fulfilled and lazy loading cannot work.
You could write your own model binder to create the instance with Create() but this is probably the worst solution as it would make your view layer very EF dependent.
If you need the child collection after model binding I would in this case load it via explicit loading:
// parent comes as parameter from POST action method
context.TestParents.Attach(parent);
context.Entry(parent).Collection(p => p.TestChildren).Load();
If your context and EF is hidden behind a repository you will need a new repository method for this, like:
void LoadNavigationCollection<TElement>(T entity,
Expression<Func<T, ICollection<TElement>>> navigationProperty)
where TElement : class
{
_context.Set<T>().Attach(entity);
_context.Entry(entity).Collection(navigationProperty).Load();
}
...where _context is a member of the repository class.
But the better way, as Darin mentioned, is to bind ViewModels and then map them to your entities as needed. Then you would have the option to instantiate the entities with Create().
One possibility is to use hidden fields inside the form that will store the values of the child collection:
#model TestParent
#using (Html.BegniForm())
{
... some input fields of the parent
#Html.EditorFor(x => x.TestChildren)
<button type="submit">OK</button>
}
and then have an editor template for the children containing the hidden fields (~/Views/Shared/EditorTemplates/TestChild.cshtml):
#model TestChild
#Html.HiddenFor(x => x.TestChildID)
#Html.HiddenFor(x => x.Name)
...
But since you are not following good practices here and are directly passing your domain models to the view instead of using view models you will have a problem with the recursive relationship you have between the children and parents. You might need to manually populate the parent for each children.
But a better way would be to query your database in the POST action and fetch the children that are associated to the given parent since the user cannot edit the children inside the view anyway.
I using a Telerik MVC Grid and configured it for Batch Mode Editing http://demos.telerik.com/aspnet-mvc/grid/editingbatch. I am trying to edit one of my entity "State" which has List of Cities, where City is another entity. Here is how the State Entity looks.
public class State {
...Some Scalar Properties
public virtual List<City> Cities { get; set; } //Navigation Property
public State() {
Cities = new List<City>();
}
}
My City Entity points back to State as given below.
public class City {
... Some Scalar Properties
public virtual State State { get; set; } //Navigation property
}
I am using this Model in one of my cshtml pages some thing like this
#(Html.Telerik().Grid<State>()
.Name("tlkStateGrid")
.Editable(e => e.Mode(GridEditMode.InCell).DisplayDeleteConfirmation(false))
.ToolBar(t => {
t.Insert().ButtonType(GridButtonType.Image);
t.SubmitChanges().ButtonType(GridButtonType.Image);
})
...Some More of code here.
In my Controller I am handling the batch updates in normal way.
public ActionResult _SaveChanges(IEnumerable<State> inserted, IEnumerable<State> updated, IEnumerable<State> deleted) {
.....
}
When I try to edit State entity using batching editing of Telerik Grid, the (IEnumerable updated) parameter of the above controller action has entries for all the States that have been modified. The States however have a Cities List with one city (which is null) even if there aren't any Cities in the State.
So the point is that I have not created any City in any part of my code, but when I receive the States as parameter to the controller action listed above, there is a null City sitting inside the Cities List. Why does this happen?
I'm not entirely sure I understand the problem. So when the grid posts, you create a new State. Attached to that State object, there's a null City object. Is that the issue? Or did I miss something?
If that's the issue, it's normal behavior and should be expected. That is how the automatic JSON de-serialization in MVC3 works--any time you create a parent object and don't define the nested object, the nested object will be returned as null. Just handle the nulls in your code.