Search code examples
asp.net-mvcentity-frameworkrazorhtml.dropdownlistfornavigation-properties

ASP.NET MVC EntityFramework How to Save New Object along with Multiple Child Objects in Create


I'm using the database first method. I have a single form where data is entered. From this, a new "form entry", a new customer and 3 new addresses will be created. 2 addresses are associated with the customer, 1 address is associated with the form entry as a whole, and the customer is associated with the form entry.

The question (revision 2): How does one usually go about preparing fields that reference objects that will be created simultaneously -- "hey, these id fields right here will reference objects created simultaneously from the same, current dataset you're being built from"?

(

i.e. How would one normally do something like the following in MVC:

1) create each of the 3 addresses as new addresses;

2) associate 2 of them with a new customer via checkoutForm.Customer.HomeAddressId and checkoutForm.Customer.MailingAddressId;

3) associate the other with checkoutForm.PlaceOfUseAddressId itself;

4) save the customer; then

5) associate the customer with checkoutForm.CustomerId; and, finally

6) save the checkoutForm itself,

while not making ModelState.IsValid false in the meantime because checkoutForm.CustomerId and each x.AddressId are required but are initially null?

)

Any references to something that explains a procedure for simultaneously creating multiple dependent objects like this would be fantastic!


Edit: I removed the bind attribute from the parameter for Create(), and that makes all the data pull into the correct fields. The dropdownlists are properly grabbing their respective StateId.

Currently, ModelState.IsValid reports false on submit. The required ids of the child objects are null and such (which makes sense, because that's what I'm trying to figure out how to tell .NET -- here's an object association that will be created once we have the data).


Edit2: I refined the question yet again, now that I'm getting closer to what I actually need to fix: ModelState.IsValid == false, because the pertinent ids for checkoutForm and Customer that reference dependencies are null.


Depiction of the model behind this form: Entity Framework model diagram

Here's the form itself: Page containing form

( Note the 2 dropdownlists. Everything else is a textbox for strings, ints, or dates. )

What I have for the controller so far (not much beyond what the scaffolding created):

// GET: CheckoutForms/Create
public ActionResult Create()
{
    ViewBag.HomeStateList = new SelectList(db.RT_STATE_LIST, "ST_SEQ", "ST_ABBR", 2);
    ViewBag.MailingStateList = new SelectList(db.RT_STATE_LIST, "ST_SEQ", "ST_ABBR", 2);
    return View();
}

// POST: CheckoutForms/Create
[HttpPost]
[ValidateAntiForgeryToken]
// public ActionResult Create([Bind(Include = "Customer.FirstName,Customer.LastName,VacuumNumber,Customer.Phone,Customer.DriversLicense,Customer.HomeAddress.Street,Customer.HomeAddress.City,Customer.HomeAddress.StateId,Customer.MailingAddress.Street,Customer.MailingAddress.City,Customer.MailingAddress.StateId,Customer.MailingAddress.PostalCode,PlaceOfUseAddress.Street,PlaceOfUseAddress.City,PlaceOfUseAddress.StateId,PickupDate,DueDate,ReturnedDate,EnteredBy,EnteredDate,ModifiedBy,ModifiedDate")] CheckoutForm checkoutForm)
public ActionResult Create(CheckoutForm checkoutForm)
{
    if (ModelState.IsValid)
    {
            checkoutForm.PlaceOfUseAddress.StateId = 1;

            // !!! Need to be given correct value once authorization is set up.
            checkoutForm.EnteredBy = -1;
            checkoutForm.ModifiedBy = -1;
            checkoutForm.EnteredDate = System.DateTime.Now;
            checkoutForm.ModifiedDate = System.DateTime.Now;

            // ??? db.Addresses.Add(checkoutForm.PlaceOfUseAddress);
            // ??? db.Addresses.Add(checkoutForm.Customer.HomeAddress);
            // ??? db.Addresses.Add(checkoutForm.Customer.MailingAddress);
            // ??? db.SaveChanges();
            // ??? checkoutForm.PlaceOfUseAddressId = checkoutForm.PlaceOfUseAddress.AddressId;
            // ??? checkoutForm.Customer.HomeAddressId = checkoutForm.Customer.HomeAddress.AddressId;
            // ??? checkoutForm.Customer.MailingAddressId = checkoutForm.Customer.MailingAddress.AddressId;
            // ??? db.Customers.Add(checkoutForm.Customer);
            // ??? db.SaveChanges();
            db.CheckoutForms.Add(checkoutForm);
      db.SaveChanges();
      return RedirectToAction("Index");
    }

        ViewBag.HomeStateList = new SelectList(db.RT_STATE_LIST, "ST_SEQ", "ST_ABBR", checkoutForm.Customer.HomeAddress.StateId);
        ViewBag.MailingStateList = new SelectList(db.RT_STATE_LIST, "ST_SEQ", "ST_ABBR", checkoutForm.Customer.MailingAddress.StateId);
        return View(checkoutForm);
}

Finally, here are the form elements from the view (with everything else stripped out:

@using (Html.BeginForm()) 
{
    @Html.HiddenFor(model => model.CustomerId)
    @Html.HiddenFor(model => model.PlaceOfUseAddressId)
    @Html.HiddenFor(model => model.Customer.HomeAddressId)
    @Html.HiddenFor(model => model.Customer.MailingAddressId)
    @Html.HiddenFor(model => model.EnteredBy)
    @Html.HiddenFor(model => model.EnteredDate)
    @Html.HiddenFor(model => model.ModifiedBy)
    @Html.HiddenFor(model => model.ModifiedDate)


    <!-- Name and Vacuum Number -->
    @Html.EditorFor(model => model.Customer.FirstName, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.EditorFor(model => model.Customer.LastName, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.EditorFor(model => model.VacuumNumber, new { htmlAttributes = new { @class = "form-control checkout" } })

    <!-- Driver's License -->
    <!-- Phone Number -->
    @Html.EditorFor(model => model.Customer.Phone, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.EditorFor(model => model.Customer.DriversLicense, new { htmlAttributes = new { @class = "form-control checkout" } })

    <!-- Place of Use Address -->
    @Html.EditorFor(model => model.PlaceOfUseAddress.Street, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.EditorFor(model => model.PlaceOfUseAddress.City, new { htmlAttributes = new { @class = "form-control checkout" } })

    <!-- Customer's Home Address -->
    @Html.EditorFor(model => model.Customer.HomeAddress.Street, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.EditorFor(model => model.Customer.HomeAddress.City, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.DropDownListFor(model => model.Customer.HomeAddress.StateId, ViewBag.HomeStateList as SelectList, htmlAttributes: new { @class = "form-control checkout" })

    <!-- Customer's Mailing Address -->
    @Html.EditorFor(model => model.Customer.MailingAddress.Street, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.EditorFor(model => model.Customer.MailingAddress.City, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.DropDownListFor(model => model.Customer.MailingAddress.StateId, ViewBag.MailingStateList as SelectList, htmlAttributes: new { @class = "form-control checkout" })

    @Html.EditorFor(model => model.Customer.MailingAddress.PostalCode, new { htmlAttributes = new { @class = "form-control checkout" } })

    <!-- Dates Picked Up, Due, and Returned -->
    @Html.EditorFor(model => model.PickupDate, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.EditorFor(model => model.DueDate, new { htmlAttributes = new { @class = "form-control checkout" } })

    @Html.EditorFor(model => model.ReturnedDate, new { htmlAttributes = new { @class = "form-control checkout" } })
}

Solution

  • Figured it out.

    On the View, the Hidden fields are connected with the foreign key address ids (as I had them), but must also be pre-populated with bogus values (so they aren't null), like so:

    @Html.HiddenFor(model => model.CustomerId, new { @Value = "0" })
    @Html.HiddenFor(model => model.PlaceOfUseAddressId, new { @Value = "0"})
    @Html.HiddenFor(model => model.Customer.HomeAddressId, new { @Value = "0" })
    @Html.HiddenFor(model => model.Customer.MailingAddressId, new { @Value = "0" })
    

    Then, the procedure on the Controller ends up being fairly straightforward, because MVC is otherwise successfully handling all the pieces. I just need to save and associate them, children on up:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(CheckoutForm checkoutForm)
    {
        Address PlaceOfUse = new Address();
        Address HomeAddy = new Address();
        Address MailingAddy = new Address();
        Customer Client = new Customer();
    
        if (ModelState.IsValid)
        {
            // Not currently available to the user to be filled in by them
            checkoutForm.PlaceOfUseAddress.StateId = 1;
    
            // !!! Need to be given correct value once authorization is set up.
            checkoutForm.EnteredBy = TestAccountId;
            checkoutForm.ModifiedBy = TestAccountId;
            checkoutForm.EnteredDate = System.DateTime.Now;
            checkoutForm.ModifiedDate = System.DateTime.Now;
    
            PlaceOfUse = checkoutForm.PlaceOfUseAddress;
            HomeAddy = checkoutForm.Customer.HomeAddress;
            MailingAddy = checkoutForm.Customer.MailingAddress;
            db.Addresses.Add(PlaceOfUse);
            db.Addresses.Add(HomeAddy);
            db.Addresses.Add(MailingAddy);
            db.SaveChanges();
            checkoutForm.PlaceOfUseAddressId = PlaceOfUse.AddressId;
            checkoutForm.Customer.HomeAddressId = HomeAddy.AddressId;
            checkoutForm.Customer.MailingAddressId = MailingAddy.AddressId;
    
            Client = checkoutForm.Customer;
            db.Customers.Add(Client);
            db.SaveChanges();
            checkoutForm.CustomerId = Client.CustomerId;
            db.CheckoutForms.Add(checkoutForm);
            db.SaveChanges();
            return RedirectToAction("Index");
        } 
        else {
            // To review in debug mode, when there are problems
            //var errors = this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors);
    
            ViewBag.HomeStateList = new SelectList(db.RT_STATE_LIST, "ST_SEQ", "ST_ABBR", checkoutForm.Customer.HomeAddress.StateId);
            ViewBag.MailingStateList = new SelectList(db.RT_STATE_LIST, "ST_SEQ", "ST_ABBR", checkoutForm.Customer.MailingAddress.StateId);
            return View(checkoutForm);
        }
    }
    

    Plus, removing the "Bind" attribute from Create()'s parameter made it all a lot easier.