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 implemen
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"))
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>
You can use the code from JonK answer but it will not work for dynamically added partials from ajax calls because "ViewData.TemplateInfo.HtmlFieldPrefix" comes empty.
The solution is to add a property to your model with the form prefix and fill it in on the action called by the ajax function.
To fetch the form prefix using jQuery you can simply do:
// "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", "");