0

I followed the two postings below but just cannot get it to work: http://www.itorian.com/2013/04/nested-collection-models-in-mvc-to-add.html and Adding more items to nested collections in MVC?

I can edit and delete, but I cannot add additional Faq to a product with the Edit form. Appreciate if someone can help me.

I have 2 models - Product and Faq. I can add as many Faqs to a product as I want.

public class Product
{
    public Product()
    {
        this.Faqs = new HashSet<Faq>();
        this.Overviews = new HashSet<Overview>();
    }
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public virtual ICollection<Faq> Faqs { get; set; }
}

public class Faq
{
    public int FaqId { get; set; }
    public string FaqQuestion { get; set; }
    public string FaqAnswer { get; set; }
    [NotMapped]
    public Nullable<bool> DeleteFaq { get; set; }
    public int ProductId { get; set; }
    public virtual Product Product { get; set; }
}

I created an Editor Template in View/Product/EditorTemplates/Faq.cshtml:

@model xyz.Models.Faq
@using xyz.Helpers;

@{ Layout = null; }

@using (Html.BeginCollectionItem("Faqs"))
{ 
<div class="Faq">
@Html.HiddenFor(model => model.FaqId)
@Html.HiddenFor(model => model.ProductId)
    <div class="col-lg-12">
        @Html.LabelFor(model => model.FaqQuestion, htmlAttributes: new { @class = "control-label" })
    </div>
    <div class="col-lg-12">
        @Html.EditorFor(model => model.FaqQuestion, new { htmlAttributes = new { @class = "form-control" } })
    </div>

    <div class="col-lg-12">
        @Html.LabelFor(model => model.FaqAnswer, htmlAttributes: new { @class = "control-label" })
    </div>
    <div class="col-lg-12">
        @Html.EditorFor(model => model.FaqAnswer, new { htmlAttributes = new { @class = "form-control" } })
    </div>

    @Html.HiddenFor(model => model.DeleteFaq, new { @class = "mark-for-delete" })
    @Html.RemoveLink("Remove", "div.Faq", "input.mark-for-delete")

}

This is my Edit.cshtml page:

@model xyz.Models.Product 
@using xyz.Helpers

@{
ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm("Edit", "product", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
@Html.AntiForgeryToken()

<div class="form-horizontal">
    <h4>Product</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.ProductId)

    <div class="form-group">
        <div id="faqs">
            @Html.LabelFor(model => model.Faqs, "Faq", new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Faqs)
            </div>
        </div>
    </div>

    <p>
@Html.AddLink("Add More Faqs", "#faqs", ".ProductId", ".Faq", "Faqs", typeof(eqinsurance.Models.Faq), Convert.ToString(Model.ProductId))
    </p>

    <div class="form-group">
        @Html.LabelFor(model => model.ProductName, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.ProductName, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.ProductName, "", new { @class = "text-danger" })
        </div>
    </div>

I created an HTML Helper in Helpers/HtmlHelpers.cs, as follows:

namespace xyz.Helpers
{
public static class HtmlHelpers
{
    public static IHtmlString RemoveLink(this HtmlHelper htmlHelper, string linkText, string container, string deleteElement)
    {
        var js = string.Format("javascript:removeNestedForm(this,'{0}','{1}');return false;", container, deleteElement);
        TagBuilder tb = new TagBuilder("a");
        tb.Attributes.Add("href", "#");
        tb.Attributes.Add("onclick", js);
        tb.InnerHtml = linkText;
        var tag = tb.ToString(TagRenderMode.Normal);
        return MvcHtmlString.Create(tag);
    }

            public static IHtmlString AddLink<TModel>(this HtmlHelper<TModel> htmlHelper, string linkText, string containerElement, string parentId, string counterElement, string collectionProperty, Type nestedType, string buttonId)
    {
        var ticks = DateTime.UtcNow.Ticks;
        var nestedObject = Activator.CreateInstance(nestedType);
        var partial = htmlHelper.EditorFor(x => nestedObject).ToHtmlString().JsEncode();
        partial = partial.Replace("id=\\\"nestedObject", "id=\\\"" + collectionProperty + "_" + ticks + "_");
        partial = partial.Replace("name=\\\"nestedObject", "name=\\\"" + collectionProperty + "[" + ticks + "]");
        partial = partial.Replace(parentId + "\\\" type=\\\"hidden\\\" value=\\\"0", parentId + "\\\" type=\\\"hidden\\\" value=\\\"" + buttonId);
        var js = string.Format("javascript:addNestedForm('{0}','{1}','{2}','{3}');return false;", containerElement, counterElement, ticks, partial);
        TagBuilder tb = new TagBuilder("a");
        tb.Attributes.Add("href", "#");
        tb.Attributes.Add("onclick", js);
        tb.InnerHtml = linkText;
        var tag = tb.ToString(TagRenderMode.Normal);
        return MvcHtmlString.Create(tag);
    }

    private static string JsEncode(this string s)
    {
        if (string.IsNullOrEmpty(s)) return "";
        int i;
        int len = s.Length;
        StringBuilder sb = new StringBuilder(len + 4);
        string t;
        for (i = 0; i < len; i += 1)
        {
            char c = s[i];
            switch (c)
            {
                case '>':
                case '"':
                case '\\':
                    sb.Append('\\');
                    sb.Append(c);
                    break;
                case '\b':
                    sb.Append("\\b");
                    break;
                case '\t':
                    sb.Append("\\t");
                    break;
                case '\n':
                    break;
                case '\f':
                    sb.Append("\\f");
                    break;
                case '\r':
                    break;
                default:
                    if (c < ' ')
                    {
                        string tmp = new string(c, 1);
                        t = "000" + int.Parse(tmp, System.Globalization.NumberStyles.HexNumber);
                        sb.Append("\\u" + t.Substring(t.Length - 4));
                    }
                    else
                    {
                        sb.Append(c);
                    }
                    break;
            }
        }
        return sb.ToString();
    }
}
}

I have a CustomJs.js file that I also include in the App_start/BundleConfig.cs:

function removeNestedForm(element, container, deleteElement) {
$container = $(element).parents(container);
$container.find(deleteElement).val('True');
$container.hide();
}

function addNestedForm(container, counter, ticks, content) {
var nextIndex = $(counter).length;
var pattern = new RegExp(ticks, "gi");
content = content.replace(pattern, nextIndex);
$(container).append(content);
}

And here's my Edit methods in ProductController:

    // GET: Product/Edit/5
    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }

        Product product = db.Product.Find(id);
        if (product == null)
        {
            return HttpNotFound();
        }

        return View(product);
    }

    // POST: Product/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(Product product)
    {
        if (ModelState.IsValid)
        {
            foreach (var item in product.Faqs)
            {
                if (item.DeleteFaq == true)
                {
                    db.Entry(item).State = EntityState.Deleted;
                }
                else if (item.FaqId == 0)
                {
                    db.Entry(item).State = EntityState.Added;
                }
               else
                {
                    db.Entry(item).State = EntityState.Modified;
                }
                db.SaveChanges();
            }

            db.Entry(product).State = EntityState.Modified;
            db.SaveChanges();

          return RedirectToAction("Details", new { id = product.ProductId });
        }
Community
  • 1
  • 1
Ng Peter
  • 1
  • 1
  • Unclear what your trying to do with this code. You not adding a hidden input for the indexer property so the `DefaultModelBinder` will not match up the collection. Refer the answers [here](http://stackoverflow.com/questions/29161481/post-a-form-array-without-successful/29161796#29161796) and [here](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) for some options for how to dynamically add and remove collection items. –  Nov 16 '15 at 07:33
  • Hi Stephen, yes, the DefaultModelBinder is not matching up the new Faqs collection, when I dynamically add new Faqs to the Product on Edit Post method. I tried placing the @using(Html.BeginCollectionItem()) in the Editor Template but I get an error message. Must it always go with a Partial View? The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.Faq_dbo.Product_ProductId". The conflict occurred in database "eqinsurance", table "dbo.Product", column 'ProductId'. The statement has been terminated. Can you teach how to insert the "hidden input for the indexer property"? – Ng Peter Nov 17 '15 at 00:15
  • Here's how I added the BeginCollectionItem. @using(Html.BeginCollectionItem("Faqs")) {
    @Html.HiddenFor(model => model.FaqId) @Html.HiddenFor(model => model.ProductId)
    @Html.LabelFor(model => model.FaqQuestion, htmlAttributes: new { @class = "control-label" })
    ...
    – Ng Peter Nov 17 '15 at 00:16
  • Using `BeginCollectionItem()` automatically adds the input (if you inspect the html you will see `` where `###` is a `Guid`). If your not using the helper (so you can do this all client side) then look at the links in my comment above - e.g. the first one has `` (in your case it would be `name="Faqs.Index"`) –  Nov 17 '15 at 00:22
  • I added BeginCollectionItem() to my EdtorTemplate and it added the hidden inputs : . It can bind the dynamically added Faq in the Edit Page. However, a new problem arises. No matter how many new Faqs I added, it post the details of the first Faq to all the Faqs. Is there something wrong with the ticks in my AddLink Htmlhelper? var ticks = DateTime.UtcNow.Ticks; – Ng Peter Nov 18 '15 at 05:59
  • You cannot use `BeginCollectionItem` in an `EditorTemplate` - it needs to be a partial and called in the main view using ``@Html.Partial()` inside a `foreach` loop –  Nov 18 '15 at 06:05
  • Dear Stephen, thank you so much. I solve it using BeginCollectionItem and Partial. I can finally edit, add and delete all the faqs in the product edit form. – Ng Peter Nov 20 '15 at 03:51

0 Answers0