Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC4 how to dynamically add line item to EditorFor field?

I have a view model witch contains iterative items. I place them in my view via the EditorFor() method.

View:

@model Models.MyModel 

@using (Html.BeginForm(@Model.Action, @Model.Controller))
{
    <div class="section" id="Terms">
        @Html.EditorFor(m => m.Terms)
    </div>

    <input type="submit" value="Save" />
}

Model:

public class MyModel 
{
    public IEnumerable<Term> Terms  { get; set; }
}

EditorTemplates\Term.cshtml:

@model Models.Term

@if (Model != null) 
{
    <fieldset>
        <legend>Term</legend>

        @Html.HiddenFor(model => model.TermID)

        <div class="editor-label">
            @Html.LabelFor(model => model.Identifier)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Identifier)
            @Html.ValidationMessageFor(model => model.Identifier)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Description)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Description)
            @Html.ValidationMessageFor(model => model.Description)
        </div>

    </fieldset> 
}

I want to be able to dynamically add / remove items from the list in the view, like this example on knockout.js, but how do I preserve the auto-id's MVC creates??:

http://knockoutjs.com/examples/cartEditor.html

Here are my requirements for this:

  • Add new terms
  • Remove terms
  • validate the new terms views that are added

I've read other questions on SO and I haven't found a real definitive answer on this. Is knockout.js the accepted way to do this? Are there any examples of doing this with Knockout AND MVC?

Thanks!

like image 582
Steve Stokes Avatar asked Feb 05 '13 03:02

Steve Stokes


1 Answers

I found this post Nested Collection Models in MVC3 by Jarrett Meyer, who has a solution that doesn't use knockout and maximizes code reuse.

This covers both add and delete methods. I'll outline the add method here.

The Model

public class Person {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public IList<PhoneNumber> PhoneNumbers { get; set; }
  public IList<EmailAddress> EmailAddresses { get; set; }
  public IList<Address> Addresses { get; set; }
}

Views

//New.cshtml:    
@using (Html.BeginForm("New", "Person", FormMethod.Post))
{
  @Html.EditorForModel()
  <p>
    <button type="submit">
      Create Person
    </button>
  </p>
}

//Person.cshtml:
@Html.AntiForgeryToken()
@Html.HiddenFor(x => x.Id)

<p>
  <label>First Name</label>
  @Html.TextBoxFor(x => x.FirstName)
</p>
<p>
  <label>Last Name</label>
  @Html.TextBoxFor(x => x.LastName)
</p>
<div id="phoneNumbers">
  @Html.EditorFor(x => x.PhoneNumbers)
</div>
<p>
  @Html.LinkToAddNestedForm("Add Phone Number", "#phoneNumbers", ".phoneNumber", "PhoneNumbers", typeof(PhoneNumber))
</p>

//PhoneNumber.cshtml:
<div class="phoneNumber">
  <p>
    <label>Telephone Number</label>
    @Html.TextBoxFor(x => x.Number)
  </p>
  <br/>
</div>

Helper

/// <param name="linkText">Text for Link</param>
/// <param name="containerElement">where this block will be inserted in the HTML using a jQuery append method</param>
/// <param name="counterElement">name of the class inserting, used for counting the number of items on the form</param>
/// <param name="collectionProperty">the prefix that needs to be added to the generated HTML elements</param>
/// <param name="nestedType">The type of the class you're inserting</param>
public static IHtmlString LinkToAddNestedForm<TModel>(this HtmlHelper<TModel> htmlHelper, string linkText,
    string containerElement, string counterElement, string collectionProperty, Type nestedType)
{
    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 + "]");

    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':
                //sb.Append("\\n");
                break;
            case '\f':
                sb.Append("\\f");
                break;
            case '\r':
                //sb.Append("\\r");
                break;
            default:
                if (c < ' ')
                {
                    //t = "000" + Integer.toHexString(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();
}

Javascript

//since the html helper can change the text of the item inserted but not the index,
//this replaces the 'ticks' with the correct naming for the collection of properties
function addNestedForm(container, counter, ticks, content) {
  var nextIndex = $(counter).length;
  var pattern = new RegExp(ticks, "gi");

  content = content.replace(pattern, nextIndex);
  $(container).append(content);
} 
like image 186
w00ngy Avatar answered Nov 15 '22 00:11

w00ngy