Reputation: 6787
I have two model classes - Parent
and Child
which are only linked via typed navigational properties.
public class Parent {
[Key]
[Required]
public int Id { get; set; }
[Required]
public string ParentName { get; set; }
public virtual ICollection<Child> Children { get; set; }
}
public class Child {
[Key]
[Required]
public int Id { get; set; }
[Required]
public string ChildName { get; set; }
[Required]
public virtual Parent Parent { get; set; }
}
Now I want to create a new Child
for a parent using ASP.Net MVC. First, I need to show a view to the user. I need to somehow pass the parent object key to the view. I also want to show the ParentName. I just fetch the Parent
object from the database, create a new Child
object, set its Parent
property to the fetched parent
object.
public ActionResult Create(int parentId) {
var parent = db.Parents.Find(parentId);
if (parent == null) {
return HttpNotFound();
}
var child = new Child() { Parent = parent};
return View(child);
}
After the user fills the form, the data is sent to the Create
action using HTTP POST.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Child child)
{
if (ModelState.IsValid)
{
//db.Parents.Attach(child.Parent); //Added later
db.Children.Add(child);
db.SaveChanges();
return RedirectToAction("Index", new { parentId = child.Parent.Id });
}
}
Here I've hit my first problem. The child.Parent
was not null and child.Parent.Id
was correct, but EF trashed it and created a new empty parent (with a different key) in the database and linked the child to it. I've fixed this problem by attaching the child.Parent
to the data context before adding the child
(db.Parents.Attach(child.Parent)
).
But then I was hit with another problem. At first, my model classes were wrong and didn't have the [Required]
attributes thus creating nullable database table columns. I've added the attribute and the code stopped working. The code doesn't work because ModelState.IsValid
is false
which happens because child.Parent.Name
of the passed child
is null
.
How can the problem of adding the child to the parent be solved? I'm interested in solution which:
child.Parent
from the database just to make the model validator happy.ParentId
) to the model.Is this possible?
Upvotes: 2
Views: 8532
Reputation: 1
var dataList = db.Menus.Include(X => X.Icon).ToList();
var ViewModellist = dataList.Join(dataList,
a => a.ParentMenuId,
b => b.MenuId,
(_menu, _parent) => new MenuView
{
MenuId = _menu.MenuId,
Action = _menu.Action,
Controller = _menu.Controller,
IsGroup = _menu.IsGroup,
MenuCaption = _menu.MenuCaption,
MenuOrder = _menu.MenuOrder,
ParentMenuId = _menu.ParentMenuId,
Visibility = _menu.Visibility,
VisibilityMM = _menu.VisibilityMM,
PMenuName = _parent.MenuCaption
}).ToList();
if (PId == 0)
{
var hierarchyList = ViewModellist.Where(x => x.ParentMenuId == null).OrderBy(x => x.MenuOrder).
Select(x => new MenuView
{
MenuId = x.MenuId,
Action = x.Action,
Controller = x.Controller,
IsGroup = x.IsGroup,
MenuCaption = x.MenuCaption,
MenuOrder = x.MenuOrder,
ParentMenuId = x.ParentMenuId,
PMenuName = x.PMenuName,
Visibility = x.Visibility,
VisibilityMM = x.VisibilityMM,
ChildList = GetChildMenulist(x.MenuId, ViewModellist)
}).FirstOrDefault();
return View(hierarchyList);
}
else
{
var hierarchyList = ViewModellist.Where(x => x.MenuId == PId).OrderBy(x => x.MenuOrder).
Select(x => new MenuView
{
MenuId = x.MenuId,
Action = x.Action,
Controller = x.Controller,
IsGroup = x.IsGroup,
MenuCaption = x.MenuCaption,
MenuOrder = x.MenuOrder,
ParentMenuId = x.ParentMenuId,
PMenuName = x.PMenuName,
Visibility = x.Visibility,
VisibilityMM = x.VisibilityMM,
ChildList = GetChildMenulist(x.MenuId, ViewModellist)
}).FirstOrDefault();
return PartialView("_Index", hierarchyList);
}
Upvotes: 0
Reputation: 1
public class Menu
{
//public Menu()
//{
// {
// this.Templates = new HashSet<MenuTemplate>();
// }
//}
[Key]
public int MenuId { get; set; }
[Column("MenuCaption")]
[Display(Name = "Menu Caption")]
[StringLength(100)]
public string MenuCaption { get; set; }
[Display(Name = "Parent Menu")]
public int? ParentMenuId { get; set; }
public virtual Menu ParentMenu { get; set; }
[Display(Name = "Is Group")]
public bool IsGroup { get; set; }
[Display(Name = "Menu Order")]
public int MenuOrder { get; set; }
[Display(Name = "Visibility")]
public bool Visibility { get; set; }
[Display(Name = "Visibility Main Menu")]
public bool VisibilityMM { get; set; }
[Column("Controller")]
[Display(Name = "Controller")]
[StringLength(100)]
public string Controller { get; set; }
[Column("Action")]
[Display(Name = "Action")]
[StringLength(150)]
public string Action { get; set; }
[Display(Name = "Icon")]
public int? IconID { get; set; }
[ForeignKey("IconID")]
public virtual Icon Icon { get; set; }
public virtual ICollection<MenuTemplate> Templates { get; set; }
}
Upvotes: 0
Reputation: 998
Think this should work for Create method:
public ActionResult Create([Bind(Exclude="Parent")]Child child)
Upvotes: 0
Reputation: 1140
Here is a link to a full answer to your question. The short answer is, that when you work with disconnected entities, EF will not respect already set entity Ids, and will mark the whole entity graph as new(e.g Added).
I personally don't like it, and simply overrride the SaveChanges(), though it works as below when you have an EnityBase base class with an int(or long) Id property (which I find extremely convenient)
public override int SaveChanges()
{
this.ChangeTracker.Entries()
.Where(x => x.Entity is EntityBase && x.State == EntityState.Added && ((EntityBase) x.Entity).Id > 0)
.ForEach(x => x.State = EntityState.Unchanged);
return base.SaveChanges();
}
Upvotes: 0
Reputation: 39807
I think attempting to attach a parent to the child is a little backwards. Typically you would attach a child to a parent. A new parent is being created most likely because you are not including an input element with the parent id in your child model. So when Child child is ModelBound coming into the POST, parent id is probably null. EF sees this and thinks you want to create a new parent too.
Also, since your parentId is part of your route, you don't need to specify it in your view model unless you are doing special things to your Html.BeginForm() in your view. Meaning, if you just use Html.BeginForm, it will post with the same URL values that you sent to the GET request.
Create Method
public ActionResult Create(int parentId) {
var parent = db.Parents.Find(parentId);
if (parent == null) {
return HttpNotFound();
}
return View(new Child());
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(int parentId, Child child)
{
if (ModelState.IsValid)
{
//Probably not a bad idea to check again...just to be sure.
//Especially since we are attaching a child to the parent object anyways.
var parent = db.Parents.Find(parentId);
if (parent == null) {
return HttpNotFound();
}
parent.Childern.Add(child);
db.SaveChanges();
return RedirectToAction("Index", new { parentId = parentid });
}
}
Upvotes: 1