ASP.NET MVC, usando Array, List, IEnumerable no ViewModel e ModelBind
Um problema comum que sempre me fazia perder um bom tempo e deixava o código um pouco mais sujo era trabalhar com Arrays ou melhor, IEnumerable's em geral. Normalmente tenho um IEnumerable<AlgumaCoisa> no meu ViewModel e quero usar todas as funcionalidades do ASP.NET MVC normalmente com o Model Binding.
Assumindo o seguinte ViewModel:
public class PredioViewModel
{
public string Nome { get; set; }
public List<AndarViewModel> Andares { get; set; }
}
public class AndarViewModel
{
public int Id { get; set;}
public string Nome { get; set; }
public decimal Preco { get; set; }
}
A primeira idéia que passa é fazer o seguinte:
@using (Html.BeginForm())
{
@Html.TextBoxFor(x => x.Nome)<br/>
foreach (var andar in Model.Andares)
{
@Html.HiddenFor(x => andar.Id)<br/>
@Html.CheckBoxFor(x => andar.Marcado)<br/>
@Html.TextBoxFor(x => andar.Nome)<br/>
@Html.TextBoxFor(x => andar.Preco)<br/>
}
<input type="submit"/>
}
O que a princípio funciona. Exibe todos os dados corretamente. Porém cada campo do Andar é gerado com o mesmo nome em todos os itens da coleção:
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="andar_Id" name="andar.Id" type="hidden" value="1" /><br/>
<input data-val="true" data-val-required="The Marcado field is required." id="andar_Marcado" name="andar.Marcado" type="checkbox" value="true" /><input name="andar.Marcado" type="hidden" value="false" /><br/>
<input id="andar_Nome" name="andar.Nome" type="text" value="Andar1" /><br/>
<input data-val="true" data-val-number="The field Preco must be a number." data-val-required="The Preco field is required." id="andar_Preco" name="andar.Preco" type="text" value="100000" /><br/>
<input id="andar_Id" name="andar.Id" type="hidden" value="2" /><br/>
<input id="andar_Marcado" name="andar.Marcado" type="checkbox" value="true" /><input name="andar.Marcado" type="hidden" value="false" /><br/>
<input id="andar_Nome" name="andar.Nome" type="text" value="Andar2" /><br/>
<input id="andar_Preco" name="andar.Preco" type="text" value="200000" /><br/>
Além de ser incorreto do ponto de vista do código HTML, já que temos Ids repetidos, complica no ModelBind, pois o ASP.NET MVC não tem como diferenciar qual campo é de cada item na coleção, deixando ela null:
Ainda assim é possível obter os valores brutos, via FormCollection, mas o código já começa a ficar com "string mágicas" dependente de detalhes de implementação e do próprio nome da Property, que pode mudar no futuro.
Além disso no caso de checkbox, quando ele está marcado, o valor postado é true,false o que dificulta a interpretação, não permitindo um simples Split por ","
Imagine ainda se cada andar tiver uma coleção de Apartamentos!
Mas já existe uma solução para isso, apesar de não ser tão conhecida. O Html.EditorFor mapeia automaticamente coleções, assim você pode passar uma coleção pro método que ele vai procurar um EditorTemplate pra classe dos itens dentro da coleção. No nosso caso ao passar um List<AndarViewModel> o EditorFor reconhece que é uma coleção de AndarViewModel e renderiza o EditorTemplate do AndarViewModel para cada item na coleção.
Assim em vez do nosso foreach, escrevemos apenas:
@Html.EditorFor(x=>x.Andares)
E criamos o EditorTemplate de AndarViewModel. Atenção que Model do EditorTemplate é AndarViewModel e não List<AndarViewModel>, pois o EditorFor faz esse mapeamento.
@model ViewModelComIEnumerable.ViewModel.AndarViewModel
@Html.HiddenFor(x => Model.Id)<br />
@Html.CheckBoxFor(x => Model.Marcado)<br />
@Html.TextBoxFor(x => Model.Nome)<br />
@Html.TextBoxFor(x => Model.Preco)<br />
E o HTML resultante agora é diferente, especificando a posição na coleção:
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Andares_0__Id" name="Andares[0].Id" type="hidden" value="1" /><br />
<input data-val="true" data-val-required="The Marcado field is required." id="Andares_0__Marcado" name="Andares[0].Marcado" type="checkbox" value="true" /><input name="Andares[0].Marcado" type="hidden" value="false" /><br />
<input id="Andares_0__Nome" name="Andares[0].Nome" type="text" value="Andar1" /><br />
<input data-val="true" data-val-number="The field Preco must be a number." data-val-required="The Preco field is required." id="Andares_0__Preco" name="Andares[0].Preco" type="text" value="100000" /><br />
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Andares_1__Id" name="Andares[1].Id" type="hidden" value="2" /><br />
<input data-val="true" data-val-required="The Marcado field is required." id="Andares_1__Marcado" name="Andares[1].Marcado" type="checkbox" value="true" /><input name="Andares[1].Marcado" type="hidden" value="false" /><br />
<input id="Andares_1__Nome" name="Andares[1].Nome" type="text" value="Andar2" /><br />
<input data-val="true" data-val-number="The field Preco must be a number." data-val-required="The Preco field is required." id="Andares_1__Preco" name="Andares[1].Preco" type="text" value="200000" /><br />
E o Model Binding funciona perfeitamente e o melhor, o código fica sem gambiarras!
Estou disponibilizando o projeto que usei como exemplo para quem tiver dificuldades em reproduzir: ViewModelComIEnumerable.zip
Fonte: http://stackoverflow.com/questions/3484935/asp-net-mvc-2-model-with-array-list#3485013