Based on the article by Steve Sanderson, I was able to find the elegant solution I was looking for:
First of all, the model should be modified as follows:
public class SimpleModel
{
public IEnumerable<EmailAddress> EmailAddresses { get; set; }
}
public class EmailAddress
{
[Required(ErrorMessage = "Email Address is required.")]
[DataType(DataType.EmailAddress)]
[DisplayName("Email Address")]
public string Value { get; set; }
}
The controller method that processes the GET method must preinstall the model with as many entries as required:
[HandleError]
public class SimpleController : Controller
{
public ActionResult Simple()
{
SimpleModel model = new SimpleModel
{
EmailAddresses =
new List<EmailAddress>
{
new EmailAddress { Value = string.Empty },
new EmailAddress { Value = string.Empty },
new EmailAddress { Value = string.Empty }
}
};
return View(model);
}
[HttpPost]
public ActionResult Simple(SimpleModel model)
{
if (ModelState.IsValid)
{
}
return View(model);
}
}
The view also needs to be changed:
<% using (Html.BeginForm()) { %>
<%= Html.ValidationSummary(true, "The form submitted is not valid.") %>
<div>
<fieldset>
<% foreach (var item in Model.EmailAddresses)
Html.RenderPartial("SimpleRows", item);
%>
<div class="editor-field">
<input type="submit" value="Submit" />
</div>
</fieldset>
</div>
<% } %>
... . , .
<% using(Html.BeginCollectionItem("EmailAddresses")) { %>
<div class="editor-label">
<%= Html.LabelFor(x => x.Value)%>
</div>
<div class="editor-field">
<%= Html.TextBoxFor(x => x.Value)%>
<%= Html.ValidationMessageFor(x => x.Value)%>
</div>
<% }%>
BeginCollectionItem - , Sanderson:
public static class HtmlPrefixScopeExtensions
{
private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";
public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
{
var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();
html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));
return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
}
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)
{
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;
}
}
}
... ... , , POST.
, , , .