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!
You want knockout MVC http://knockoutmvc.com/CartEditor
You don't have to use knockout for this, what you really need is javascript with validations and create/delete actions which map onto restful controller actions on the MVC side of things. How you go about implementing that is up to you. Knockout makes it easy though.
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);
}
You need to do following:
- Send a model to your controller and render one editable field for term.
- You ask user to submit this or click on add to add more terms.
- If user clicks on add you create a copy of existing fields and empty them or whatever so they can create new.
- When user submits you send it back to action that accepts array of terms and add it to database or what not. Or you can use ajax like the one in above example or from this example you can see what it sends to server is json array object and not form with named elements.
- You can process them as they get created or you can process them on submit.
Depends on your application as well so knockout is just helping you on client for step 3 where you are creating new from old copy. You can sue JQuery templates for that as well and json2 to serialise with old browser support.
All you need to understand is that once you are on client you have sent your model here once so dont worry about server side. Whatever you build at client side can be send either one model at a time to saveTerm action which retruns json with term id and other info or you can return a collection of saveTerm as a json array and it would work fine.
if you are thinking of sending an array on postBack and not ajax just keep the form element names same and duplicate term input fields and send over. MVC will map them to a array of terms as it does with json.