2

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.

Elsa K.
  • 42
  • 1
  • 9
  • I am sure somebody will post an answer which allows to do what you want. But I think that you should have separate controllers for these things instead. MVC suggests to have controllers separated on function sets not on pages like in old asp.net world, where one page had one pagebehind class. You partial view should be served with an other, `bookingGuest` specific controller :) – cly Apr 07 '21 at 19:05
  • So, you mean that on striking the single Submit button I somehow need to invoke two different Controller Actions? – Elsa K. Apr 07 '21 at 19:09
  • @ElsaK Where is the partial view being appended when it is returned? 1. Is it being placed inside the form tags? 2. If it's a list, each element needs a name property that includes an index of the expected list on the server side post. 3. Are these partial view items included in the Model `VMBooking`?, and "No" you don't need to invoke two controller actions nor use a new controller to perform this action. – Ryan Wilson Apr 07 '21 at 19:12
  • 1. Yes I am trying to place the partial view values inside the same form. 2. Yes, it's a list and it has been indexed as well (or this is at least what i think i am doing by creating a List of VMBooking Guest as shown above in the models) 3. No the partial view items have a seperate model. – Elsa K. Apr 07 '21 at 19:20
  • @ElsaK. It looks to me like the Model bound to your view does contain the list you want: `public List VMBookingGuests { get; set; }` so when rendering your partial view the first input would be like the following: ``. Notice how the name is the Property name of the list in your bound Model, followed by an index of the item in the list, followed by the .PropertyName of that object. Also, you should be using unique id values on each item in your partial view. Html ids are supposed to be unique. – Ryan Wilson Apr 07 '21 at 19:27
  • @ElsaK. Also you say: "Yes I am trying to place the partial view values inside the same form" are they rendered within the `form` tags or aren't they? If they are outside the `form` tag they won't submit in your Post – Ryan Wilson Apr 07 '21 at 19:32
  • They were being rendered inside the same form and now that i have used this approach stackoverflow.com/a/66993730/8007381 as suggested by @King my values are being passed correctly, thank you very much for your kind response and help. – Elsa K. Apr 08 '21 at 10:14

3 Answers3

1

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.

Tieson T.
  • 20,774
  • 6
  • 77
  • 92
  • I haven't tried this yet because the way suggested by @King King here https://stackoverflow.com/a/66993730/8007381 worked for me. But i would for sure try this in my free time and will let you know. Thank you for your kind effort. – Elsa K. Apr 08 '21 at 08:54
1

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.

King King
  • 61,710
  • 16
  • 105
  • 130
  • 1
    Thank you very much.. this worked for meee.. I am so happy thankuuuu..!! And as for any readers to come, I have tried the tag approach and it just worked..!! – Elsa K. Apr 08 '21 at 08:50
0

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)
{
    .................................
}
Meysam Asadi
  • 6,438
  • 3
  • 7
  • 17
  • I appreciate your kind effort but nope this isn't working as well, the model is still empty. – Elsa K. Apr 07 '21 at 19:16
  • Well, I have tried this but it works partially, i guess as suggested by other i have to index my values. Because the way you have suggested it returns value for only one instance of the model and not all. Anyways, thank you. – Elsa K. Apr 08 '21 at 07:10