Ark-kun
Ark-kun

Reputation: 6787

Adding a child object to parent without explicit foreign key id fields with EF Code-First in ASP.Net MVC

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:

Is this possible?

Upvotes: 2

Views: 8532

Answers (5)

bipin thapa
bipin thapa

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

bipin thapa
bipin thapa

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

Vasiliy
Vasiliy

Reputation: 998

Think this should work for Create method:

public ActionResult Create([Bind(Exclude="Parent")]Child child)

Upvotes: 0

MBoros
MBoros

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

Tommy
Tommy

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

Related Questions