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>
3
  • 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? Commented Sep 1, 2015 at 15:52
  • alternatively, build a json object and send that Commented Sep 1, 2015 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? Commented Sep 1, 2015 at 16:03

3 Answers 3

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)

Sign up to request clarification or add additional context in comments.

7 Comments

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?
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)
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 ?
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
Hi Stephen, I've been looking into the article 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?
|
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"

3 Comments

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 ?
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 stackoverflow.com/questions/4898321/….. at the same you you'll need to change your foreach loops to for loops to fix the naming issues c-sharpcorner.com/UploadFile/4b0136/…
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 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.
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.

4 Comments

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.
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).
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 <input name="Index" value="xx" > 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)
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.