Dynamically adding items into view and posting back to controller (ASP.NET MVC 4)

I have a ASP.NET MVC 4 app with model, that contains and colection (IEnumerable<T> or IList<T>), i.e.:

class MyModel
{
  public int Foo { get; set; }
  public IList<Item> Bar { get; set; }
}

class Item
{
  public string Baz { get; set; }
}

And I render the data in view with classic @for..., @Html.EditorFor... ad so on. Now there's a need to add on client side to add dynamically new items and then post it back to server.

I'm looking for an easy solution to handle the adding (in JavaScript), aka not manually creating all the inputs etc. Probably to get it somehow from editor template view. And to add it the way that when the form is submitted back to server the model binder will be able to properly create the IList<T> collection, aka some smart handling of inputs' names. I read a bunch of articles, but nothing that was easy and worked reliably (without magic strings like collection variable names, AJAX callbacks to server, ...).

So far this looks promising, but I'd like to rather rely on rendering (items known in advance) on server.

Answers:

Answer

I'm not sure what do you mean 'collection variable names' and probably my solution is kind of magic you noticed.

My solution is based on copying existing editor for element and altering input names via Javascript.

First of all, we need to mark up our editor. This is a code of form outputs editor for collection

        @for (var i = 0; i < Model.Count; i++)
        {
            <div class="contact-card">
                @Html.LabelFor(c => Model[i].FirstName, "First Name")
                @Html.TextBoxFor(c => Model[i].FirstName)
                <br />
                @Html.LabelFor(c => Model[i].LastName, "Last Name")
                @Html.TextBoxFor(c => Model[i].LastName)
                <br />
                @Html.LabelFor(c => Model[i].Email, "Email")
                @Html.TextBoxFor(c => Model[i].Email)
                <br />
                @Html.LabelFor(c => Model[i].Phone, "Phone")
                @Html.TextBoxFor(c => Model[i].Phone)
                <hr />
            </div>

        }

Our editor is placed into div with class contact-card. On rendering, ASP.NET MVC gives names like [0].FirstName, [0].LastName ... [22].FirstName, [22].LastName to inputs used as property editors. On submitting Model Binder converts this to collection of entities based both on indexes and property names.

Next we create javascript function that copies last editor and increases index in brackets by 1. On submitting it adds additional element to collection:

var lastContent = $("#contact-form .contact-card").last().clone();
$("#contact-form .contact-card").last().after(lastContent);

$("#contact-form .contact-card")
    .last()
    .find("input")
    .each(function () {
        var currentName = $(this).attr("name");
        var regex = /\[([0-9])\]/;
        var newName = currentName.replace(regex, '[' + (parseInt(currentName.match(regex)[1]) + 1) + ']');
        $(this).val('');
        $(this).attr('name', newName);
    });

VOILA!! On submitting we will get one more element!

Answer

At the end I did similar stuff what STO was suggesting, but with the custom (non-linear) indices for collections suggested by Phil Haack.

This uses manual naming of elements (so I'm not binding directly to the model) and I can use custom instances (for empty element templates). I've also created some helper methods to generate me the code for the instance, so it's easier to generate code for actual instances from the model or empty ones.

Answer

I did this with help of Backbone (for file uploader) where i insert template whenever user click #addButton View:

@using Telerik.Web.Mvc.UI
@{
    ViewBag.Title = "FileUpload";
    Layout = "~/Areas/Administration/Views/Shared/_AdminLayout.cshtml";
}
<div id="fileViewContainer" class="span12">
<h2>File upload</h2>
@foreach(var fol in (List<string>)ViewBag.Folders){
        <span style="cursor: pointer;" class="uploadPath">@fol</span><br/>
    }
    @using (Html.BeginForm("FileUpload", "CentralAdmin", new { id = "FileUpload" }, FormMethod.Post, new { enctype = "multipart/form-data" }))
    {
        <label for="file1">Path:</label>
        <input type="text" style="width:400px;" name="destinacionPath" id="destinacionPath"/><br />
        <div id="fileUploadContainer">
            <input type="button" class="addButton" id="addUpload" value="Add file"/>
            <input type="button" class="removeButton" id="removeUpload" value="Remove file"/>
        </div>

        <input type="submit" value="Upload" />
    }
</div>
<script type="text/template" id="uploadTMP">
     <p class="uploadp"><label for="file1">Filename:</label>
     <input type="file" name="files" id="files"/></p>
</script>
@{
    Html.Telerik().ScriptRegistrar().Scripts(c => c.Add("FileUploadInit.js"));
}

FileUploadInit.js

$(document).ready(function () {
    var appInit = new AppInit;
    Backbone.history.start();
});
window.FileUploadView = Backbone.View.extend({
    initialize: function () {
        _.bindAll(this, 'render', 'addUpload', 'removeUpload', 'selectPath');
        this.render();
    },
    render: function () {
        var tmp = _.template($("#uploadTMP").html(), {});
        $('#fileUploadContainer').prepend(tmp);
        return this;
    },
    events: {
        'click .addButton': 'addUpload',
        'click .removeButton': 'removeUpload',
        'click .uploadPath': 'selectPath'
    },
    addUpload: function (event) {
        this.render();
    },
    removeUpload: function (event) {
        $($('.uploadp')[0]).remove();
    },
    selectPath: function (event) {
        $('#destinacionPath').val($(event.target).html());
    }
});
var AppInit = Backbone.Router.extend({
    routes: {
        "": "defaultRoute"
    },
    defaultRoute: function (actions) {
        var fileView = new FileUploadView({ el: $("#fileViewContainer") });
    }
});

In Controller you keep your code

I Hope this will help.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.