puntapret
puntapret

Reputation: 201

Struggling using MVC 5 and Entity Framework 6

I'm new to MVC 5 and EF 6, and i'm having lots of trouble of understanding how EF works. I'm using Database first, and used Visual Studio to create the edmx file. Please bear with me if it's long, and i'm really want to learn EF 6 with MVC 5

So in my database i have this

Book
-----
Id
Name

AttributeType (ex : Book Size, Book Content, Book Cover)
-------------
Id
Name

Attribute (ex : Pocket, Mature, Young Reader, Hard Cover, Soft Cover) (FK to AttributeType)
--------
Id
AttributeTypeId
Name

BookAttribute (FK to Book and Attribute, PK (AttributeId and BookId)
-------------
AttributeId
BookId

So using Database first, VS 2013 creates automatically my entities :

public partial class Book {
    public int Id {get;set;}
    public virtual ICollection<Attribute> Attributes { get; set; }
}

and in my DbContext

public virtual DbSet<Book> Books { get; set; }

and i added some classes

  public enum BookAttributeEnum { BookSize = 1, CoverType = 2, BookAudience = 3 }

public partial class Book {
    [NotMapped]
    [Display(Name = "BookSize", ResourceType = typeof(Resources.Resources))]
    public Attribute BookSize
    {
        get { return Attributes.FirstOrDefault(c => c.AttributeTypeId == (int) BookAttributeEnum.BookSize); }
    }

    [NotMapped]
    [Display(Name = "CoverType", ResourceType = typeof(Resources.Resources))]
    public Attribute CoverType 
    {
        get { return Attributes.FirstOrDefault(c => c.AttributeTypeId == (int)BookAttributeEnum.CoverType); }
    }

    [NotMapped]
    [Display(Name = "BookAudience", ResourceType = typeof(Resources.Resources))]
    public Attribute BookAudience
    {
        get { return Attributes.FirstOrDefault(c => c.AttributeTypeId == (int)AttributeTypeEnum.BookAudience); }
    }
}

and in my EditorTemplate for book :

    <div class="form-group">
    @Html.LabelFor(model => model.BookSize, new { @class = "control-label col-md-2" })
    <div class="col-lg-8">
        @Html.DropDownListFor(model => model.BookSize.Id, (IEnumerable<SelectListItem>)ViewData["BookSizes"], new { @class = "form-control m-bot15" })
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(model => model.CoverType, new { @class = "control-label col-md-2" })
    <div class="col-lg-8">
        @Html.DropDownListFor(model => model.CoverType.Id, (IEnumerable<SelectListItem>)ViewData["CoverTypes"], new { @class = "form-control m-bot15" })
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(model => model.BookAudience, new { @class = "control-label col-md-2" })
    <div class="col-lg-8">
        @Html.DropDownListFor(model => model.BookAudience.Id, (IEnumerable<SelectListItem>)ViewData["BookAudiences"], new { @class = "form-control m-bot15" })
    </div>
</div>

And my BookContoller

    public ActionResult Edit(int id)
    {
        var Db = new CArtEntities();

        var attributes = Db.Attributes.ToList();


        ViewData["BookSizes"] = attributes.Where(c => c.AttributeTypeId == (int)AttributeTypeEnum.BookSize).ToList()
            .ToListItems(c => c.Id.ToString(), d => d.Name, true);

        ViewData["CoverTypes"] = attributes.Where(c => c.AttributeTypeId == (int)AttributeTypeEnum.CoverType).ToList()
            .ToListItems(c => c.Id.ToString(), d => d.Name, true);

        ViewData["BookAudiences"] = attributes.Where(c => c.AttributeTypeId == (int)AttributeTypeEnum.BookAudience).ToList()
            .ToListItems(c => c.Id.ToString(), d => d.Name, true);

        var art = Db.Books
            .Include("Attributes")
            .Include("ApplicationUser")
            .First(u => u.Id == id);

        return View(art);
    }

This is the part where i can't seem find a way to update using Entity Framework

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Edit(Book model)
    {
        if (ModelState.IsValid)
        {
            var Db = new BookEntities();

            // this is not a good idea, but i don't know how to do it correctly 
            var book = Db.Books
            .Include("Attributes")
            .Include("ApplicationUser")
            .First(u => u.Id == id);

            book.Name = model.Name;
            Db.Entry(book).State = System.Data.Entity.EntityState.Modified;


            List<int> listAttributes = new List<int>();
            listAttributes.Add(Int32.Parse(Request["BookSize.Id"]));
            listAttributes.Add(Int32.Parse(Request["CoverType.Id"]));
            listAttributes.Add(Int32.Parse(Request["BookAudience.Id"]));


            for (int i = book.Attributes.Count - 1; i >= 0; i--)
            {
                Attribute at = book.Attributes.ToList()[i];

                if (!listAttributes.Contains(at.Id))
                    Db.Entry(at).State = EntityState.Deleted;
            }

            foreach (int i in listAttributes)
            {
                if (book.Attributes.FirstOrDefault(c => c.Id == i) == null)
                {
                    Attribute at = new Attribute {Id = i};
                    book.Attributes.Add(at);
                }
            }

            await Db.SaveChangesAsync();

            return RedirectToAction("Index");
        }
        return View(model);
    }

So my questions :

  1. When saving, do i need to re-get the current object that i'm saving ? Since i think i need to know which attribute has been removed and added.
  2. How can i add attribute to the book ? book.Attributes.Add(at); this line throw an error when i do SaveChangesAsync(), since it seems want to add a new Attribute (which is not correct at all, since it's already in database, and it has an id, and it throw ValidationError (Name is required)
  3. What is the best way to generating dynamically the book Attributes
    in my Editor Template ? Or did i do it correctly by creating
    property for each AttributeType ?

I hope someone can help me out, i've already searching for hours about how to do it, but failed until now.

Thanks

Upvotes: 0

Views: 937

Answers (1)

Slauma
Slauma

Reputation: 177133

About 1) Yes.

About 2) I answer this together with some other possible improvements of your Edit POST action (from top to bottom):

  • Good practice: Instantiate your context in a using block to ensure it gets disposed when your processing is finished:

    using (var Db = new BookEntities())
    {
        //...
    }
    
  • Prefer the lambda version of Include over the string-based version because you will have compile time checks if your navigation properties are correct:

    .Include(b => b.Attributes)
    .Include(b => b.ApplicationUser)
    
  • Prefer Single (or SingleOrDefault) over First if you know there can only be one entity with the given id.

  • Remove the line Db.Entry(book).State = System.Data.Entity.EntityState.Modified; completely. Your loaded book is a tracked entity and EF will recognize that by setting book.Name = model.Name; you changed the entity and that it has to be updated when you call SaveChanges.

  • I believe the for loop is wrong because with setting the state of an Attribute to Deleted EF will try to really delete the attribute from the Attributes table which - I think - is not what you want. You only want to delete the relationship between book and attribute. I would rewrite the for loop like so:

    foreach (var at in book.Attributes.ToList())
    {
        if (!listAttributes.Contains(at.Id))
            book.Attributes.Remove(at);
    }
    

    Change tracking will detect that you removed the attribute from the book and translate that to a DELETE statement in the link table BookAttributes (and not in the Attributes table!).

  • I would prefer if (!book.Attributes.Any(c => c.Id == i)) over if (book.Attributes.FirstOrDefault(c => c.Id == i) == null). It expresses the intent of the code better.

  • To avoid adding a new attribute to the Attribute table (your real question 2) attach it to the context:

    Attribute at = new Attribute {Id = i};
    Db.Attributes.Attach(at);
    book.Attributes.Add(at);
    

About 3) Seems OK to me. Some people (including myself) don't like the ViewData dictionary and prefer to put every data the view needs into a single ViewModel class and pass that as the model to the view. But it's perhaps a matter of taste and discussing that in detail would let your question explode a bit.

Upvotes: 1

Related Questions