ASP.NET MVC: Como manter o estado de caixa de texto quando seu ViewModel é uma lista / IEnumerable coleção /
-
18-09-2019 - |
Pergunta
Eu estou usando ASP.NET MVC 2 Beta. Eu posso criar um assistente como fluxo de trabalho usando a técnica de Steven Sanderson (em seu livro Pro ASP.NET MVC Framework), utilizando Session em vez de campos de formulários ocultos para preservar os dados através de pedidos. I pode ir e voltar entre as páginas e manter os valores em uma caixa de texto sem qualquer problema quando o meu modelo não é uma coleção. Um exemplo seria um modelo de pessoa simples:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
Mas eu sou incapaz de chegar a este trabalho quando eu passar em torno de um IEnumerable. No meu ponto de vista eu estou tentando executar através do Modelo e gerar uma caixa de texto para o nome e e-mail para cada pessoa na lista. I pode gerar a boa forma e eu posso enviar o formulário com os meus valores e ir para a Etapa 2. Mas quando eu clico no botão Voltar no Passo 2 leva-me de volta ao Passo 1 com uma forma vazia. Nenhum dos campos que anteriormente povoada estão lá. Deve haver algo que eu estou ausente. alguém pode me ajudar?
Aqui é a minha opinião:
<% using (Html.BeginForm()) { %>
<% int index = 0;
foreach (var person in Model) { %>
<fieldset>
<%= Html.Hidden("persons.index", index.ToString())%>
<div>Name: <%= Html.TextBox("persons[" + index.ToString() + "].Name")%></div>
<div>Email: <%= Html.TextBox("persons[" + index.ToString() + "].Email")%></div>
</fieldset>
<% index++;
} %>
<p><input type="submit" name="btnNext" value="Next >>" /></p>
<% } %>
E aqui está o meu controlador:
public class PersonListController : Controller
{
public IEnumerable<Person> persons;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
persons = (Session["persons"]
?? TempData["persons"]
?? new List<Person>()) as List<Person>;
// I've tried this with and without the prefix.
TryUpdateModel(persons, "persons");
}
protected override void OnResultExecuted(ResultExecutedContext filterContext)
{
Session["persons"] = persons;
if (filterContext.Result is RedirectToRouteResult)
TempData["persons"] = persons;
}
public ActionResult Step1(string btnBack, string btnNext)
{
if (btnNext != null)
return RedirectToAction("Step2");
// Setup some fake data
var personsList = new List<Person>
{
new Person { Name = "Jared", Email = "test@email.com", },
new Person { Name = "John", Email = "test2@email.com" }
};
// Populate the model with fake data the first time
// the action method is called only. This is to simulate
// pulling some data in from a DB.
if (persons == null || persons.Count() == 0)
persons = personsList;
return View(persons);
}
// Step2 is just a page that provides a back button to Step1
public ActionResult Step2(string btnBack, string btnNext)
{
if (btnBack != null)
return RedirectToAction("Step1");
return View(persons);
}
}
Solução
Tanto quanto eu posso dizer, isso não é suportado em ASP.NET MVC 2 Beta, nem é suportado em ASP.NET MVC 2 RC. Cavei através do código fonte MVC e olha modelos como dicionários são suportados mas não que são IEnumerable <> (ou que contêm IEnumerable aninhados objetos) e é herdeiros como IList <>.
A questão está na classe ViewDataDictionary. Particularmente, o método GetPropertyValue só fornece uma maneira de obter valores de propriedade de propriedades de dicionário (chamando GetIndexedPropertyValue) ou propriedades simples usando o método PropertyDescriptor.GetValue para retirar o valor.
Para corrigir isso, eu criei um método GetCollectionPropertyValue que lida com modelos que são coleções (e até mesmo modelos que contêm coleções aninhadas). Eu estou colando o código aqui para referência. Nota: Eu não fazer quaisquer afirmações sobre elegância - na verdade, toda a análise de cadeia é muito feio, mas parece estar funcionando. Aqui está o método:
// Can be used to pull out values from Models with collections and nested collections.
// E.g. Persons[0].Phones[3].AreaCode
private static ViewDataInfo GetCollectionPropertyValue(object indexableObject, string key)
{
Type enumerableType = TypeHelpers.ExtractGenericInterface(indexableObject.GetType(), typeof(IEnumerable<>));
if (enumerableType != null)
{
IList listOfModelElements = (IList)indexableObject;
int firstOpenBracketPosition = key.IndexOf('[');
int firstCloseBracketPosition = key.IndexOf(']');
string firstIndexString = key.Substring(firstOpenBracketPosition + 1, firstCloseBracketPosition - firstOpenBracketPosition - 1);
int firstIndex = 0;
bool canParse = int.TryParse(firstIndexString, out firstIndex);
object element = null;
// if the index was numeric we should be able to grab the element from the list
if (canParse)
element = listOfModelElements[firstIndex];
if (element != null)
{
int firstDotPosition = key.IndexOf('.');
int nextOpenBracketPosition = key.IndexOf('[', firstCloseBracketPosition);
PropertyDescriptor descriptor = TypeDescriptor.GetProperties(element).Find(key.Substring(firstDotPosition + 1), true);
// If the Model has nested collections, we need to keep digging recursively
if (nextOpenBracketPosition >= 0)
{
string nextObjectName = key.Substring(firstDotPosition+1, nextOpenBracketPosition-firstDotPosition-1);
string nextKey = key.Substring(firstDotPosition + 1);
PropertyInfo property = element.GetType().GetProperty(nextObjectName);
object nestedCollection = property.GetValue(element,null);
// Recursively pull out the nested value
return GetCollectionPropertyValue(nestedCollection, nextKey);
}
else
{
return new ViewDataInfo(() => descriptor.GetValue(element))
{
Container = indexableObject,
PropertyDescriptor = descriptor
};
}
}
}
return null;
}
E aqui é o método GetPropertyValue modificado que chama o novo método:
private static ViewDataInfo GetPropertyValue(object container, string propertyName) {
// This method handles one "segment" of a complex property expression
// First, we try to evaluate the property based on its indexer
ViewDataInfo value = GetIndexedPropertyValue(container, propertyName);
if (value != null) {
return value;
}
// If the indexer didn't return anything useful, continue...
// If the container is a ViewDataDictionary then treat its Model property
// as the container instead of the ViewDataDictionary itself.
ViewDataDictionary vdd = container as ViewDataDictionary;
if (vdd != null) {
container = vdd.Model;
}
// Second, we try to evaluate the property based on the assumption
// that it is a collection of some sort (e.g. IList<>, IEnumerable<>)
value = GetCollectionPropertyValue(container, propertyName);
if (value != null)
{
return value;
}
// If the container is null, we're out of options
if (container == null) {
return null;
}
// Third, we try to use PropertyDescriptors and treat the expression as a property name
PropertyDescriptor descriptor = TypeDescriptor.GetProperties(container).Find(propertyName, true);
if (descriptor == null) {
return null;
}
return new ViewDataInfo(() => descriptor.GetValue(container)) {
Container = container,
PropertyDescriptor = descriptor
};
}
Novamente, isso é no arquivo ViewDataDictionary.cs em ASP.NET MVC 2 RC. Devo criar um novo problema para rastrear isso no MVC site do CodePlex?