0

I am trying to pass the Model data from a View (and PartialView within the View) back to the Controller upon HttpPost. (Adapted from Pass SelectedValue of DropDownList in Html.BeginForm() in ASP.NEt MVC 3)

Why? I want to show a list of assets each with a DropDownList and number of options. Upon submission of form to read the selected items from DropDownList.

My 2 (simplified) models:

public class Booking
{
    public int BookingID { get; set; }
    public int StoreID { get; set; }
    ...
    public IEnumerable<AssetShort> Assets { get; set; }
}

and

public class AssetShort
{
    public int AssetID { get; set; }
    ....
    public int SelectedAction { get; set; }
    public IEnumerable<SelectListItem> ActionList { get; set; }
}

In my Booking Controller > Create I build the List:

public ActionResult Booking(int id)
    {
     // get myBag which contains a List<Asset>
     //  booking corresponds to 'id'

        var myAssets = new List<AssetShort>();
        foreach (var a in myBag.Assets)
        {
            var b = new AssetShort();
            b.AssetID = a.ID;
            b.SelectedAction = 0;
            b.ActionList = new[]
                { 
            new SelectListItem { Selected = true, Value = "0", Text = "Select..."},
            new SelectListItem { Selected = false, Value = "1", Text = "Add"},
            new SelectListItem { Selected = false, Value = "2", Text = "Remove"},
            new SelectListItem { Selected = false, Value = "3", Text = "Relocate"},
            new SelectListItem { Selected = false, Value = "4", Text = "Upgrade"},
            new SelectListItem { Selected = false, Value = "5", Text = "Downgrade"}
                };
            myAssets.Add(b);
        };

        var model = new BookingRequirementsViewModel
            {
                BookingID = booking.ID,
                StoreID = booking.StoreID,
                Assets = myAssets.ToList(),
            };
        return View(model);

My View:

@model uatlab.ViewModels.BookingRequirementsViewModel
@{
    ViewBag.Title = "Booking step 2";
}
<h4>Your booking ref. @Model.BookingID</h4>
@using (Html.BeginForm("Booking2", "Booking", FormMethod.Post))
{
<fieldset>
  @Html.AntiForgeryToken()
  @Html.HiddenFor(model => model.StoreID)

  @Html.Partial("_Assets", Model.StoreAssets)

  <input type="submit" value="Cancel" class="btn btn-default" />
  <input type="submit" value="Next" class="btn btn-default" />
</fieldset>
}

The Partial View includes

@foreach (var item in Model)
{
    <tr>
        <td>@item.Name</td>
        <td>@item.Number</td>
        <td>@Html.DropDownListFor(modelItem=>item.SelectedAction, item.ActionList)</td>
    </tr>
}

So, all this works fine in the browser and I can select dropdowns for each asset listed but when I submit the only value posted back is the StoreID as it is in a "HiddenFor".

The booking2 controller has the model for a parameter:

public ActionResult Booking2(BookingRequirementsViewModel model)
{
    //loop through model.Assets and display SelectedActions
}

Let me make it clear what the problems is - in Booking2 controller the Model is null when viewed in Debug mode and I get error "Object reference not set to an instance of an object."

Any ideas please how to pass back the Model to controller from view?

Regards Craig

Community
  • 1
  • 1
Craig Roberts
  • 171
  • 1
  • 4
  • 18
  • 1
    You cant use a partial for generating controls in a collection. If you inspect the html you will see that you have duplicate `name` attributes (as well as duplicate `id` attributes which is invalid html). You need to use a `for` loop in the main view, or use a custom `EditorTemplate` for `Assets` –  Dec 18 '14 at 23:05

1 Answers1

1

You need to create an EditorTemplate for AssetShort. I also suggest moving ActionList to the BookingRequirementsViewModel so your not regenerating a new SelectList for each AssetShort

The models you have posted aren't making sense. Your controller has var model = new BookingRequirementsViewModel { ..., Assets = myAssets.ToList() }; but in the view you refer to @Html.Partial("_Assets", Model.StoreAssets)? Are these 2 different properties. I will assume that StoreAssets is IEnumerable<AssetShort>

/Views/Shared/EditorTemplates/AssetShort.cshtml

@model AssetShort
<tr>
  <td>@Html.DispayFor(m => m.Name)</td>
  ....
  <td>
    @Html.DropDownListFor(m => m.SelectedAction, (IEnumerable<SelectListItem>)ViewData["actionList"], "--Please select--")
    @Html.ValidationMessageFor(m => m.SelectedAction)
  </td>
</tr>

In the main view

@model uatlab.ViewModels.BookingRequirementsViewModel
....
@using (Html.BeginForm()) // Not sure why you post to a method with a different name
{
  ....
  @Html.HiddenFor(m => m.StoreID)
  @Html.EditorFor(m => m.StoreAssets, new { actionList = Model.ActionList })
  ....
}

In the controller

public ActionResult Booking(int id)
{
  ....
  var model = new BookingRequirementsViewModel
  {
    BookingID = booking.ID,
    StoreID = booking.StoreID,
    Assets = myBag.Assets.Select(a => new AssetShort()
    {
      AssetID = a.ID,
      SelectedAction = a.SelectedAction, // assign this if you want a selected option, otherwise the "--Please select--" option will be selected
      ....
    })
  };
  ConfigureViewModel(model); // Assign select list
  return View(model);
}

And a separate method to generate the SelectList because it needs to be called in the GET method and again in the POST method if you return the view. Note use the overload of DropDownListFor() to generate the option label (null value) as above, and there is no point setting the Selected property (the value of SelectedAction determines what is selected, not this)

private ConfigureViewModel(BookingRequirementsViewModel model)
{
  model.ActionList = new[]
  {
    new SelectListItem { Value = "1", Text = "Add"},
    ....
    new SelectListItem { Value = "5", Text = "Downgrade"}
  };
}

and the POST

public ActionResult Booking(BookingRequirementsViewModel model)
{
  if (!ModelState.IsValid)
  {
    ConfigureViewModel(model); // Re-assign select list
    return View(model);
  }
  // save and redirect
}

I recommend also making SelectedAction nullable with the [Required] attribute so you get client and server side validation

public class AssetShort
{
  public int AssetID { get; set; }
  ....
  [Required]
  public int? SelectedAction { get; set; }
}
  • Stephen One question please... The line in the Booking controller where populating list of AssetShort in BookingRequirementsModel model: SelectedAction = a.SelectedAction, This looks like I need a SelectedAction in Assets model too, which I dont have at present. Is that right? regards – Craig Roberts Dec 19 '14 at 09:21
  • I just assumed that you had it. If you not actually saving the value of the selected option to the database then I guess you don't need it but in that case, whats the actual purpose of it in `AssetShort`? –  Dec 19 '14 at 09:27
  • Stephen - one thing is not working as expected. The model returned in POST does not include all the model values from GET, except the StoreID which is in a HiddenFor. If I include StoreAssets in a HiddenFor the ModelState is not Valid. Do I have to do something in the Main View? Regards – Craig Roberts Dec 19 '14 at 10:40
  • Re purpose of SelectedAction in AssetShort is to know what action to take at that point wrt the Asset. e.g. Add one. I could record it to the DB but don't have that need at the moment. More the problem is accessing the Model data when POSTed because I cannot determine which Action was chosen for which Asset unless I interrogate the DB again for the list which seems a waste. – Craig Roberts Dec 19 '14 at 10:45
  • NThe only property that renders back for `StoreAssets` is the value of `SelectedAction` (the dropdown) If you also want to post back say `AssetID`, then you will need `@Html.TextBoxFor()` or `@Html.TextBoxFor()`. But as I mentioned, you did not post all the models and what you did was confusing and did not seem to match your controller or view code so hard to say.An not sure whtt you mean by _include StoreAssets in a HiddenFor_? (you cant use `HiddenFor()` to bind to a complex object, only to value types. –  Dec 19 '14 at 10:50
  • Might be easiest if you update your question with the `BookingRequirementsViewModel` and `AssetShort` models so I can check it against the assumptions I made in my code –  Dec 19 '14 at 11:00