Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested BeginCollectionItem

I'm using Steve Sanderson's BeginCollectionItem approach to add dynamic content. Everything works fine when I'm doing it on the first level. However, when try to implement a nested collection meaning a BeginCollectionItem in another BeginCollectionItem, it doesn't seem to work.

My models are as follows:

public class Order
{

        [Key]
        [HiddenInput]
        public int id { get; set; }

        [Display(Name = "Order number")]
        public string number { get; set; }

        ...

        [Display(Name = "Payment method")]
        public List<PaymentMethod> payment_methods { get; set; }

        ...
}

public class PaymentMethod
{
        public MethodOfPayment method { get; set; }
        public CC cc { get; set; }
        public CASH cash { get; set; }
        public TT tt { get; set; }
}

public class TT
{
        [Key]
        public int id { get; set; }

        [Required(ErrorMessage = "{0} is required.")]
        [Display(Name = "Total amount")]
        public double? total_value { get; set; }

        ...

        [Display(Name = "Transfers")]
        public List<Transfer> transfers { get; set; }
}

public class Transfer
{
        [Key]
        public int id { get; set; }

        [Display(Name = "Payment")]
        public int payment_id { get; set; }

        [Required(ErrorMessage = "{0} is required.")]
        [Display(Name = "SWIFT")]
        public string swift { get; set; }

        [Required(ErrorMessage = "{0} is required.")]
        [Display(Name = "Amount transferred")]
        public double? transfer_amount { get; set; }

        [Required(ErrorMessage = "{0} is required.")]
        [Display(Name = "Date transferred")]
        public DateTime transfer_date { get; set; }

        ...
}

Now what i have is an Order which could have several payment methods, and if one of the payment methods is a TT (telex transfer) it could have several transfers involved. Implementing several payment methods in one Order as a collection works, but when I try to implement several transfers within a TT, nothing of these transfers gets passed to the controller.

Here's what my view looks like:

@model prj.Models.Model.Order

@using (Html.BeginForm("Create")){
@Html.ValidationSummary(true, "Creation was unsuccessful. Please correct the errors and try again.")

...

@Html.TextBoxFor(m => m.number, new { id = "txtnumber" })

...


<div id="editorPaymentRows">
    @foreach (var payment in Model.payment_methods)
    {
        @Html.Partial("_NewPayment", payment)
    }
</div>

}

In the _NewPayment partial:

@using prj.Helpers 
@model prj.Models.Model.PaymentMethod

<div class="editPaymentRow">

@using (Html.BeginCollectionItem("payment_methods"))
{
...

<div class="editor-label">
    @Html.LabelFor(m => m.tt.total_value)<req>*</req>      
</div>

<div class="editor-field">
    @Html.TextBoxFor(m => m.tt.total_value)
</div>

...


<div id="editorTransferRows">
   @if (Model.tt != null)
{
    foreach (var transfer in Model.tt.transfers)
    {
         @Html.Partial("_NewTransfer", transfer)
    }
}
...
</div>



}

</div>

and finally in the _NewTransfer partial:

@using prj.Helpers 
@model prj.Models.Model.Transfer
...

<div class="editTransferRow">
//using (Html.BeginCollectionItem("transfers"))
@using (Html.BeginCollectionItem("tt.transfers"))
{
...

<div class="editor-label">
    @Html.LabelFor(m => m.swift)<req>*</req>      
</div>

<div class="editor-field">
    @Html.TextBoxFor(m => m.swift, new { @class = "t_swift" })
</div>

...

<div class="editor-label">
    @Html.LabelFor(m => m.transfer_amount)<req>*</req>      
</div>

<div class="editor-field">
    @Html.TextBoxFor(m => m.transfer_amount, new { @class = "t_transfer_amount" })
</div>

...
}

</div>

So everything works, except in the controller the List transfers which is in TT property of the PaymentMethods is always null. It's not being passed to the controller properly. Is there something i'm missing?

Does nested BeginCollectionItem not work? is there an extra step I must do? Please shed some light. Thanks


I figured it out using Joe Stevens' method shown at the following link:

http://www.joe-stevens.com/2011/06/06/editing-and-binding-nested-lists-with-asp-net-mvc-2/

Cheers

like image 816
noobi Avatar asked Mar 05 '12 23:03

noobi


2 Answers

I couldn't properly adapt Job Stevens' method with MVC 5. I just use Job Stevens' below extension class with name BeginCollectionItem2

public static class HtmlPrefixScopeExtensions
{
    private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";

    public static IDisposable BeginCollectionItem2(this HtmlHelper html, string collectionName)
    {
        if (html.ViewData["ContainerPrefix"] != null)
        {
            collectionName = string.Concat(html.ViewData["ContainerPrefix"], ".", collectionName);
        }

        var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
        string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();

        var htmlFieldPrefix = string.Format("{0}[{1}]", collectionName, itemIndex);

        html.ViewData["ContainerPrefix"] = htmlFieldPrefix;

        // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
        html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));

        return BeginHtmlFieldPrefixScope(html, htmlFieldPrefix);
    }

    public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
    {
        return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
    }

    private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
    {
        // We need to use the same sequence of IDs following a server-side validation failure,  
        // otherwise the framework won't render the validation error messages next to each item.
        string key = idsToReuseKey + collectionName;
        var queue = (Queue<string>)httpContext.Items[key];
        if (queue == null)
        {
            httpContext.Items[key] = queue = new Queue<string>();
            var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
            if (!string.IsNullOrEmpty(previouslyUsedIds))
                foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
                    queue.Enqueue(previouslyUsedId);
        }
        return queue;
    }

    private class HtmlFieldPrefixScope : IDisposable
    {
        private readonly TemplateInfo templateInfo;
        private readonly string previousHtmlFieldPrefix;

        public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
        {
            this.templateInfo = templateInfo;

            previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
            templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
        }

        public void Dispose()
        {
            templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
        }
    }
}

scripts like below:

function addRow() {

        $.ajax({
            type: "POST",
            data: {processTypeId:@Model.Id},
            url: '@Url.Action("GetFlowItemRow", "Flow")',
            success: function (partialView) {
                $('#divItemList').append(partialView);
            }
        });
    }

 function addParameterRow(rw, prx) {

        $.ajax({
            type: "POST",
            url: '@Url.Action("GetFlowItemParameterRow", "Flow")' + '?pId=' + '@Model.Id' + '&prefix=' + prx ,
            success: function (partialView) {
                rw.closest('table').find("tbody").append(partialView);
            }
        });
    }

html buttons to add partial views like :

<a title="Add Operation" href="javascript:;" onclick="addRow()">
 <i class="la la-plus-circle"></i>
</a>
<a title="Add Operation Parameter" href="javascript:;" onclick="addParameterRow($(this),'@ViewData["ContainerPrefix"]')">
 <i class="la la-plus-circle"></i>
</a>

partial view methods on controller:

public PartialViewResult GetFlowItemRow(int? processTypeId)
    {
        FlowItemModel _item = new FlowItemModel() { ProcessTypeId = processTypeId ?? 0 };
        return PartialView("~/Views/Flow/Partial/_FlowItem.cshtml", _item);
    }

    public PartialViewResult GetFlowItemParameterRow(int? pId, string prefix)
    {
        ViewData["ContainerPrefix"] = prefix;
        FlowItemParameterModel _item = new FlowItemParameterModel() { };

        return PartialView("~/Views/Flow/Partial/_FlowItemParameter.cshtml", _item);
    }

Flow Item partial :

<tr>
        @using (Html.BeginCollectionItem2("OperationList"))
        {
            @Html.HiddenFor(model => model.ItemId)
            <td style="vertical-align:middle">
                @Html.TextBoxFor(m => m.Name, new { @class = "form-control" })
            </td>
            <td>
                <table style="width:100%">
                    <thead>
                        <tr>
                            <th class="kt-font-success">Name</th>
                            <th class="kt-font-success">Unit</th>

                            <th>
                                <a title="Add Parameter" href="javascript:;" onclick="addParameterRow($(this),'@ViewData["ContainerPrefix"]')">Add                                        
                                </a>
                            </th>
                        </tr>
                    </thead>
                    <tbody id="divParameterList">


                    </tbody>

                </table>

            </td>                
        }       
</tr>

Item Parameter Partial

<tr>
    @using (Html.BeginCollectionItem2("ParameterList"))
    {

        <td>@Html.TextBoxFor(m => m.ParameterName, new { @class = "form-control" })</td>
        <td>
            @Html.TextBoxFor(m => m.Unit, new { @class = "form-control" })
        </td>                                                
    }</tr>
like image 96
Bengü Hasdil Avatar answered Oct 19 '22 19:10

Bengü Hasdil


To get the prefix with an Html.BeginCollectionItem, you can access ViewData.TemplateInfo.HtmlFieldPrefix (I'm using the nuget package). You're on the right track with tt.transfers, but you need the specific prefix instead.

Instead of just

Html.BeginCollectionItem("tt.transfers")

you'll need the prefix of the current payment_method as well.

@{
    var paymentMethodPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;
}
@using (Html.BeginCollectionItem(paymentMethodPrefix + ".tt.transfers"))

and a quick test looks like you can also just:

@using (Html.BeginCollectionItem(ViewData.TemplateInfo.HtmlFieldPrefix + ".tt.transfers"))
like image 17
JonK Avatar answered Oct 19 '22 20:10

JonK