12

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

4

3 回答 3

17

要获取带有 的前缀Html.BeginCollectionItem,您可以访问ViewData.TemplateInfo.HtmlFieldPrefix(我使用的是 nuget 包)。您在正确的轨道上使用tt.transfers,但您需要特定的前缀。

而不仅仅是

Html.BeginCollectionItem("tt.transfers")

您还需要当前 payment_method 的前缀。

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

快速测试看起来你也可以:

@using (Html.BeginCollectionItem(ViewData.TemplateInfo.HtmlFieldPrefix + ".tt.transfers"))
于 2013-08-20T21:05:36.867 回答
3

我无法使用 MVC 5 正确调整 Job Stevens 的方法。我只是使用 Job Stevens 的下面扩展类,名称为 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;
        }
    }
}

如下脚本:

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 按钮添加部分视图,如:

<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>

控制器上的部分视图方法:

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);
    }

流项目部分:

<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>

项目参数部分

<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>
于 2020-01-16T12:57:10.423 回答
1

您可以使用 JonK answer 中的代码,但它不适用于从 ajax 调用动态添加的部分,因为“ViewData.TemplateInfo.HtmlFieldPrefix”为空。

解决方案是使用表单前缀向模型添加一个属性,并将其填充到 ajax 函数调用的操作中。

要使用 jQuery 获取表单前缀,您可以简单地执行以下操作:

// "this" a button that will trigger the ajax call and is within the div with the class "partial-enclosing-class"
var parentRow = $(this).parents('.partial-enclosing-class');
var hiddenInput = parentRow.find('input[name$="].Id"]');
var formPrefix = hiddenInput.prop('name').replace(".Id", "");
于 2020-08-04T17:16:48.197 回答