0

I've been struggling for quite some time now with trying to maintain a list of objects when the ViewModel is submitted back to the controller. The ViewModel receives the list of objects just fine, but when the form is submitted back to the controller the list is empty. All of the non-collection properties are available in the controller, so I'm not sure what the issue is. I have already read the guide a few people have referenced from Scott Hanselman here

From what I can see, everyone solves this by building an ActionResult and letting the model binder map the collection to a parameter:

Controller:

[HttpPost]
public ActionResult Submit(List<ConfigurationVariable> variables)
{
}

View:

@for (int i = 0; i < Model.ConfigurationVariables.Count; i++)
{
    <div class="row">
        <div class="col-xs-4">
            <label name="@Model.ConfigurationVariables[i].Name" value="@Model.ConfigurationVariables[i].Name" />
        </div>
    </div>
    <div class="row">
        <div class="col-xs-4">
            <input type="text" class="form-control" name="@Model.ConfigurationVariables[i].Value" value="@Model.ConfigurationVariables[i].Value" />
        </div>
    </div>
}

What I really want is to be able to pass my ViewModel back to the controller, including the ConfigurationVariables List:

Controller:

[HttpPost]
public ActionResult Submit(ReportViewModel report) //report.ConfigurationVariables is empty
{ 
}

View:

@for (int i = 0; i < Model.ConfigurationVariables.Count; i++)
{
    <div class="row">
        <div class="col-xs-4">
            @Html.LabelFor(model => model.ConfigurationVariables[i].Name, new { @class = "form-control" })
        </div>
    </div>
    <div class="row">
        <div class="col-xs-4">
            @Html.TextBoxFor(model => model.ConfigurationVariables[i].Value, new { @class = "form-control" })
        </div>
    </div>
}

This will be a complicated form and I can't just put every collection into the ActionResult parameters. Any help would be greatly appreciated

  • TextBoxFor will only submit a list of selected Ids with the form, and you need an appropriate view model element to store those. Just google @Html.TextBoxFor and you will find myriad examples for capturing the form values. If you want to pass the whole list back, you need to make a hidden input element and set the value = to your list. I have expanded into an answer that may help a little more. – user7396598 Mar 16 '18 at 23:16
  • The 2nd example generates the correct `name` attribute from your model and will work fine. If its not binding then its because of code you have not shown us, almost certainly because `ConfigurationVariables` is a field, not a property in your `ReportViewModel` (and you first example is dreadful code - never generate the html manually - always use the `HtmlHelper` methods) –  Mar 17 '18 at 04:05
  • @Stephen thanks for the comment. I 100% agree that the first example is terrible, which is why I asked :) I’m not sure why there are so many examples recommending that method. I didn’t realize that fields were exempt from model binding so that might actually be the case here. I don’t have the code in front of me but I’m pretty sure it was a field – Brandon Church Mar 17 '18 at 08:39
  • If that is the case, suggest [this](https://stackoverflow.com/questions/28212005/complex-object-and-model-binder-asp-net-mvc/28228211#28228211) as a dupe (or delete the question to stop more users wasting time adding answers) –  Mar 17 '18 at 08:45
  • @StephenMuecke It's actually a property. One thing I missed, though, was this line: @Html.HiddenFor(model => model.ConfigurationVariables) My page will only submit to the controller if I include that, but it seems like it isn't linked to the properties of ConfigurationVariables that I am setting with the other controls. – Brandon Church Mar 19 '18 at 14:20
  • You cannot include a `@Html.HiddenFor(model => model.ConfigurationVariables)` - model binding will fail and the collection will be `null` (look at the `value` attribute of the html it generates to understand). I am not sure what you mean by _My page will only submit to the controller if I include that_ (that makes no sense) –  Mar 19 '18 at 22:15
  • @StephenMuecke It means that I could only reach the action result when I included that control. If I tried it with the individual properties of ConfigurationVariables it doesn't even reach the controller. I just figured out the issue though... I had a constructor in my "ConfigurationVariable" class which included parameters. Apparently MVC was trying to call that constructor and failing. I would have thought it modifies the existing view model rather than creating an entirely new one to pass back to the controller... I'll be deleting this in a few minutes since I found a duplicate. – Brandon Church Mar 19 '18 at 22:27
  • Do you mean that you did not also have a parameter-less constructor? - you must have one otherwise the `DefaultModelBinder` cannot initialize the model –  Mar 19 '18 at 22:28
  • @StephenMuecke Yes exactly. That's what fixed it. I didn't realize that a new model gets initialized. I thought the controls just updated the values of the existing model. Thanks anyways for the help though. – Brandon Church Mar 19 '18 at 22:30
  • What happens internally is that the `DefaultModelBinder` calls `activator.CreateInstance()` to initialize the model(s) which is why you must have a parameter-less constructor –  Mar 19 '18 at 22:34

2 Answers2

1

You need to hold the Name property in a hidden input so that it's submitted. Label values are lost.

@Html.HiddenFor(model => model.ConfigurationVariables[i].Name)
nitsram
  • 686
  • 5
  • 5
0

Alright, based on your comment you won't be able to utilize mvc's form binding. No worries.

Instead of this controller definition:

public ActionResult Submit(List<ConfigurationVariable> variables)

Use one of these two:

public ActionResult Submit()
public ActionResult Submit(FormCollection submittedForm)

In the first you can access the Request object, you'll need to debug and locate your variable, then you'll need some logic to parse it apart and used the values submitted.

In the second, the form collection will be made up of all the INPUT elements in your form. You will be able to parse through them directly on the object without interference from the other attributes of the Request object.

In both cases you will probably need to use @Html.TextBox, and not TextBoxFor, and you will need to dynamically populate your dropdowns in your view.

I'm not 100% sure about the Request object, but for sure on the FormCollection you will need to create an Input element for each value/collection you want submitted. Including hidden inputs for your textboxes

Your textboxes will need to be SelectListItem collections. those require a key and a value, and when they are submitted you can loop through the collection and check the .Selected attribute.

I would try with FormCollection first, and if that doesn't work fall back to the Request object.

Also note: you are not getting a viewmodel back from the form submission, you will need to rebuild it from the form elements. If you want to post prepopulated data to the view you will need to build a view model and do appropriate parsing on the view to display it.

user7396598
  • 1,269
  • 9
  • 6
  • The collection I'm dealing with is dynamic. I'm trying to build a tool that can read an XML file, display the elements (which works), and then pass that same collection back to the controller in case any edits were made. Is that possible? Does that make a difference to your answer? If not, can you show me what you mean by: "You will also need an array or list<> for each textboxfor that you use. It will need to be named the same as the "name" value of your textboxfor element" – Brandon Church Mar 16 '18 at 23:42
  • it is possible, I will update the answer to assist in doing it that way. – user7396598 Mar 16 '18 at 23:56
  • Suggesting to use `FormCollection` is dreadful advice. NEVER use `FormCollection` - always strongly bind to the model. –  Mar 17 '18 at 05:45
  • read his comments, he doesn't want to build a form model for each of his cases, he wants it to be able to change based on the requirements per user, or some other criteria. Thus, an answer that doesn't use model binding – user7396598 Mar 19 '18 at 16:45