Updating Navigation Properties in DbContext EF 4.1 - asp.net-mvc

After researching all day and night, I have something that is currently working. However, I am not sure I really understand what's going on with navigation properties and entity relationships, so I'm concerned that my code might cause problems in the future. I had to manually set the navigation properties to "EntityState.Modified". My model may eventually have many levels of navigation objects and collections. Is there an easier way to update the related entities? If there is no easier way, is this approach okay?
Here is the view model
public class ViewModel {
public ViewModel() { }
public ViewModel(Context context) {
this.Options = new SelectList(context.Options, "Id", "Name");
}
public Parent Parent { get; set; }
public SelectList Options { get; set; }
}
entity classes
public class Parent {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ChildOne ChildOne { get; set; }
public virtual ChildTwo ChildTwo { get; set; }
}
public class ChildOne {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Parent Parent { get; set; }
public virtual int OptionId { get; set; }
public virtual Option Option { get; set; }
}
public class ChildTwo {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Parent Parent { get; set; }
public virtual int OptionId { get; set; }
public virtual Option Option { get; set; }
}
public class Option {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ChildOne> ChildrenOnes { get; set; }
public virtual ICollection<ChildTwo> ChildrenTwos { get; set; }
}
context
public class Context : DbContext {
public DbSet<Parent> Parents { get; set; }
public DbSet<ChildOne> ChildrenOnes { get; set; }
public DbSet<ChildTwo> ChildrenTwos { get; set; }
public DbSet<Option> Options { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Parent>()
.HasOptional(x => x.ChildOne)
.WithOptionalPrincipal(x => x.Parent);
modelBuilder.Entity<Parent>()
.HasOptional(x => x.ChildTwo)
.WithOptionalPrincipal(x => x.Parent);
}
}
controller
private Context db = new Context();
public ActionResult Edit() {
ViewModel viewmodel = new ViewModel(db);
viewmodel.Parent = db.Parents.Find(1);
return View(viewmodel);
}
public void Save(Parent parent) {
if (ModelState.IsValid) {
db.Entry(parent).State = EntityState.Modified;
db.Entry(parent.ChildOne).State = EntityState.Modified;
db.Entry(parent.ChildTwo).State = EntityState.Modified;
db.SaveChanges();
}
}
and view
#model MvcApp7.Models.ViewModel
<div id="Parent">
#Html.HiddenFor(model => model.Parent.Id)
#Html.TextBoxFor(model => model.Parent.Name)
<div id="ChildOne">
#Html.HiddenFor(model => model.Parent.ChildOne.Id)
#Html.TextBoxFor(model => model.Parent.ChildOne.Name)
#Html.DropDownListFor(model => model.Parent.ChildOne.OptionId, Model.Options)
</div>
<div id="ChildTwo">
#Html.HiddenFor(model => model.Parent.ChildTwo.Id)
#Html.TextBoxFor(model => model.Parent.ChildTwo.Name)
#Html.DropDownListFor(model => model.Parent.ChildTwo.OptionId, Model.Options)
</div>
</div>
<input id="SaveButton" type="button" value="save" />
<script type="text/javascript">
$('#SaveButton').click(function () {
var data = $('input, select, textarea').serialize();
$.post('#Url.Action("Save")', data, function () { });
});
</script>

Yes you are doing it right. When working with detached entities like in web application you must tell EF exactly what state each entity has. In your scenario you will first call:
db.Entry(parent).State = EntityState.Modified;
In EFv4.1 this operation causes that parent is attached to context. Only attached entities can be persisted to database when SaveChanges is called. The state of the entity is set to modified so context will try to update existing record in the database when persisting the entity. There is one more important thing which happened when you called that statement: All related entities are attached as well but their state is set to unchanged. You must manually set correct state of all related entities because EF doesn't know which one is new, modified or deleted. That is why your following calls are correct as well.
Edit:
Be aware that this updates values in parent and child but it doesn't update relation itself. If you swap one child with another the process is much more complicated when using independent associations.

Related

MVC4 Entity Framework -- How do I display One-to-Many Relationships

I have a class with with one-to-many relationship and trying to display the virtual object in the view. I am fairly new to MVC and EF so this maybe something simple.
I am trying to display a drop down list for PODObject on "Request/Create" action view.
Here are the models
public class PODObject
{
public int PODObjectID { set; get; }
public string name { set; get; }
public string CustomAttribute1 { set; get; }
public string CustomAttribute2 { set; get; }
public string CustomAttribute3 { set; get; }
public string CustomAttribute4 { set; get; }
public string CustomAttribute5 { set; get; }
public virtual ICollection<BookingObject> BookingObjectspublic { set; get; }
}
public class RequestsQueueObject
{
// Request Attributes
public int RequestsQueueObjectID { set; get; }
public string CustomAttribute1 { set; get; }
public string CustomAttribute2 { set; get; }
public string CustomAttribute3 { set; get; }
public string CustomAttribute4 { set; get; }
public string CustomAttribute5 { set; get; }
public int PODObjectID { set; get; }
public virtual PODObject PODObject { set; get; }
}
Here is the controller Create action
public ActionResult Create([Bind(Include="RequestsQueueObjectID,POCPODAccess,CustomAttribute1,CustomAttribute2,CustomAttribute3,CustomAttribute4,CustomAttribute5")] RequestsQueueObject requestsqueueobject)
{
if (ModelState.IsValid)
{
db.RequestsQueueObjects.Add(requestsqueueobject);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(requestsqueueobject);
}
Here is the cshtml
<div class="form-group">
#Html.DropDownListFor(model => model.PODObject, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.PODObject)
#Html.ValidationMessageFor(model => model.PODObject)
</div>
</div>
Maybe I am wrong but your PODOObject doesn't look like a List, an Array or whatever, so you can't really use it as a source for your dropdown...
I think what you're looking for is being able to pick from a list of existing PODObject instances to set as the related item for RequestQueueObject. You've got a few issues you need to clear up for that.
You have a property on RequestQueueObject to hold the id for the relationship with PODObject, but this currently isn't being used. Entity Framework's convention is to look for an property with the naming pattern [RelatedProperty][RelatedClassIdProperty]. In your scenario, that would be PODObjectPODObjectID or PODObject_PODObjectID. You can clear that up by adding the ForeignKey attribute to your PODObjectID property:
[ForeignKey("PODObject")]
public int PODObjectID { get; set; }
Html.DropDownListFor requires an IEnumerable<SelectListItem> for its second parameter, which you don't currently have. MVC won't do this for you; you need to create the list of choices yourself. Ideally, you would use a view model with this as a property, but for the time being, you can use ViewBag:
In your action
var podObjectChoices = db.PODObjects.Select(m => new SelectListItem
{
Value = m.PODObjectID,
Text = m.name
});
ViewBag.PODObjectChoices = podObjectChoices.ToList();
return View();
In your view
#Html.DropDownListFor(m => m.PODObjectID, (IEnumerable<SelectListItem>)ViewBag.PODOBjectChoices, new { #class = "control-label col-md-2" })
I pretty much just covered this in the sample code above, but you can't directly set PODOBject via a drop down list. Instead, you set the foreign key property for the relationship, PODOBjectID, and Entity Framework will wire it up.

EF Code First - Populating data in Many to Many

I'm new to ASP.Net MVC and want to create a simple Blog project, therefore I have two entity posts and categories. each post can belong to many categories and each category can belong to many posts.
Models.cs
public class Category
{
[Key]
public int CategoryId { get; set; }
public int? ParentId { get; set; }
public virtual Category Parent { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public virtual ICollection<News> News { get; set; }
public Category()
{
News = new List<News>();
}
}
public class News
{
[Key]
public int NewsId { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public string Content { get; set; }
public string Source { get; set; }
public string SourceURL { get; set; }
public string Images { get; set; }
public string Password { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public DateTime? DeletedAt { get; set; }
public string CreatedBy { get; set; }
public string DeletedBy { get; set; }
public virtual PublishPeriod PublishPeriodId { get; set; }
public virtual ICollection<Category> Categories { get; set; }
public News()
{
Categories = new List<Category>();
}
}
ModelsMap.cs
public class CategoryMap:EntityTypeConfiguration<Category>
{
public CategoryMap()
{
Property(one => one.Title).HasMaxLength(100).IsRequired();
HasOptional(x => x.Parent).WithMany().HasForeignKey(x => x.ParentId);
}
}
public class NewsMap:EntityTypeConfiguration<News>
{
public NewsMap()
{
Property(x => x.CreatedBy).HasMaxLength(150);
Property(x => x.DeletedBy).HasMaxLength(150);
Property(x => x.Title).IsRequired().HasMaxLength(150);
Property(x => x.Summary).IsRequired();
Property(x => x.Content).IsRequired().HasColumnType("ntext");
Property(x => x.CreatedAt).HasColumnType("datetime");
Property(x => x.Password).IsOptional().HasMaxLength(128);
Property(x => x.DeletedAt).IsOptional();
Property(x => x.ModifiedAt).IsOptional();
HasMany(x => x.Categories).WithMany(x => x.News).Map(x =>
{
x.ToTable("NewsCategories");
x.MapLeftKey("News_NewsId");
x.MapRightKey("Category_CategoryId");
});
}
}
And DB Context
public DbSet<Category> Categories { get; set; }
public DbSet<News> News { get; set; }
public DbSet<PublishPeriod> PublishPeriod { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.Add(new CategoryMap());
modelBuilder.Configurations.Add(new NewsMap());
modelBuilder.Configurations.Add(new PublishPeriodMap());
I have a create view for posts that displays categories in a list with checkboxs and each checkbox value is category's ID. How can I insert or update posts and keep relation between post and categories.
NewsController
//
// POST: /Admin/News/Create
[HttpPost]
public ActionResult Create(News news, List<string> Category)
{
ViewBag.Categories = catRepository.All.OrderBy(x => x.Title);
if (ModelState.IsValid)
{
foreach (var item in Category)
{
news.AddCategory(catRepository.Find(int.Parse(item)));
}
news.CreatedAt = DateTime.Now;
news.CreatedBy = "M.Hesabi";
newsRepository.InsertOrUpdate(news);
newsRepository.Save();
return RedirectToAction("Index");
}
else
{
return View();
}
}
UPDATE: I created a method in News Model as #DanS said and edited my controller.
I'd recommend creating a method on the News class:
public void AddCategory(Category category) {
Categories.Add(category);
category.News.Add(this);
}
From your Controller you can then add each selected Category to the News instance, and then add the News to the DbContext prior to calling SaveChanges. This may depend, however, on how your repositories make use of the context -- in that if they open their own, instead of accessing a shared context, you might have to attach the categories to the News repository's context prior to saving. Hopefully this helps...
Update
IEntityChangeTracker error:
It appears as if MVCScaffolding uses a separate context for each repository. As mentioned, having separate contexts can lead to some additional required steps. As it stands now, your categories are tracked by Context A while your news is tracked by Context B-- You could detach/attach the category entities between the two contexts, but I'd say the recommended solution would be to change your repositories to accept a shared context through their constructors.
I'm assuming that you are instantiating the repositories in the controller's constructor, rather than using dependency injection, so you would modify your constructor code to do something like the following:
myContext = new YourContextClass();
catRepository = new CategoryRepository(myContext);
newsRepository = new NewsRepository(myContext);
You would then have to add the constructors to your repositories to assign the internal context property, and finally, adjust your controller to properly dispose of the context.
protected override void Dispose(bool disposing)
{
if (disposing)
myContext.Dispose();
base.Dispose(disposing);
}

MVC Multiple tables one model

I am having difficulty with my understanding of MVC coming from an aspx world.
I have a Model called CustomerGarment. This has a Order and a Customer along with a few garments.
public class CustomerGarment
{
public int CustomerGarmentId { get; set; }
public virtual Customer Customer { get; set; }
public virtual Order Order { get; set; }
public virtual GarmentJacket GarmentJacket { get; set; }
public virtual GarmentShirt GarmentShirt { get; set; }
}
I have a method for get and post. When the page loads, it creates a new CustomerGarment instance and querys the database to fill the Customer and Order variables. I then use the viewbag to show on the screen a list of GarmentJackets and GarmentShirts
The page then views and using the view I can access the model perfectly. Drop downs load with the viewbag contents and I can access all Customer and Order variables using the model I have passed.
The problem I then face is when I use the HttpPost. The model is not passed back with the information I passed to it.
public ActionResult AddGarments(int orderId, int customerId)
{
CustomerGarment cg = new CustomerGarment();
cg.Order = (from a in db.Orders where a.OrderId == orderId select a).FirstOrDefault();
cg.Customer = (from a in db.Customers where a.CustomerId == customerId select a).FirstOrDefault();
var jackets = from a in db.GarmentJackets orderby a.Type, a.SleeveLengthInches, a.ChestSizeInches select a;
var shirts= from a in db.GarmentKilts orderby a.PrimarySize, a.DropLength select a;
ViewBag.GarmentJacket = new SelectList(jackets, "GarmentJacketId", "GarmentJacketId");
ViewBag.GarmentShirt = new SelectList(shirts, "GarmentShirtId", "GarmentShirtId");
return View(cg);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddGarments(CustomerGarment cg)
{
// Here, I do not have the customer info for example
db.CustomerGarments.Add(cg);
db.SaveChanges();
return RedirectToAction("Index");
return View(cg);
}
This is a bit of my view
#Html.HiddenFor(model => model.Order.OrderId)
#Html.HiddenFor(model => model.Order.CustomerId)
<div class="display-field">
#Html.DisplayFor(model => model.Customer.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.GarmentJacket, "Jacket")
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.GarmentJacket, (SelectList)ViewBag.GarmentJacket, new {style="width:312px;height:30px;margin-top:2px;margin-bottom:5px"})
</div>
EDIT
My Garment Jacket Model
public class GarmentJacket : Garment
{
public int GarmentJacketId { get; set; }
[Required]
public string Type { get; set; }
[Required]
[Display(Name = "Chest Size")]
public int ChestSizeInches { get; set; }
[Required]
[Display(Name = "Sleeve Length")]
public int SleeveLengthInches { get; set; }
}
public class Garment
{
[DataType(DataType.Date)]
public DateTime? DateRetired { get; set; }
[Required]
public string Barcode { get; set; }
[Required]
public bool Adults { get; set; }
}
In your CustomerGarment class, you should have:
public class CustomerGarment
{
public int CustomerGarmentId { get; set; }
public int CustomerId { get; set; }
public int OrderId { get; set; }
public int GarmentJacketId { get; set; }
public int GarmentShirtId { get; set; }
public virtual Customer Customer { get; set; }
public virtual Order Order { get; set; }
public virtual GarmentJacket GarmentJacket { get; set; }
public virtual GarmentShirt GarmentShirt { get; set; }
}
And, then, in your View, your DropDownList will look like:
#Html.DropDownListFor(m => m.GarmentJacketId, (SelectList)ViewBag.GarmentJacket, new {style="width:312px;height:30px;margin-top:2px;margin-bottom:5px"})
Your DropDownList only posts one value, which is the GarmentJacketId. You can't bind that Id to the whole GarmentJacket class.
By the way, you also need to replace your hidden inputs with these:
#Html.HiddenFor(model => model.OrderId)
#Html.HiddenFor(model => model.CustomerId)
I think I know your problem. As you suggested in you comment above you need to post everything you want retained in the view. This is one of the differences beteween webforms and MVC, webforms has viewstate that could contain information that you don't explicitly add to the view and post back, giving the impression of state. In MVC you have to add it to the view.
On the other hand you don't need to pass in more information than you need either. You pass inn the customerId as a hidden field. On post method you get the customer from the db using the Id, then you add the order to the customer.
I have some questions about your design, but given that a customer holds a collection of Orders, you could do something like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddGarments(CustomerGarment cg)
{
// Get the customer from the database
var customer = db.Customers.Find(c=>c.id==cb.Customer.Id)
var order = new Order();
//Create your order here using information from CustomerGarment model
//If the model already holds a valid Order object then just add it.
//i.e. you could get a Garment object from the DB using the GarmentId from
//the ViewModel if you really need more than just the Id to create the order
customer.Orders.Add(order);
db.SaveChanges();
return RedirectToAction("Index");
}

populating list in view using editorTemplates

here is my model
namespace chPayroll.Models.CustInformations
{
public class CustContact
{
public int cId { get; set; }
public int cNoType { get; set; }
public string cNo1 { get; set; }
public string cNo2 { get; set; }
public string cNo3 { get; set; }
public List<CustContact> contact { get; set; }
}
}
here is my editorTemplates
#model chPayroll.Models.CustInformations.CustContact
#Html.TextBoxFor(model => model.cNo1)
#Html.TextBoxFor(model => model.cNo2)
#Html.TextBoxFor(model => model.cNo3)
I need to show three textbox for taking email,three text box for taking telephone no. in view. how can I add the items to the list contact defined in the model so that it shows like this
email:--textbox1----textbox2----textbox3--
telephone:--textbox1----textbox2----textbox3--
and sends value to the controller
actually I am trying to send my data in list named contact here ie inside list at
index 0-email1-email2-email3
index 1-tel1-tel2-tel3
#Sanjay: you have a strange construct in your view model:
public class CustContact
{
public List<CustContact> contact;
}
Even if it compiles and machine understands it, I wouldn't use it as it is - you trying to lift yourself from the ground by pulling your hair up :)
It should be defined something along these lines (following your naming conventions & logic):
public class CustContact // single
{
public int cId { get; set; }
public int cNoType { get; set; }
public string cNo1 { get; set; } // those are actual phones, emails etc data
public string cNo2 { get; set; }
public string cNo3 { get; set; }
}
public class CustContacts // plural
{
public List<CustContact> Contacts;
}
View:
#model CustContacts
#EditorFor(m => Model)
Editor template:
#model CustContact
#Html.EditorFor(m => m.cNo1)
#Html.EditorFor(m => m.cNo2)
#Html.EditorFor(m => m.cNo3)
For brevity, we don't deal here with annotations, decorations, error handling etc.
Hope this helps.
Based on the comment on the question, I would build the models as below
public class CustContact
{
public int cId { get; set; }
public int cNoType { get; set; }
public string cNo1 { get; set; }
public string cNo2 { get; set; }
public string cNo3 { get; set; }
}
public class Customer
{
public CustContact Email {get; set;}
public CustContact Telephone {get; set;}
}
then create an editor template for Customer and in that editor template have following logic
#Html.EditorFor(model => model.Email)
#Html.EditorFor(model => model.Telephone)
Hope this helps

How can I use DropDownListFor() on a dynamic type?

I get the following error in the DropDownListFor() "An expression tree may not contain a dynamic operation" because the lambda uses a dynamic type.
How can I set the selected option on the DropDownList without resorting to jQuery?
I would also rather not make a template or custom helper.
Model
public class Thing : Base {
public virtual Nullable<int> OptionID { get; set; }
public virtual Option Option { get; set; }
}
public class Option : Base {
public virtual ICollection<Thing> Things { get; set; }
}
public class Base {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
Controller
public ActionResult Edit(int Id) {
return View(new ViewModel(context, new Thing()));
}
View
#model MvcApp7.Models.ViewModel
#{
var Entity = (dynamic)Model.Entity;
}
#Html.DropDownListFor(x => Entity.OptionID , (System.Web.Mvc.SelectList)Model.Options)
ViewModel
public class ViewModel {
public ViewModel(Context context, object entity) {
this.Entity = entity;
this.Options = new SelectList(context.Options, "Id", "Name");
}
public dynamic Entity { get; set; }
public SelectList Options { get; set; }
}
EDIT: Please excuse me. I forgot that I could specify the selected option in the SelectList itself. I moved the responsibility into the ViewModel and will try to deal with it from there. However, it would still be good to know how to work around this in the View itself in case it was necessary.
I did this in the ViewModel
this.Options = new SelectList(context.Options, "Id", "Name", this.Entity.OptionID);
and this in the View
#Html.DropDownList("OptionID", Model.Options)
View
#model MvcApp7.Models.ViewModel
#Html.DropDownListFor(x => x.Entity.OptionID , (System.Web.Mvc.SelectList)Model.Options)
ViewModel
public class ViewModel {
public ViewModel(Context context, object entity) {
this.Entity = entity;
this.Options = new SelectList(context.Options, "Id", "Name");
}
public dynamic Entity { get; set; }
public SelectList Options { get; set; }
}

Resources