2

I have a MVC form which is more complex than all of my others, utilising three models.

Company -> Base_IP -> RequestedIP which goes ViewModel -> Partial1 -> Partial2

I am using BeginCollectionItem for this has each model has a property list of the the model down from it. IE - Company has a property called baseIps, the BaseIp class has a property called requestedIps, it is requestedIps that is coming back null, the count is there on page render, but is not on submit.

When submitting to the database in the post Create(), I get nulls on the 'requestedIps' property, why is this?

I've added the offending controller and partial code samples below, not the entire thing as it's massive/redundant - any questions, please let me know.

Controller - [HttpGet]Create()

public ActionResult Create()
        {
            var cmp = new Company
            {
               contacts = new List<Contact>
                {
                    new Contact { email = "", name = "", telephone = "" }
                }, pa_ipv4s = new List<Pa_Ipv4>
                {
                    new Pa_Ipv4 
                    { 
                        ipType = "Pa_IPv4", registedAddress = false, existingNotes = "", numberOfAddresses = 0, returnedAddressSpace = false, additionalInformation = "",
                        requestedIps = new List<IpAllocation>
                        {
                            new IpAllocation { allocationType = "Requested", cidr = "", mask = "", subnet  = "" }
                        }
                    }
                }
            };
            return View(cmp);
        }

Controller - [HttpPost]Create()

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Company cmp) // does not contain properties assigned/added to in view render
        {
            if (ModelState.IsValid)
            {
                db.companys.Add(cmp);
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            return View(cmp);
        }

Create View

@model Company
@using (Html.BeginForm())
{
            <div id="editorRowsAsn">
                @foreach (var ip in Model.pa_ipv4s)
                {
                    @Html.Partial("Pa_IPv4View", ip)
                }
            </div>
            <br />
            <div data-role="main" class="ui-content">
                <div data-role="controlgroup" data-type="horizontal">
                    <input type="submit" class="ui-btn" value="Create" />
                </div>
            </div>
}

Pa_Ipv4 View

@model Pa_Ipv4
@using (Html.BeginCollectionItem("pa_ipv4s"))
{
    @Html.AntiForgeryToken()

    <div id="editorRowsRIpM">
        @foreach (var item in Model.requestedIps)
        {
            @Html.Partial("RequestedIpView", item)
        }
    </div>
    @Html.ActionLink("Add", "RequestedManager", null, new { id = "addItemRIpM", @class = "button" }

}

RequestedIpView

@model IpAllocation
<div class="editorRow">
    @using (Html.BeginCollectionItem("requestedIps"))
    {
        <div class="ui-grid-c ui-responsive">
            <div class="ui-block-a">
                <span>
                    @Html.TextBoxFor(m => m.subnet, new { @class = "checkFiller" })
                </span>
            </div>
            <div class="ui-block-b">
                <span>
                    @Html.TextBoxFor(m => m.cidr, new { @class = "checkFiller" })
                </span>
            </div>
            <div class="ui-block-c">
                <span>
                    @Html.TextBoxFor(m => m.mask, new { @class = "checkFiller" })
                    <span class="dltBtn">
                        <a href="#" class="deleteRow"><img src="~/Images/DeleteRed.png" style="width: 15px; height: 15px;" /></a>
                    </span>
                </span>
            </div>
        </div>
    }
</div>
PurpleSmurph
  • 2,055
  • 3
  • 32
  • 52
  • this is a pretty common problem with the de-serialisation of the html request to an object. which is non-trivial for this two level hierarchy. You need to look at the html request, see how the request variables are named. do they match the expected default binding behaviour? – Ewan Sep 01 '15 at 15:52
  • alternatively, build a json object and send that – Ewan Sep 01 '15 at 15:53
  • I've not really used JSON, do you have a link so I can see how to use it in this scenario? The default binding behaviour, ie if I only use Model -> partial with BCI, everything works great - is that what you meant? – PurpleSmurph Sep 01 '15 at 16:03

3 Answers3

2

You first (outer) partial will be generating correct name attributes that relate to your model (your code does not show any controls in the Pa_Ipv4.cshtml view but I assume you do have some), for example

<input name="pa_ipv4s[xxx-xxx].someProperty ...>

however the inner partial will not because @using (Html.BeginCollectionItem("requestedIps")) will generate

<input name="requestedIps[xxx-xxx].subnet ...>
<input name="requestedIps[xxx-xxx].cidr ...>

where they should be

<input name="pa_ipv4s[xxx-xxx].requestedIps[yyy-yyy].subnet ...>
<input name="pa_ipv4s[xxx-xxx].requestedIps[yyy-yyy].cidr ...>

Normally you can pass the prefix to the partial using additional view data (refer this answer for an example), but unfortunately, you do not have access to the Guid generated by the BeginCollectionItem helper so its not possible to correctly prefix the name attribute.

The articles here and here discuss creating your own helper for handling nested collections.

Other options include using nested for loops and including hidden inputs for the collection indexer which will allow you to delete items from the collection and still be able to bind to your model when you submit the form.

for (int i = 0; i < Model.pa_ipv4s.Count; i++)
{
  for(int j = 0; j < Model.pa_ipv4s[i].requestedIps.Count; j++)
  {
    var name = String.Format("pa_ipv4s[{0}].requestedIps.Index", i);
    @Html.TextBoxFor(m => m.pa_ipv4s[i].requestedIps[j].subnet)
    @Html.TextBoxFor(m => m.pa_ipv4s[i].requestedIps[j].cidr)
    ...
    <input type="hidden" name="@name" value="@j" />
  }
}

However if you also need to dynamically add new items you would need to use javascript to generate the html (refer examples here and here)

Community
  • 1
  • 1
  • Okay, that's probably more complex than I need, if I remove the `List<>` element from the `pa_ipv4s` so it's `pa_ipv4` would I then be able to work out to `m.pa_ipv4.requestedIps[x].subnet` if only using BCI on the second partial? – PurpleSmurph Sep 02 '15 at 08:42
  • No, because `pa_ipv4` is a collection property so the `name` attribute needs a indexer otherwise it will not bind to your model (see the required name in my 3rd code snippet) - and in case its should not have the leading `m.` (your model does not have a property named `m`) –  Sep 02 '15 at 08:46
  • Thanks, looking at your answer to do with prefixes in this instance I think there's a different way around it for what I need. Will only need one pa_ipv4, so can stop it being a collection property, so would have `company.pa_ipv4.requestedIps[x].subnet`. Or will BCI categorically not work from a partial in this manner? IE `ViewModel.PartialModel.Partial2Models[x].Property` ? – PurpleSmurph Sep 02 '15 at 08:58
  • Firstly it's not `company.pa_ipv4.requestedIps[x].subnet` its `pa_ipv4.requestedIps[x].subnet` (did you not understand my last comment? - you dont have a model with a property named `company`). And, yes you could make it work if `requestedIps` is a single object (not a collection property). Delete the first partial, and in the second partial use `@using (Html.BeginCollectionItem("ipv4.requestedIps")) {`. Although if that's the case you really should be using a view model anyway to better represent want you want to display/edit –  Sep 02 '15 at 09:05
  • Hi Stephen, I've been looking into the [article](http://www.joe-stevens.com/2011/06/06/editing-and-binding-nested-lists-with-asp-net-mvc-2/) you suggested and it is exactly what I want with the `pa_ipv4s[xxx-xxx].requestedIps[yyy-yyy].subnet` desired outcome. I've amended the file like mentioned (making the code change in the BCI method) and all compiles to dll which is now referenced, but the html isn't rendering correctly/the object is still `null` when debugging - if I raise another question on the subject could you take a look? – PurpleSmurph Sep 04 '15 at 15:57
  • Sure, just give me the link and I'll have a look. –  Sep 05 '15 at 00:55
  • Cheers - fixed a few of the issues but still two outstanding [here](http://stackoverflow.com/questions/32412960/partial-within-partial-not-behaving-correctly) – PurpleSmurph Sep 05 '15 at 12:18
1

If you look at your final markup you will probably have inputs with names like

input name="subnet" 
input name="cidr" 
input name="mask" 

This is how the form collection will appear when the form gets posted. Unfortunately this will not bind to your Company model.

Your fields will need to look like this instead

input name="Company.pa_ipv4s[0].subnet"
input name="Company.pa_ipv4s[0].cidr"
input name="Company.pa_ipv4s[0].mask"

input name="Company.pa_ipv4s[1].subnet"
input name="Company.pa_ipv4s[1].cidr"
input name="Company.pa_ipv4s[1].mask"
JamieD77
  • 13,796
  • 1
  • 17
  • 27
  • Yes that's right (the input markup), how can I get around this problem when using models this way? I am only creating one of the second model(first partial), so would that work if I used a `Html.BeginForm()` rather than BCI ? – PurpleSmurph Sep 01 '15 at 15:59
  • you will probably need to find a good html helper that can render a partial view and accept a field prefix parameter and apply it to the field names http://stackoverflow.com/questions/4898321/asp-mvc-3-razor-add-model-prefix-in-the-html-partialview-extension.. at the same you you'll need to change your foreach loops to for loops to fix the naming issues http://www.c-sharpcorner.com/UploadFile/4b0136/editing-multiple-records-using-model-binding-in-mvc/ – JamieD77 Sep 01 '15 at 16:03
  • Thanks. Multiple records to update is fine, it's the second model's model list property that is causing the issue. I'm looking at [this](http://stackoverflow.com/questions/4898321/asp-mvc-3-razor-add-model-prefix-in-the-html-partialview-extension) you're first link as that seems similar. This does however seem very complex for something I thought would be a lot simpler, coming from a Web Forms background. – PurpleSmurph Sep 01 '15 at 16:19
0

There are multiple ways to "fix" this, and each has its own caveats.

One approach is to setup "Editor" views (typically in ~/Views/Shared/EditorTemplates/ClassName.cshtml), and then use @Html.EditorFor(x => x.SomeEnumerable). This will not work well in a scenario in which you need to be able to delete arbitrary items from the middle of a collection; although you can still handle those cases by means of an extra property like ItemIsDeleted that you set (e.g. via javascript).

Setting up a complete example here would be lengthy, but you can also reference this tutorial: http://coding-in.net/asp-net-mvc-3-how-to-use-editortemplates/

As a start, you would create a simple template like

~/Views/Share/EditorTemplates/Contact.cshtml:

 @model yournamespace.Contact
 <div>
     @Html.LabelFor(c => c.Name)
     @Html.TextBoxFor(c => c.Name)
     @Html.ValidationMessageFor(c => c.Name)
</div>
<div>
     @Html.LabelFor(c => c.Email)
     @Html.TextBoxFor(c => c.Email)
     @Html.ValidationMessageFor(c => c.Email)
</div>
... other simple non-enumerable properties of `Contact` ...
@Html.EditorFor(c => c.pa_ipv4s) @* uses ~/Views/Shared/EditorTemplates/pa_ipv4s.cshtml *@

In your view to edit/create a Company, you would invoke this as

@Html.EditorFor(company => company.Contacts)

(Just like the EditorTemplate for Company invokes the EditorFor pa_ipv4s.)

When you use EditorFor in this way, MVC will handle the indexing automatically for you. (How you handle adding a new contact/IPv4/etc. here is a little more advanced, but this should get you started.)


MVCContrib also has some helper methods you can use for this, but it's not particularly simple from what I recall, and may tie you down to a particular MVC version.

Carl Bussema
  • 1,684
  • 2
  • 17
  • 35
  • This wont work because OP wants to be able to delete items in the collection which means the collection will post back with non-consecutive indexers and binding will fail. –  Sep 02 '15 at 01:30
  • If you bind to IEnumerable<> instead of IList<> you can have non-consecutive indexes. Another option for deleting items is to use a hidden field like `Item.State` and use an `enum` with possible states, like `Added` `Updated` `Deleted` `NoChange`. This can make handling things on the back end easier (also makes it easy to "undelete" something before you submit the form). – Carl Bussema Sep 02 '15 at 15:20
  • No you cannot not. The `DefaultModelBinder` will only bind collections where the indexer starts at zero and is consecutive unless you include a special field `` where xx is the value of the indexer. And you cannot do this using an `EditorTemplate` (because the template does not know its index in the collection) –  Sep 02 '15 at 22:46
  • Hm. OK, I knew I had code using non-consecutive indexes, but I checked and it does have the Index output. So I'll update the answer. – Carl Bussema Sep 03 '15 at 18:56