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);
}
Community
  • 1
  • 1
Robert M.
  • 575
  • 5
  • 17
  • 1
    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? –  Nov 11 '16 at 04:23
  • 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? – Robert M. Nov 11 '16 at 05:20
  • 1
    Answer the query in my first comment :) And why are you using `DisplayFor()` - that just renders text and means nothing is editable –  Nov 11 '16 at 05:22
  • 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. – Robert M. Nov 11 '16 at 05:27
  • 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? –  Nov 11 '16 at 05:30
  • 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. – Robert M. Nov 11 '16 at 05:32
  • OK, will add an answer shortly. But you still need to be able to edit existing items that have been created. –  Nov 11 '16 at 05:34
  • Thanks for being patient with me. I'm really new at this. – Robert M. Nov 11 '16 at 05:35
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/127859/discussion-between-stephen-muecke-and-robert-m). –  Nov 11 '16 at 05:54

1 Answers1

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)
  • How do I do a remove? – Travis Tubbs Jan 13 '17 at 19:45
  • 1
    @TravisTubbs, See update for deleting items from the collection (and for reparsing the validator so that you get client side validation for new items –  Jan 16 '17 at 04:15
  • Thanks Stephen! This helped me loads. – Travis Tubbs Jan 17 '17 at 14:50
  • If I try to submit it without adding any new items it crashes on the foreach. How is this prevented? – Travis Tubbs Jan 18 '17 at 19:10
  • 1
    @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`) –  Jan 18 '17 at 20:43
  • can do! Ill see if I can get it posted by tonight! – Travis Tubbs Jan 19 '17 at 13:56