A Partial View passing a collection using the Html.BeginCollectionItem helper
Asked Answered
B

1

15

I made a small project to understand the answer from Stephen Muecke here: Submit same Partial View called multiple times data to controller?

Almost everything works. The javascript adds new fields from the Partial View, and I can tell they're bound to the model by the "temp" values inserted by the controller method for the partial view.

However, when I submit the new fields the AddRecord() method throws an exception showing that the model isn't getting passed in ("Object reference not set to an instance of an object").

Also, when I view the page source, the BeginCollectionItem helper is inserting a hidden tag as it should around the table in the main view that displays pre-existing records, but not around the new fields that the javascript adds.

What am I doing wrong? I'm pretty new at this so thanks for your patience!

My main view:

@model IEnumerable<DynamicForm.Models.CashRecipient>

@using (Html.BeginForm("AddDetail", "CashRecipients", FormMethod.Post))
{
    @Html.AntiForgeryToken()
    <div id="CSQGroup">
    </div>
}

<div>
    <input type="button" value="Add Field" id="addField" onclick="addFieldss()" />
</div>

<script>
    function addFieldss()
    {   
        //alert("ajax call");
        $.ajax({
            url: '@Url.Content("~/CashRecipients/RecipientForm")',
            type: 'GET',
            success:function(result) {
                //alert("Success");
                var newDiv = document.createElement("div"); 
                var newContent = document.createTextNode("Hi there and greetings!"); 
                newDiv.appendChild(newContent);  
                newDiv.innerHTML = result;
                var currentDiv = document.getElementById("div1");  
                document.getElementById("CSQGroup").appendChild(newDiv);
            },
            error: function(result) {
                alert("Failure");
            }
        });
    }
</script>

My Partial View:

@model DynamicForm.Models.CashRecipient
@using HtmlHelpers.BeginCollectionItem

@using (Html.BeginCollectionItem("recipients"))
{
    <div class="editor-field">
        @Html.LabelFor(model => model.Id)
        @Html.LabelFor(model => model.cashAmount)
        @Html.TextBoxFor(model => model.cashAmount)
        @Html.LabelFor(model => model.recipientName)
        @Html.TextBoxFor(model => model.recipientName)
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Save" class="btn btn-default" />
        </div>
    </div>
}

My model:

public class CashRecipient
{
    public int Id { get; set; }
    public string cashAmount { get; set; }
    public string recipientName { get; set; }  
}

In my controller:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddDetail([Bind(Include = "Id,cashAmount,recpientName")] IEnumerable<CashRecipient> cashRecipient)
{
    if (ModelState.IsValid)
    {
        foreach (CashRecipient p in cashRecipient) {
            db.CashRecipients.Add(p);
        }
        db.SaveChanges();
        return RedirectToAction("Index");

    }

    return View(cashRecipient);
}

public ActionResult RecipientForm()
{
    var data = new CashRecipient();
    data.cashAmount = "temp";
    data.recipientName = "temp";
    return PartialView(data);
}
Barnett answered 11/11, 2016 at 0:22 Comment(9)
You cant apply Html.BeginCollectionItem() to a model which is a collection - its applied to a single object. And your view is not even creating editable controls for typeof CashRecipient. There are numerous errors in the code but its unclear exactly what your trying to do here. Are you wanting a form where you can dynamically add (and/or remove) new CashRecipient and post back a collection?Caldera
I'm trying to do the exact same thing Brajesh was trying to do in the question you helped him with. I'm using the same javascript and roughly the same Partial View code as him, except with CashRecipient instead of his model AdminProductDetailModel. Each control that gets added is for a single object. The model itself isn't a collection. I thought BeginCollectionItem wrapped the data for multiple controls as a collection...I probably misunderstood. Can you help me understand?Barnett
Answer the query in my first comment :) And why are you using DisplayFor() - that just renders text and means nothing is editableCaldera
DisplayFor() is just in the table that displays already existing items. The part that matches Bajesh's query is below that, starting with the div with id CSQGroup. If it helps, you can ignore that whole table, I probably shouldn't have included it. Yes, I'm wanting a form where I can dynamically add new CashRecipient controls, and post back multiple instances of CashRecipient.Barnett
Bajesh does not exist in relation to this question so please stop referring to it. And is there a particular reason you do not also want to edit existing CashRecipient objects?Caldera
I only mentioned him to explain my intent. I'm trying to do the same thing. I apologize. I should not have included that table, it's extraneous to the problem I'm trying to solve, again I'm sorry. Just pretend that there are no model objects prior to this form.Barnett
OK, will add an answer shortly. But you still need to be able to edit existing items that have been created.Caldera
Thanks for being patient with me. I'm really new at this.Barnett
Let us continue this discussion in chat.Caldera
C
27

First start by creating a view model to represent what you want to edit. I'm assuming cashAmount is a monetary value, so therefore should be a decimal (add other validation and display attributes as required)

public class CashRecipientVM
{
    public int? ID { get; set; }
    public decimal Amount { get; set; }
    [Required(ErrorMessage = "Please enter the name of the recipient")]
    public string Recipient { get; set; }  
}

Then create a partial view (say) _Recipient.cshtml

@model CashRecipientVM
<div class="recipient">
    @using (Html.BeginCollectionItem("recipients"))
    {
        @Html.HiddenFor(m => m.ID, new { @class="id" })
        @Html.LabelFor(m => m.Recipient)
        @Html.TextBoxFor(m => m.Recipient)
        @Html.ValidationMesssageFor(m => m.Recipient)
        @Html.LabelFor(m => m.Amount)
        @Html.TextBoxFor(m => m.Amount)
        @Html.ValidationMesssageFor(m => m.Amount)
        <button type="button" class="delete">Delete</button>
    }
</div>

and a method to return that partial

public PartialViewResult Recipient()
{
    return PartialView("_Recipient", new CashRecipientVM());
}

Then your main GET method will be

public ActionResult Create()
{
    List<CashRecipientVM> model = new List<CashRecipientVM>();
    .... // add any existing objects that your editing
    return View(model);
}

and its view will be

@model IEnumerable<CashRecipientVM>
@using (Html.BeginForm())
{
    <div id="recipients">
        foreach(var recipient in Model)
        {
            @Html.Partial("_Recipient", recipient)
        }
    </div>
    <button id="add" type="button">Add</button>
    <input type="submit" value="Save" />
}

and will include a script to add the html for a new CashRecipientVM

var url = '@Url.Action("Recipient")';
var form = $('form');
var recipients = $('#recipients');
$('#add').click(function() {
    $.get(url, function(response) {
        recipients.append(response);
        // Reparse the validator for client side validation
        form.data('validator', null);
        $.validator.unobtrusive.parse(form);
    });
});

and the script to delete an item

$('.delete').click(function() {
    var container = $(this).closest('.recipient');
    var id = container.find('.id').val();
    if (id) {
        // make ajax post to delete item
        $.post(yourDeleteUrl, { id: id }, function(result) {
            container.remove();
        }.fail(function (result) {
            // Oops, something went wrong (display error message?)
        }
    } else {
        // It never existed, so just remove the container
        container.remove();
    }
});

And the form will post back to

public ActionResult Create(IEnumerable<CashRecipientVM> recipients)
Caldera answered 11/11, 2016 at 5:50 Comment(6)
How do I do a remove?Limpet
@TravisTubbs, See update for deleting items from the collection (and for reparsing the validator so that you get client side validation for new itemsCaldera
Thanks Stephen! This helped me loads.Limpet
If I try to submit it without adding any new items it crashes on the foreach. How is this prevented?Limpet
@TravisTubbs, Not sure what you mean,or what errors your getting. Since this is not your question, I suggest you ask a a question showing the code your using and the details of the error (but best guess is that your not passing a model to the view so foreach(var recipient in Model) throws an exception - you cannot iterate over null)Caldera
can do! Ill see if I can get it posted by tonight!Limpet

© 2022 - 2024 — McMap. All rights reserved.