I've looked around for hours, but I can't seem to find a straight forward method for solving my problem.
Basically, I have a 1-to-many relationship, bridged with a table. My controller seems to handle the 'create' method perfectly, creating records within the '1' table and the bridging 'many' table.
However, the 'edit' method doesn't seem to modify/update the bridging table at all. Any thoughts on what I'm doing wrong, and why it differs from the 'create' approach?
For brevity, let's say I have the following model:
public class Incident
{
[Key]
public int IncidentID { get; set; }
public string Title { get; set; }
public virtual ICollection<IncidentResources> Resources{ get; set; }
}
And an Resource model:
public class Resource
{
[Key]
public int ResourceID{ get; set; }
public int Name { get; set; }
...OTHER FIELDS REMOVED FOR BREVITY...
}
And I am bridging these two via a bridging table and model:
public class IncidentResource
{
[Key]
public int IncidentResourceID { get; set; }
public int IncidentID { get; set; }
public int ResourceID { get; set; }
public virtual <Resource> Resource { get; set; }
}
In my Controllers, I have the following code:
[HttpPost]
public ActionResult Create(Incident incident)
{
//incident.Resources is returned as a valid object...
if (ModelState.IsValid)
{
db.Incidents.Add(incident);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(incident);
}
The above works perfectly! As I mentioned, the new Incident is inserted into the proper table, and all Resources assigned to the incident are added to the IncidentResources table.
However, the following code DOES NOT work this way for the edit...
[HttpPost]
public ActionResult Edit(Incident incident)
{
if (ModelState.IsValid)
{
//THE USER CAN ADD ADDITIONAL RESOURCES
foreach (IncidentResource r in incident.Resources)
{
r.IncidentID = incident.IncidentID;
}
db.Entry(incident).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(incident);
}
Again, the above seemingly does nothing to the IncidentResources table. Records are not added, removed, or updated. I don't receive an error either. It just doesn't do anything.
Please note that I DO NOT want to just trash the bridge table data and reinsert. I will have 1-to-many relationships to that data that I do not want to orphan.
What am I not understanding about how this works?
You need to attach the entity instance created outside EF and let EF know that it has been modified.
Try add:
db.Incidents.Attach(incident);
before you changing state. In your case your last block of code should looks like:
[HttpPost]
public ActionResult Edit(Incident incident)
{
if (ModelState.IsValid)
{
//THE USER CAN ADD ADDITIONAL RESOURCES
foreach (IncidentResource r in incident.Resources)
{
r.IncidentID = incident.IncidentID;
}
db.Incidents.Attach(incident);
db.Entry(incident).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(incident);
}
It seems the IncidentID in the IncidentResource object is not mapping as a Foreign Key to the Incident table. Try adding a ForeignKey annotation to explicitly set IncidentID as a Foreign Key pointing to Incident:
[ForeignKey("Incident")]
public int IncidentID { get; set; }
I wonder if, in your current code, Entity Framework is seeing that the Incident object hasn't been changed at all, and isn't trying to see if there are IncidentResources that need to be adjusted.
Try adding the following line in your loop:
db.Entry(r).State = EntityState.Modified;
EDIT: I should add that, after writing this answer, I was watching a talk by Julie Lerman on Entity Framework where she did mention that the "modified state won't cascade through a graph", which means that the above code should do it for you.
Unless you are tracking the IncidentID in a hidden field in the form, I would bet the problem stems from it trying to update a Incident that doesn't exist (IncidentID:0)
This will depend on your routes, but assuming the default route pattern:
[HttpPost]
public ActionResult Edit(int id, Incident incident)
{
incident.IncidentID = id;
if (ModelState.IsValid)
{
//THE USER CAN ADD ADDITIONAL RESOURCES
foreach (IncidentResource r in incident.Resources)
{
r.IncidentID = incident.IncidentID;
}
db.Entry(incident).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(incident);
}
If you are able, you may try adding the foreign key IncidentID of the IncidentResource table to the primary key to make a composite primary key. I was having a similar issue and this was what helped me.
I got the solution from a couple other stackoverflow questions. My set up did not match these exactly because I am using AutoMapper with Entity model first unit of work approach but making the composite key on the relationship table fixed my problems.
Is it possible to remove child from collection and resolve issues on SaveChanges?
What's the difference between identifying and non-identifying relationships?
Shouldn't you be doing the following
[HttpPost]
public ActionResult Edit(Incident incident)
{
if (ModelState.IsValid)
{
//THE USER CAN ADD ADDITIONAL RESOURCES
foreach (IncidentResource r in incident.Resources)
{
r.IncidentID = incident.IncidentID;
db.Entry(r).State = EntityState.Modified;
}
db.SaveChanges();
return RedirectToAction("Index");
}
return View(incident);
}
Related
In my web i am retrieving a question object Including the subcategories it falls in and the language related data. It also has the QuestionID that is Primary Key for Question. I place this object in PageSession and modify the properties if user wants to.
using (var bo = new BL.QuestionBO())
{
var question = bo.Get(QuestionID);
PageSession.CurrentQuestion = question;
}
public Question Get(long QuestionID)
{
return DataSource.Questions
.Include(q => q.TransQuestions)
.Include(q => q.SubCategories)
.FirstOrDefault(q => q.QuestionID == QuestionID);
}
When I add the object back to the DataContext it adds a new object instead of updating the privious one.
DataSource.Questions.Add(QuestionEnt);
DataSource.SaveChanges();
Am i missing something or coding wrongly?
I have below areas in which i may be wrong.
The data context objects are different while retrieving an object.
I am using Questions.Add()
The proxy object may be don't care about insert/update.
Below is the class generated by .edmx
public partial class Question
{
public Question()
{
this.ChallengeDetails = new HashSet<ChallengeDetail>();
this.TransQuestions = new HashSet<TransQuestion>();
this.SubCategories = new HashSet<SubCategory>();
}
public long QuestionID { get; set; }
public int QuestionTypeID { get; set; }
public int Status { get; set; }
public int InsertedBy { get; set; }
public Nullable<int> ModifiedBy { get; set; }
public virtual ICollection<ChallengeDetail> ChallengeDetails { get; set; }
public virtual QuestionType QuestionType { get; set; }
public virtual ICollection<TransQuestion> TransQuestions { get; set; }
public virtual ICollection<SubCategory> SubCategories { get; set; }
}
Here is the reason.
The reason it happens is that when you use the DbSet.Add method (that
is, Screencasts.Add), not only is the state of the root entity marked
“Added,” but everything in the graph that the context was not
previously aware of is marked Added as well. Even though the developer
may be aware that the Topic has an existing Id value, Entity Framework
honors its EntityState (Added) and creates an Insert database command
for the Topic, regardless of the existing Id.
Source
To update an entity, here are the options you could do.
1. Option A, Change the State
DataSource.Entry(QuestionEnt).State = EntityState.Modified;
DataSource.SaveChanges();
2. Option B, Get the entity from context
var questionEntDb = DataSource.Questions.Find(QuestionEnt.QuestionID);
questionEntDb.PropertyA = QuestionEnt.PropertyA;
questionEntDb.PropertyB = QuestionEnt.PropertyB;
questionEntDb.PropertyC = QuestionEnt.PropertyC;
DataSource.SaveChanges();
When you call Add method, even though it has the existing Id, the state of the entity becomes Added and if you call Attachafter calling Add, the state becomes Unchanged. In this case, it will not do the insert nor the update.
// State = Detached
DataSource.Questions.Add(QuestionEnt); // -> State = Added
DataSource.Questions.Attach(QuestionEnt); // -> State = Unchanged
DataSource.SaveChanges();
I'm getting this error when trying to add an "Entity" to site.
"An entity object cannot be referenced by multiple instances of IEntityChangeTracker"
The Relation Between Site and "Entity" is one to many.
My Models Looks as below:
Site:
public class Site : Model
{
// The collection of Entities belonging to this site
public virtual ICollection<Entity> Entities { get; set; }
}
Entity:
public class Entity : Model
{
public string Label { get; set; }
public string Name { get; set; }
public virtual Site Site { get; set; }
}
My Code in Controller:
[HttpPost]
public ActionResult Add(EntityModel _entityModel)
{
var model = _entityModel.ToEntity();
if (ModelState.IsValid)
{
model.Site = _siteRepository.Find(1);
model.Label = model.Name.Replace(' ','_').ToLower();
_entityRepository.Add(model);
}
return RedirectToAction("Index");
}
Code in EFEntityRepository.cs:
public void Add(Entity entity)
{
DateTime dateModified = DateTime.Now;
entity.CreatedOn = dateModified;
entity.LastUpdatedOn = dateModified;
this._context.Entities.Add(entity);
this._context.SaveChanges();
}
Please suggest a solution.
It sounds like somewhere you have multiple Context classes holding a reference to this object, possibly by key.
Ensure your context is disposed after every request and in addition, there is no other operation causing another context instance to hold onto this entity.
I'm developing a small app in order to better understand how MVC3 anda Razor works. I'm using MVC3, all code was generated automatically (dbContext via T4, Controller via Add Controller, Databese from EDMX model...).
In my model I have this simple model:
http://i.stack.imgur.com/nyqu4.png
public partial class Application
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ApplicationType ApplicationType { get; set; }
}
public partial class ApplicationType
{
public int Id { get; set; }
public string Type { get; set; }
}
As you can see, ApplicationType is basically an enum (shame that EF 4 has no support for enums). So, in my ApplicationController I have this:
public ActionResult Create()
{
ViewBag.AppTypes = new SelectList(db.ApplicationTypes.OrderBy(c => c.Type), "Id", "Type");
return View();
}
[HttpPost]
public ActionResult Create(Application application)
{
if (ModelState.IsValid)
{
db.Applications.Add(application);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(application);
}
And in my view:
#Html.DropDownListFor(model => model.ApplicationType.Id, (SelectList)ViewBag.AppTypes, "Choose...")
Now I'm facing two problems:
1) ApplicationType not being populated:
As #Html.DropDownListFor renders only a simple select, it fills the ID, but does not fill Type property as you can see below (sorry, I can't post images as I'm new here):
http://i.stack.imgur.com/96IR1.png
In the picture you can see that the ID is ok, but Type is empty.
What I'm doing wrong?
2) Duplicated Data
The second problem is that if I fill the Type property manually during debug (simulating a correct workflow scenario), ApplicationType is being duplicated in the database, instead of only referring to an old registry.
So, how can I make #Html.DropDownListFor refer to a previous existing item instead of creating a new one?
Thanks for your help!
I believe the mistake you're making is using your domain models in the view and assuming that on post the entire model should be completely binded and ready to store in the database. While it is possible to use domain models in the view, it's better practice to create separate View Models.
For example :
public class ApplicationViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public SelectList ApplicationTypeList { get; set; }
public string ApplicationTypeId { get; set; }
}
In your view:
#Html.DropDownListFor(model => model.ApplicationTypeId, Model.ApplicationTypeList , "Choose...")
In your controller
[HttpPost]
public ActionResult Create(ApplicationViewModel model)
{
if (ModelState.IsValid)
{
Application application = new Application()
{
Id = model.Id,
Name = model.Name,
ApplicationType = db.ApplicationTypes
.First(a => a.Id == model.ApplicationTypeId);
};
db.Applications.Add(application);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(model);
}
You can then make verifying that your View Model's ApplicationTypeId corresponds to a real application type part of your modelstate's verification. You can use AutoMapper to speed up the process of converting view models to domain models.
Have you tried:
#Html.DropDownListFor(model => model.ApplicationType.Id, m => m.ApplicationType.Type, "Choose...")
Note the second parameter change.
I have a class Article:
public class Article
{
public int Id { get; set; }
public string Text { get; set; }
public Title Title { get; set; }
}
And Title:
public class Title
{
public int Id { get; set; }
public string Name { get; set; }
public int MaxChar { get; set; }
}
Before you can write an Article, you have to choose your Title from a list, so your StringLength for Article.Text can be determined. Meaning, this article can only have a certain amount of chars, deppending on what 'Title' the writer has. Example: Title.Name "Title1" can only write an article with 1000 chars (MaxChar), and Title.Name "Title2" can write an article with 3000 chars. So. Thats means the the string length for Article.Text has to come from Title.MaxChar.
The Title entity is prefixed data that will be stored in the db.
Here's what ive done sone far:
The titles from the db are listed in a view, with a link to create action of the ArticleController with a "title" querystring:
#Models.Title
#foreach (var item in Model) {
#Html.ActionLink(item.Name, "Create", "Article", new { title = item.Id}, new FormMethod())
}
You fill the form, and post it. The HttpPost Create action:
[HttpPost]
public ActionResult Create(Article article)
{
if (article.Text.Length > article.Title.MaxChar)
{
ModelState.AddModelError("Text",
string.Format("The text must be less than {0} chars bla bla", article.Title.MaxChar));
}
if (ModelState.IsValid)
{
db.Article.Add(article);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(hb);
}
Here's the issue. The controller also adds a new Title entity. So the next time I navigate to the view where I have to choose Title, there's a duplicate of the last entity I used to write an article.
Should I do this in an entirly new way, or is there a small tweak. Only other thing I can think of, is just sending the MaxChar as a querystring, and have no relations between the models at all. Just seems a bit silly/webforms kindda.
Cheers
UPDATE #1:
Maybe im doing this the wrong way?
Get Create action
public ActionResult Create(int title)
{
var model = new Article
{
Title = db.Title.Find(title)
};
return View(model);
}
Or maybe its in the Model? Like, do I have to set foreign keys? Something like:
[ForeignKey("Title")]
public int MaxChar { get; set; }
public virtual Title Title { get; set; }
But im pretty sure I read some where that it isnt necesary, that EF takes care of that.
Easiest way would probably be to attach the title to the context in your Create action:
// ...
if (ModelState.IsValid)
{
db.Titles.Attach(article.Title);
db.Article.Add(article);
db.SaveChanges();
return RedirectToAction("Index");
}
// ...
Attach tells EF that article.Title already exists in the database, thereby avoiding that a new Title is inserted when you add the article to the context.
You need to have a distinction between your MVC model and your Entities model. Your MVC Article model should look something like this (bear in mind there are some religious debates about what goes into a model):
public class Article
{
public int Id { get; set; }
public string Text { get; set; }
public int TitleID { get; set; }
public IEnumerable<Title> AvailableTitles {get;set;}
}
In your view, you can create a dropdown based off the available titles, and bind it to the TitleID property. The list of available titles would be populated in the parameterless controller method (and the model-bound method as well).
When your model-bound method brings back the TitleID, instantiate the Title object from the Entities framework based off the ID. Create your Entities Article object using that Title object, and save your changes. This should get you where you want to be.
My model is something like this:
public class Line
{
public int CatalogNumber {get; set;}
public int TeamCode {get; set;}
}
I have a view that gets catalog number and a team code, And I want to check two things:
there is such catalog number in our database
There is such catalog number for the given team
I wrote an attribute (that derives from ValidationAttribute) that checks if there is such a catalog number in our Data Base. But it does nothing!
Maybe it's impossible to such check with an attribute?
(I know that I can implement IValidable and override IsValid method but for my own reasons I prefer doing it with an attribue).
I've to do it without serverPost (ajax would be good)
I'll really appreciate a good example hot to do it.
p.s. (We are using mvc3)
I think that Remote Validation could help you out with this problem. You could use it to check that the catalog number exists in the database:
public class Line
{
[Remote("QueryCatalogNumberExists", "Home")]
public int CatalogNumber { get; set; }
public int TeamCode { get; set; }
}
Then in your controller (I haven't tested this code but it should be something along the lines of):
public JsonResult QueryCatalogNumberExists(int catalogNumber)
{
if (_repository.QueryCatalogNumberExists(catalogNumber))
{
return Json(true, JsonRequestBehavior.AllowGet);
}
return Json(false, JsonRequestBehavior.AllowGet);
}
I believe you can also have additional fields so that you could check that the catalog number is valid for the given TeamCode (I think the TeamCode would have to be nullable as the user may not enter one before entering a catalog number in your current model as the team code is not required). So your model would be:
public class Line
{
[Remote("QueryCatalogNumberExistsForTeamCode", "Home", AdditionalFields = "TeamCode")]
public int CatalogNumber { get; set; }
public int TeamCode { get; set; }
}
and the controller code:
public JsonResult QueryCatalogNumberExistsForTeamCode(int catalogNumber, int? teamCode)
{
if (_repository.QueryCatalogNumberExistsForTeamCode(catalogNumber, teamCode))
{
return Json(true, JsonRequestBehavior.AllowGet);
}
return Json(false, JsonRequestBehavior.AllowGet);
}
I hope this points you in the right direction to a solution.