0

I have a Person class that has a navigation property ICollection<Address> Addresses. The Address entity has a property City.

I want to give the user an option to create/update a Person along with its addresses, including adding and removing ones.

So for displaying the addresses I simply call @Html.EditorFor(m => m.Addresses), and the razor engine takes care of the collection using my custom template that resides in the EditorTemplates folder, generating foreach field a matching signature like id="Addresses_0__City" name="Addresses[0].City".
So far so good.

The problem is adding new Addresses.
I created a button that when clicked, a jQuery function calls an action (that's rendered via the custom EditorTemplate), but its fields don't have that signature as above, but just id="City" name="City", and thus, isn't recognized in the post action as part of the Person entity.

How do I have those id and name fields generated with the correct signature?

I've read this article and many others, but found none that address the ids and names issue.

Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632
  • A couple of options for dynamically adding new objects shown [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). Using `BeginCollectionItem` means calling the server each time but is easier to maintain. Using a template means its all done client side but you need to update the template if your class ever changes and can get messy if you have validation attributes as well. –  Mar 29 '15 at 02:11
  • I don't mind using the server for it, but I don't want to involve additional NuGet packages. I there a simple way to maintain this via server or jQ (no querying DB)? – Shimmy Weitzhandler Mar 29 '15 at 02:20
  • So you want to add/update _just_ the `Address` portion via that jquery click and not the entire `Person` entity? – ethorn10 Mar 29 '15 at 02:22
  • It's the whole `Person` entity. My question is what `name` and `id` attributes should be given to newly added rows, and whether there is a simple way to generate it. – Shimmy Weitzhandler Mar 29 '15 at 02:23
  • Without jquery or using the `BeginCollectionItem` helper, there is really no option but to post back the model to a method that then adds a new item to your collection then return the view, so the existing and the newly added items are rendered correctly in the loop with the indexers in the name attributes. –  Mar 29 '15 at 02:32
  • I assume using a client templating approach? I had to use my own defined template (I was using kendo UI) and not use EditorFor. In my approach, I rendered a template using Html.NameFor, gave it a bogus index reference -99, and in javascript replaced -99 with the correct index. It wasn't easy and took a lot of work to ensure the ID's matched the existing server-generated IDs. – Brian Mains Mar 29 '15 at 02:34
  • @StephenMuecke: Wrong. Everything that can be done with jQuery can also be done with straight DOM. It's just more code. – SLaks Mar 29 '15 at 02:35
  • I was having this exact problem and I found this post with a good solution. It uses a custom HtmlHelper, which may be a little different than what you were expecting. http://www.mattlunn.me.uk/blog/2014/08/how-to-dynamically-via-ajax-add-new-items-to-a-bound-list-model-in-asp-mvc-net/ – Mahmoud Ali Mar 29 '15 at 02:36
  • @BrianMains Yeah. I thought about giving the newly created entity an Id of `-1` but I prefer the non-hardcoded way. – Shimmy Weitzhandler Mar 29 '15 at 02:36
  • @SLaks, And how would it be possible to dynamically add new items to the DOM without using jquery or posting back? –  Mar 29 '15 at 02:37
  • @StephenMuecke: How do you think jQuery works? `document.createElement()` and `appendChild()`. – SLaks Mar 29 '15 at 02:46
  • @SLaks without getting into the argument which I agree with (jQ is built on top of play JS), I do happen to use jQ. And Schabse, what do you do in these one-to-many cases? – Shimmy Weitzhandler Mar 29 '15 at 02:49
  • @SLaks, I was referring to jquery because that's what OP referred to. I am well aware you could use vanilla javascript. My comment was in relation to an option of not using jquery/javascript or the `BeginCollectionItem` helper. –  Mar 29 '15 at 02:50
  • Please vote on [this](https://aspnet.uservoice.com/forums/41201-asp-net-mvc/suggestions/7364410-please-provide-an-easy-way-to-handle-one-to-many-m) suggestion. – Shimmy Weitzhandler Mar 29 '15 at 05:29
  • @StephenMuecke I'm looking right now at your answer [here](http://stackoverflow.com/a/25735425/75500), I'm sure I'm sure the solution I'm looking for is involved with the `ViewData.TemplateInfo` properties. I wanna create a helper method that takes the collection and generates a new field including IDs. – Shimmy Weitzhandler Mar 29 '15 at 05:51
  • @Shimmy, That wont necessarily solve the problem. One issue is that `EditorFor()` renders indexers starting `0` and are consecutive. If you dynamically delete an item, binding will fail on post back, so you need to generate existing items in a `for` loop with an special hidden input for an `.Index` property as per [this answer](http://stackoverflow.com/questions/29161481/post-a-form-array-without-successful/29161796#29161796) (not using `EditorFor()`) –  Mar 29 '15 at 06:00
  • For dynamically adding new items, you could possibly pass a prefix with an indexer (say "Addresses[#]") where `#` is based on the number of current items, but again, if you added an item and then deleted it or another one, the indexers would not be consecutive so binding would fail. –  Mar 29 '15 at 06:05
  • You can check out here. It may help you - http://stackoverflow.com/questions/29044104/asp-net-mvc-creating-an-object-with-related-object-in-one-view/29046544#29046544 – vortex Mar 29 '15 at 07:38
  • @Shimmy please read this post: http://www.mattlunn.me.uk/blog/2014/08/how-to-dynamically-via-ajax-add-new-items-to-a-bound-list-model-in-asp-mvc-net/ It solves your problem. I tested the solution and the model is correctly populated after the POST. – Mahmoud Ali Mar 29 '15 at 12:09
  • Hello again everybody and thanks for all your help. I made an extension method wrapping up everybody's solutions and suggestions. [Here](http://stackoverflow.com/a/29346074/75500) it is. HTH. – Shimmy Weitzhandler Apr 17 '15 at 01:54

1 Answers1

1

I ended up using the following as per the suggestions on comments.

Anyway it bothered me that I have to wrap the new item as collection, and that the hidden field is just appended after the collection item, rather than being injected to (because at removal it stays there).

So I ended up adding the following extensions to be used both on the Razor cshtml files, and on the action that's called when adding a new item to the collection:

Here are the extensions (there are some more overloads, please see the full code here):

private static string EditorForManyInternal<TModel, TValue>(HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, IEnumerable<TValue> collection, string templateName)
{
  var sb = new StringBuilder();

  var prefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
  var htmlFieldName = (prefix.Length > 0 ? (prefix + ".") : String.Empty) + ExpressionHelper.GetExpressionText(expression);

  var items = collection ?? expression.Compile()(html.ViewData.Model);
  foreach (var item in items)
  {
    var guid = Guid.NewGuid().ToString();

    var dummy = new { Item = item };
    var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
    var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, expression.Parameters);

    var editor = html.EditorFor(singleItemExp, templateName, string.Format("{0}[{1}]", htmlFieldName, guid));
    var hidden = String.Format(@"<input type='hidden' name='{0}.Index' value='{1}' />", htmlFieldName, guid);

    var eNode = HtmlNode.CreateNode(editor.ToHtmlString().Trim());
    if (eNode is HtmlTextNode)
      throw new InvalidOperationException("Unsuported element.");

    if (eNode.GetAttributeValue("id", "") == "")
      eNode.SetAttributeValue("id", guid);

    var hNode = HtmlNode.CreateNode(hidden);
    eNode.AppendChild(hNode);
    sb.Append(eNode.OuterHtml);
  }

  return sb.ToString();
}

public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string templateName)
{
  var value = EditorForManyInternal(html, expression, null, templateName);
  return new MvcHtmlString(value);
}

Usage in view:

<div>
  <h4>@Resources.Person.Children</h4>
  <ul id="patientChildren" class="list-group ajax-collection">
    @Html.EditorForMany(m => m.Children)
  </ul>

  @Ajax.ActionLink("Create Child", "CreateChild", new { patientId = Model.Id, lastName = Model.LastName }, new AjaxOptions { UpdateTargetId = "patientChildren", InsertionMode = InsertionMode.InsertAfter, OnSuccess = CommonData.AjaxOnSuccessJsFuncName }, new { @class = "button btn-default" })
</div>

Here's the ajax function being called (it's important to have the generated items classed with ajax-collection-item and have a remove button classed btn remove):

//#region Ajax add and remove
var ajaxCollectionItemSelector = '.ajax-collection-item';
function attachAjaxRemoveHandlers(id) {
  var context = $(id ? '#' + id : ajaxCollectionItemSelector);

  var removeButton = context.find('.btn.remove');

  removeButton.click(function () {
    var button = $(this);
    var collectionItem = button.closest(ajaxCollectionItemSelector);
    collectionItem.remove();
  });
};

function ajaxOnSuccess(ajaxContext) {
  var collectionItem = $(ajaxContext);
  var id = collectionItem.prop('id');
  attachAjaxRemoveHandlers(id);
  //TODO: following line doesn't work
  collectionItem.find(':text:first-of-type').focus();
};

function runCommonScripts() {
  attachAjaxRemoveHandlers();
};
//#endregion Ajax add and remove

The new item action (CreateChild) looks like the following (the EditorForSingle extension is on the same place:

public ContentResult CreateChild(int patientId, string lastName)
{
  return this.EditorForSingle((Patient p) => p.Children, 
    new PatientChild
    { 
      PatientId = patientId, 
      LastName = lastName 
    });
}
Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632
  • I have just stumbled in the same problem but I needed more flexibility in the implementation as I was dealing with wizards that had to include several lists of different items with slightly different layouts. I therefore created a simple templating engine for lists that is now available on NuGet: https://dynamic-vml.github.io/ – Cesar May 09 '20 at 13:29