简体   繁体   中英

How to pass main and partial views values to the controller when different models are involved?

Original Question: This might be a simple thing to do but I have been looking for it for more than 30 hrs by now and couldn't find any suitable answer that works for me (tried a tons of ways as well).

So, what I am trying to achieve is that I have to display some data on a view and also take in some values within the same view. For this I have created a main view, this displays some data and takes in some values. Also a partial view within the main view, which also displays some data and takes in some values.

The Models for main view and Partial View are different and the related DB tables as well.

Main View Model:

public class VMBooking : VMBase
    {
        public int BookingID { get; set; }
        public Guid BookingGUID { get; set; }
        public int NoOfChildrenArrived { get; set; }
       ...
        public List<VMBookingGuest> VMBookingGuests { get; set; }
       ...
    }

Partial View Model

 public class VMBookingGuest
    {
        public int BookingGuestID { get; set; }
        public int BookingID { get; set; }
        public int GuestID { get; set; }
        public string GuestName { get; set; }
        public string GuestCNIC { get; set; }
        public bool IsCNIC { get; set; }
        public bool IsGuestArrived { get; set; }
        public bool IsGuestDeparted { get; set; }
    }

Now I have successfully passed the partial view model in the partial view and my data is being displayed as well..

My Main View CSHTML:

 @using VMBooking  
<form class="form-horizontal" id="validation-form" method="post" defaultbutton="btnSubmit">
          @Html.HiddenFor(c => c.BookingID)
          @Html.HiddenFor(c => c.BookingGUID)
                           
          @Html.Partial("_Arrival", Model.VMBookingGuests)
                                   
          <div class="form-group">
          <input asp-for="@Model.NoOfChildrenArrived" type="text" id="NoOfChildrenArrived" />
          </div>    
          <button type="submit" id="btnSubmit">Submit</button>
    </form>

Partial View CSHTML:

 @foreach (var item in Model)
            {
                <tr class="center">
                    <td>@item.GuestName</td>
                    <td>@item.GuestCNIC</td>
                    <td>
                        <div>
                            <input id="IsGuestArrived" name="IsGuestArrived" asp-for="@item.IsGuestArrived" type="checkbox"/>
                        </div>
                    </td>
                </tr>
            }

The corresponding controller action is as follows

 [HttpGet]
        public IActionResult Arrival(int id)
        {
            VMBooking model = _BookingRepo.GetBookingByID(id);
            model.VMBookingGuests = _BookingRepo.GetGuestinfo(id);
            PopulateDropdowns(model);
            return View(model);
        }

        [HttpPost]
        public IActionResult Arrival(VMBooking vmBooking)
        {
            VMBooking model = _BookingRepo.GetBookingByID(vmBooking.BookingID);
            model.VMBookingGuests = _BookingRepo.GetGuestinfo(vmBooking.BookingID);
            if (model != null)
            {
                _BookingRepo.ArrivalUpdate(vmBooking);
                foreach (var item in model.VMBookingGuests)
                {
                    _BookingRepo.GuestArrivalUpdate(item);
                }
                return RedirectToAction("Index");
            }
            PopulateDropdowns(model);
            return View();
        }
  

Things are working fine, but the problem arises where I have to submit this combined input data (from main and partial view) on a Single submit button, which is on the main view. When i press the submit button only the values from my main view are passed to the controller and not of the partial view.

Note that I have to pass a list of Check Box values (id="IsGuestArrived") to the controller for every guest entry.

And as said before I have tried a number of different ways but none of them is working for me. So I am asking, What would be the suitable way to achieve this?

Edit: I have found the answer to my query and now i would like to display the changes that i made to my code on the suggestion of @KingKing...

What i did was I inserted partial tag instead of @html.partial in my main view, so the code for my main view goes as

 @using VMBooking  
    <form class="form-horizontal" id="validation-form" method="post" defaultbutton="btnSubmit">
              @Html.HiddenFor(c => c.BookingID)
              @Html.HiddenFor(c => c.BookingGUID)
                               
              <partial name="_Arrival" for="VMBookingGuests" />
                                       
              <div class="form-group">
              <input asp-for="@Model.NoOfChildrenArrived" type="text" id="NoOfChildrenArrived" />
              </div>    
              <button type="submit" id="btnSubmit">Submit</button>
        </form>

And as for my partial view i went with

  @foreach (var item in Model)
                {
                <input type="hidden" asp-for="@Model[i].GuestID" />
                <input type="hidden" asp-for="@Model[i].BookingID" />

                    <tr class="center">
                        <td>@item.GuestName</td>
                        <td>@item.GuestCNIC</td>
                        <td>
                            <div>
                                <input id="IsGuestArrived" name="IsGuestArrived" asp-for="@item.IsGuestArrived" type="checkbox"/>
                            </div>
                        </td>
                    </tr>
                }

notice the Hidden values that i have used inside my partial view.

And within my controller instead of using

 model.VMBookingGuests = _BookingRepo.GetGuestinfo(vmBooking.BookingID);

I used

    model.VMBookingGuests = vmBooking.VMBookingGuests;

Because without the hidden values the guest IDs were not being recognized.

The EditorFor helper is probably a better fit for what you're trying to do, but I would suggest simplifying things first (then you can go that route).

So, instead of this (which generates some invalid markup, by the way):

@using VMBooking  
<form class="form-horizontal" id="validation-form" method="post" defaultbutton="btnSubmit">
    @Html.HiddenFor(c => c.BookingID)
    @Html.HiddenFor(c => c.BookingGUID)
                   
    @Html.Partial("_Arrival", Model.VMBookingGuests)
                           
    <div class="form-group">
        <input asp-for="@Model.NoOfChildrenArrived" type="text" id="NoOfChildrenArrived" />
    </div>    
    <button type="submit" id="btnSubmit">Submit</button>
</form>

use something like this:

@using VMBooking  
<form class="form-horizontal" id="validation-form" method="post" defaultbutton="btnSubmit">
    <input type="hidden" asp-for="BookingID" />
    <input type="hidden" asp-for="BookingGUID" />
                           
    <table>
        @for (int i = 0; i < Model.VMBookingGuests.Count; i++)
        {
            <tr class="center">
                <td>@Model.VMBookingGuests[i].GuestName</td>
                <td>@Model.VMBookingGuests[i].GuestCNIC</td>
                <td>
                    <div>
                        <input type="checkbox" asp-for="VMBookingGuests[i].IsGuestArrived" />
                        <input type="hidden" asp-for="VMBookingGuests[i].BookingGuestID" />
                    </div>
                </td>
            </tr>
        }
    </table>
                                   
    <div class="form-group">
        <input asp-for="NoOfChildrenArrived" type="text" id="NoOfChildrenArrived" />
    </div>    
    <button type="submit" id="btnSubmit">Submit</button>
</form>

You need some sort of markup with names like VMBookingGuests[0].IsGuestArrived etc. in order for the values to be correctly model-bound when the POST action is being processed, and only properties which have some sort of successful control in the view will be submitted - hence adding the hidden input for the BookingGuestID property. There's quite a few other resources that will explain why you need that naming style, but the gist is that if you have something like this (as the names of the inputs actually submitted):

VMBookingGuests[0].IsGuestArrived
VMBookingGuests[1].IsGuestArrived
VMBookingGuests[4].IsGuestArrived
VMBookingGuests[5].IsGuestArrived

Then the modelbinder stops rebuilding the VMBookingGuests list from the point where the indexing is "broken" - in that example, when the index jumps from 1 to 4. It will also only build the list from something like VMBookingGuests[0].SomePropertyHere - you can't just have an input named SomePropertyHere from each guest, nor does something like SomePropertyHere[] work.

Now, you have this in the POST action:

foreach (var item in model.VMBookingGuests)
{
    _BookingRepo.GuestArrivalUpdate(item);
}

That would seem to indicate that you're using your "data" models as "view" models, and relying on your ORM to figure out what's changed. In that case, you'll need to add hidden fields for every property of the guest model. If writing a "service"-style method is an option, you really only need the ID property ( BookingGuestID , in your case) and the IsGuestArrived property (since that's the only thing that should be changing). An example of a method like that might be:

public bool UpdateGuestArrivals(int bookingID, params VMBookingGuest[] guests)
{
    bool success = false;

    if(guests?.Any() == true)
    {
        foreach(var guest in guests)
        {
            var bookingGuest = nameofyourDbContexthere.VMBookingGuests.SingleOrDefault(m => m.BookingID == bookingID && m.BookingGuestID == guest.BookingGuestID);
            if(bookingGuest != null)
            {
                bookingGuest.IsGuestArrived = guest.IsGuestArrived;
            }
        }
 
        nameofyourDbContexthere.SaveChanges();
        success = true;
    }

    return success;
}

There's a lot of assumptions in that example, but I think the idea is clear. You can probably change that to use your repo class.

Elements rendered in partial views should be able to be rendered with their names prefixed with the correct path. It's a bit tricky with Html.Partial (will have a solution at the end). But if you use the tag helper <partial> , it would be easier by using the for attribute (note it's different from model attribute which will not pass along the current name path), like this:

<partial name="_Arrival" for="VMBookingGuests"/>

Your partial view need to be updated as well, because you're mapping an array, you need to use indices to access each array member so that the name for each item can be rendered correctly (with the index), like this:

<!-- NOTE: this requires the view model of your partial view must support accessing item by index, like an IList -->
@for(var i = 0; i < Model.Count; i++) {
            <tr class="center">
                <td>@Model[i].GuestName</td>
                <td>@Model[i].GuestCNIC</td>
                <td>
                    <div>
                        <input id="IsGuestArrived-@i" asp-for="@Model[i].IsGuestArrived" type="checkbox"/>
                    </div>
                </td>
            </tr>
}

Now it should work expectedly because the element names are rendered correctly like VMBookingGuests[0].IsGuestArrived , ...

Now for Html.Partial , you can still use it but you need to pass in the prefix info via a ViewDataDictionary , you need to build that prefix by your own, but it's easy because we use just a standard method to get that, like this:

@{
    var vd = new ViewDataDictionary(ViewData);
    vd.TemplateInfo.Prefix = Html.NameFor(e => e.VMBookingGuests);
}

<!-- for Html.Partial -->
@Html.Partial("_Arrival", Model.VMBookingGuests, vd)

Now with that prefix info passed along, the element names are also rendered correctly as when using <partial> . Of course you still need the same corrected code for your partial view above.

I think you should change the definition of a partial view as follows

@Html.Partial("_Arrival", Model)

And then set the partial view code as follows to send the data to the controller correctly.

@using VMBooking  
@foreach (var item in Model.VMBookingGuests)
{
     <tr class="center">
        <td>@item.GuestName</td>
        <td>@item.GuestCNIC</td>
        <td>
           <div>
              <input id="IsGuestArrived" name="IsGuestArrived" asp-for="@item.IsGuestArrived" type="checkbox"/>
           </div>
        </td>
      </tr>
}

If that doesn't work, use the code below

[HttpPost]
public IActionResult Arrival(VMBooking vmBooking, VMBookingGuest[] VMBookingGuests)
{
    .................................
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM