0

I got two tables, one User and Comment. Both User and Comment got Id as Primary Key. Comment also have a FK key for User Id so called CreatorUserID. Once I try to edit a Comment row I get the following error:

The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_Comment_User". The conflict occurred in database "XX", table "dbo.User", column 'Id'.

I'm not touching FK CreatorUserId in Comment table. I also made sure to enable Cascade in Update and Delete for FK-relationships in SQL Server.

This is how my Edit action looks like:

public ActionResult Edit(Guid? id) {
    Comment  comment = db.Comment.Find(id);
    if (review == null) {
        return HttpNotFound();
    }
    return View(comment);
}


[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,CreatorUserID,Description,Title")] Comment comment) {
    if (ModelState.IsValid) {

        db.Entry(comment).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(comment);
}

Additional info:

  • Database-first

  • Using Guids for Id's.

This is how url looks like before I click update which result error: http://localhost:41003/Comment/Edit/8cab7ab2-3184-e611-9d7a-6c71d98d2a40

User-Table

Id(Guid) PK               
Email(nvarchar)           
Password(nvarchar)

Comment-table

Id(Guid) PK
CreatorUserID(Guid) FK
Description(nvarchar)  // this and title is the only thing I'm trying to edit
Title(nvarchar)

What am I doing wrong? I just want to Update Description and Title. I don't touch Id or CreatorUserID during Update.

View: (I have only included form controls for the properties I am editing)

<div class="form-group">
    @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
    </div>
</div>

<div class="form-group">
    @Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
    </div>
</div>
skylake
  • 409
  • 2
  • 9
  • 24
  • 1
    Are you posting back a value for `CreatorUserID`? (you have not even shown your view). If your not editing `CreatorUserID` you should be using a view model with only those properties you need, and in the POST method, get the object based on its ID and update the relevant properties based on the view model and then save the data model - [What is ViewModel in MVC?](http://stackoverflow.com/questions/11064316/what-is-viewmodel-in-mvc) –  Sep 27 '16 at 01:20
  • @StephenMuecke I added a View(auto generated code) with Edit-template and removed `Id` and `CreatorUserID` to avoid them completely(wrong thing to do?). I will check ViewModel you linked, but what do you mean by POST method? Is it my action in controller so called `Edit`? – skylake Sep 27 '16 at 01:35
  • Your `[HttpPost] public ActionResult Edit(...)` method :) If you do not include a hidden input in your view for property `CreatorUserID` then its value will not be send to the POST method when you submit the form, so that property will just have a default value (which will not exist in your database, hence the exception.). But the correct approach, especially when editing data is to always use a view model (note you do not need a hidden input for `Id` because that value is added to the url as a route value, and will be correctly bound to the model) –  Sep 27 '16 at 01:40
  • @StephenMuecke Aah I see :) I just read the link you gave me which gave me a much better understanding about view model. Just two question, are view models normally stored in Models directory as well? And what is 'hidden input' and do I still need hidden input for `CreatorUserID` in view model? – skylake Sep 27 '16 at 01:58
  • No, create a separate folder (say) `ViewModels` (better separation plus no issues with it being added to your database context). You view model will contain only `Guid ID`, `string Title` and `string Descrption` (and be decorated with validation and display attributes as required). You generate a hidden input using `@Html.HiddenFor(m => m.yourProperty)` but if you using a view model you do not need it (and you also do not need any `[Bind]` attribute) –  Sep 27 '16 at 02:02
  • Even if you dont want to touch the other fields except description and title, `db.Entry(comment).State = EntityState.Modified;` this will mark everything as modified and will be included in the update statement. So you gotta check whether the created by id is getting populated in the model properly. If you just need to specifically update title and description, then do `var entry = db.Entry(comment); entry.Property(p=>p.Title).IsModified = true;entry.Property(p=>p.Description).IsModified = true;` – Developer Sep 27 '16 at 02:07
  • 1
    Your using a view model so the POST method becomes `public ActionResult Edit(CommentVM) { if(!ModelState.IsValid) { return View(model) } Comment comment = db.Comment.Where(x => x.Id = model.ID).FirstOrDefault(); comment.Title = model.Title; db.SaveChanges(); return RedirectToAction("Index"); }` - You get the data model from the db based on the view models ID, then update the data model and save it. –  Sep 27 '16 at 02:16
  • @StephenMuecke I see :) But shouldn't just `string Title` and `string Description` be added to view model and not `id` as I've no intention to manually add `Guid ID` for new rows? As I got better understanding about view model and hidden input, I'm still not confident to get them to work since I'm new to MVC. Would you mind giving me an example? I would be greatly appreciated. – skylake Sep 27 '16 at 02:19
  • You need the `ID` property so that it will be bound (from the url route value) when you submit (otherwise you don't know what it is) - but you do not need a hidden input for it so long as its named `ID` and you using the default route (`url: "{controller}/{action}/{id}`) which you are. –  Sep 27 '16 at 02:25
  • @Developer I get error message: `Member 'IsModified' cannot be called for property 'Title' because the entity of type 'Comment' does not exist in the context.` **for** `entry.Property(p => p.Title).IsModified = true;`. Any idea how to tackle this? – skylake Sep 27 '16 at 02:27
  • @skylake, You do not need `entry.Property(p => p.Title).IsModified = true;` if your using a view model. As far as an example, If you edit your question to show the view, then I can add an answer. –  Sep 27 '16 at 02:29
  • @StephenMuecke I edited my code as you mentioned but I couldn't make `model` work. I also added my `CommentViewModel` in the end of the post. Hopefully I did it correct. – skylake Sep 27 '16 at 02:42
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/124252/discussion-between-stephen-muecke-and-skylake). –  Sep 27 '16 at 02:42
  • Did you miss `@Html.HiddenFor(model => model.ID)`? It's required to make Bind() working. – Han Sep 27 '16 at 03:35
  • @Han, No I did not. It is not required - the `Html.BeginForm()` method adds the value `Id` in the `
    ` element as a route value, which is read by the `DefaultModelBinder` and bound to the model (see last comment in the answer)
    –  Sep 27 '16 at 03:49

1 Answers1

1

The reason for the exception is that you are not generating a form control for property CreatorUserID so when you submit, the DefaultModelBinder initializes and instance of Comment sets the value of Id from the route values and the values of Title and Description from the form values. But CreatorUserID remains the default for Guid so when you update the database, its is looking for a User with Id = {00000000-0000-0000-0000-000000000000} which does not exist.

You could solve this by adding a hidden input in the view for CreatorUserID so that its value is posted and bound to the model

@Html.HiddenFor(m => m.CreatorUserID)

However a far better solution that will address other issues as well is to use a view model (refer What is ViewModel in MVC?).

public class CommentVM
{
    public Guid? ID { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
}

and decorate the properties with view specify display and validation attributes as required. Then you view needs to be changed to use the view model rather than the data model

@model yourAssembly.CommentVM

and the controller methods become

public ActionResult Edit(Guid id) { // should not be nullable
    Comment comment = db.Comment.Find(id);
    if (comment == null) {
        return HttpNotFound();
    }
    // Initialize a view model
    CommentVM model = new CommentVM()
    {
        ID = id,
        Title = comment.Title,
        Description = comment.Description
    };
    return View(model); // return view model
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(CommentVM model) { // BindAttribute not required
    if (!ModelState.IsValid) {
        return View(model);
    }
    // Get the data model
    Comment comment = db.Comment.Where(x => x.Id == model.ID).FirstOrDefault();
    // Update its properties
    comment.Title = model.Title;
    comment.Description = model.Description;
    // Save and redirect
    db.SaveChanges();
    return RedirectToAction("Index");
}

Note that you do not need a hidden input for the ID property because that will be bound from the url's route values (so long as the property is named ID and your using the default route - url: "{controller}/{action}/{id})

Community
  • 1
  • 1
  • It work like a charm. Thank you for giving me a better understanding how these things works. – skylake Sep 27 '16 at 04:00
  • 1
    I noticed you attempted an edit (which I rejected because some of it was incorrect). The only error was the missing `;` at the end of `CommentVM model = new CommentVM() { .... };` –  Sep 27 '16 at 06:16
  • @StephenMuecke - Do we really need a database hit (for getting comment for edit) here? Wouldnt this work - `Comment comment = new Comment{ Id = model.ID }; db.Comment.Attach(comment); comment.Title = model.Title; comment.Description = model.Description; // Save and redirect db.SaveChanges();` – Developer Sep 27 '16 at 11:49
  • @Developer, And where are you setting the value of `CreatorUserID`? –  Sep 27 '16 at 11:57
  • @StephenMuecke - is `CreatorUserID` required while update? If its regarding `.Attach`, then just primary key is enough. And whatever value you modify after attaching (here Title and Description) only those values will be marked as `EntityState.Modified` by EF. So if you check the generated query, only `Title` and `Description` will be there in that update query. – Developer Sep 27 '16 at 12:31
  • @Developer, Are you really recommending not to use view models? –  Sep 27 '16 at 12:33
  • @StephenMuecke - no no...I was just trying to avoid unnecessary database hit (`db.Comment.Where(x => x.Id == model.ID).FirstOrDefault();`) while doing an update. – Developer Sep 27 '16 at 12:34