2

I am relatively new to MVC 5, and having a problem figuring out how to update a relational table. I am using EF code first. I've spent a lot of time searching for a solution but I just can't find anything that relates to my code which is why I have no other choise than to ask here, so I apologise up front if this has already been answered.

I am including the relational table in my Razor view which works very well. But when I want to update the table it simply doesn't update even though I get no errors with my current code. I've also tried adding an extra Bind[(Include = "Id,FirstName,LastName")], but this used the Booking FirstName and LastName. I am unsure if my methods are the "proper" ones. My logic tells me that I need to fetch the data from the view and retrieve them in the controller but I don't know how if I can't use Bind or the db.Entry(item).State = EntityState.Modified?

Booking.cs

namespace Ventures.Innovation.InteractiveModel.Context
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class Bookings
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]

        public Bookings()
        {
            AttendingPeople = new HashSet<AttendingPeople>();
        }

        public int Id { get; set; }

        [Required]
        [StringLength(128)]
        public string AspNetUserFk { get; set; }

        [Required]
        public DateTime Date { get; set; }

        [Required]
        [StringLength(100)]
        public string FirstName { get; set; }

        [Required]
        [StringLength(50)]
        public string LastName { get; set; }

        public virtual AspNetUsers AspNetUsers { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]

        public virtual ICollection<AttendingPeople> AttendingPeople { get; set; }
    }
}

AttendingPeople.cs

namespace Ventures.Innovation.InteractiveModel.Context
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class AttendingPeople
    {
        public int Id { get; set; }
        public int BookingId { get; set; }

        [Required]
        [StringLength(100)]
        public string FirstName { get; set; }

        [Required]
        [StringLength(50)]
        public string LastName { get; set; }

        public virtual Bookings Bookings { get; set; }
    }
}

Controller

// GET: AdminHome/Edit/5
public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    Bookings bookings = db.Bookings.Find(id);
    if (bookings == null)
    {
        return HttpNotFound();
    }

    ViewBag.AspNetUserFk = new SelectList(db.AspNetUsers, "Id", "Email", bookings.AspNetUserFk);

    var attendingPeople = db.AttendingPeople.Where(a => a.BookingId == id).ToList();
    bookings.AttendingPeople = attendingPeople;

    return View(bookings);
}

// POST: AdminHome/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,AspNetUserFk,Date,FirstName,LastName")] Bookings bookings)
{
    if (ModelState.IsValid)
    {
        db.Entry(bookings).State = EntityState.Modified;
        db.SaveChanges();

        var editedAttendingPeople = db.AttendingPeople.Where(a => a.BookingId == bookings.Id).ToList();

        foreach (var item in editedAttendingPeople)
        {
            db.Entry(item).State = EntityState.Modified;
        }

        db.SaveChanges();

        return RedirectToAction("Index");
    }

    ViewBag.AspNetUserFk = new SelectList(db.AspNetUsers, "Id", "Email", bookings.AspNetUserFk);

    return View(bookings);
}

View

@model Ventures.Innovation.InteractiveModel.Context.Bookings

@{
    ViewBag.Title = "Edit booking";
}

<h2>Edit booking</h2>
<hr />

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

<div class="form-horizontal">
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Id)
</div>

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

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

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

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

    @if (Model.AttendingPeople.Count > 0)
    {
        @Html.Label("Attending people")

        foreach (var attendingPeople in Model.AttendingPeople)
        {
            <div class="form-group">
                <div class="col-md-10">
                    @Html.LabelFor(modelItem => attendingPeople.FirstName, htmlAttributes: new { @class = "control-label col-md-5", @style = "padding-left: 0;" })
                    @Html.LabelFor(modelItem => attendingPeople.LastName, htmlAttributes: new { @class = "control-label col-md-5", @style = "padding-left: 0;" })
                </div>
                <div class="col-md-10">
                    @Html.EditorFor(modelItem => attendingPeople.FirstName, new { htmlAttributes = new { @class = "form-control col-md-5", @style = "display: initial;" } })
                    @Html.EditorFor(modelItem => attendingPeople.LastName, new { htmlAttributes = new { @class = "form-control col-md-5", @style = "display: initial;" } })
                </div>
            </div>
        }
    }

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Save changes" class="btn btn-primary" />
        </div>
    </div>
</div>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

I want to save the updated data to AttendingPeople table in the database but nothing happens.

Ghost
  • 313
  • 1
  • 2
  • 14
  • Okay, I get now that the code: db.Entry(item).State = EntityState.Modified; Doesn't actually do anything because there are no updates to it. What I don't understand is, that even though i succesfully add AttendingPeople to the Bookings, and am able to do a loop in the View, I don't get anything back from the view to the controller, meaning that bookings.AttendingPeople.Count is 0. I think I need to retrieve the AttendingPeople data and get them to the controller, but how? – Ghost Jul 01 '19 at 14:42
  • Spent 2 days on this so far. I've noticed the FormCollection only has 1 attendingPeople.FirstName and attendingPeople.LastName, But if I change the foreach to a for, I get multiple results (if I have more than 1 attending people) like attendingPeople[0].FirstName and attendingPeople[0].LastName. Still not closer to a solution though. – Ghost Jul 02 '19 at 11:51
  • Is your form binding back to the controller action as you expect? your question is narrow but your code example is so broad. One thing I noticed is in the more important bit of your controller is that there is a comment "// POST: AdminHome/Edit/5" and you seem to be posting from a "bookings" view, when you hit that Save (submit) button on your page, have you debugged the xhr request that goes over using developer tools? is it going to the right controller and action with the form data you expect? – JARRRRG Jul 03 '19 at 08:57
  • Yes it is going from the scaffolded Edit page to the correct controller and action which is the Edit method that has a Booking as parameter. But I am not sure how to bind the AttendingPeople changes to the Booking when I press save. In the top Edit method I add the AttendingPeople to the Booking and in my view (Edit.cshtml) I can get the values from Model. I've tried adding "ICollection attendingPeople" and even "string[] attendingPeople" to the parameter of the Edit method but they are always null. – Ghost Jul 03 '19 at 09:16
  • 1
    Is your AttendingPeople populating correctly on the view? Also have you checked what html gets rendered out as html? Are your attendingpeople being added as array objects e.g. the id of the editor field is AttendingPeople[0], AttendingPeople[1] etc.? This is what I'm trying to refer to - the voted answer on https://stackoverflow.com/questions/19964553/mvc-form-not-able-to-post-list-of-objects --- Note, you do not need to implement the solution as editor/display templates in the answer. There are other ways of doing it. – JARRRRG Jul 03 '19 at 09:30
  • If you mean that I can see the data from AttendingPeople in my foreach loop, then yes it is populating correctly on the view. I've changed it to a for loop instead though because I read earlier that it would distinguish better if there are multiple values of the same type. When I use the for loop I can see the objects as attendingPeople[0], attendingPeople[1] etc. So I get what I want in the view and I can see the attendingPeople[0], attendingPeople[1] in the FormCollection. I see that the question you link to is the same issue I have. – Ghost Jul 03 '19 at 14:36
  • Haha, I finally got it with your kind help. :D I needed to name my Edit parameter "ICollection attendingPeople" and the for loop "attendingPeople" so they matched. 3 days spent on figuring out that it was a simple naming issue. – Ghost Jul 03 '19 at 14:41
  • Glad you got it working :) – JARRRRG Jul 04 '19 at 14:10

1 Answers1

1

Thanks to JARRRRG for helping me out here. :)

To solve this I needed to make sure my naming was exactly the same. Apparently this is how it finds/binds from the view to the controller. Also I needed to make sure I used a for loop so the collection could distinguish between the objects.

So this is my updated view:

@if (Model.AttendingPeople.Count > 0)
{
    @Html.Label("Attending people")
    var attendingPeople = Model.AttendingPeople.ToArray();

    foreach (int i = 0; i < attendingPeople.Length; i++)
    {
        @Html.HiddenFor(modelItem => attendingPeople[i].Id)
        <div class="form-group">
            <div class="col-md-10">
                @Html.LabelFor(modelItem => attendingPeople[i].FirstName, htmlAttributes: new { @class = "control-label col-md-5", @style = "padding-left: 0;" })
                @Html.LabelFor(modelItem => attendingPeople[i].LastName, htmlAttributes: new { @class = "control-label col-md-5", @style = "padding-left: 0;" })
            </div>
            <div class="col-md-10">
                @Html.EditorFor(modelItem => attendingPeople[i].FirstName, new { htmlAttributes = new { @class = "form-control col-md-5", @style = "display: initial;" } })
                @Html.EditorFor(modelItem => attendingPeople[i].LastName, new { htmlAttributes = new { @class = "form-control col-md-5", @style = "display: initial;" } })
            </div>
        </div>
    }
}

And this is my new Edit:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,AspNetUserFk,Date,FirstName,LastName")] Bookings bookings, ICollection<AttendingPeople> attendingPeople)
{
    if (ModelState.IsValid)
    {
        db.Entry(bookings).State = EntityState.Modified;
        db.SaveChanges();

        foreach (var item in attendingPeople)
        {
            item.BookingId = bookings.Id;
            db.Entry(item).State = EntityState.Modified;
        }

        db.SaveChanges();

        return RedirectToAction("Index");
    }

    ViewBag.AspNetUserFk = new SelectList(db.AspNetUsers, "Id", "Email", bookings.AspNetUserFk);

    bookings = db.Bookings.Include(at => at.AttendingPeople).SingleOrDefault(b => b.Id == bookings.Id)

    return View(bookings);
}
Ghost
  • 313
  • 1
  • 2
  • 14