Reputation: 201
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 :
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
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 Attribute
s 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