Pergunta

I parecem estar tendo um problema com ASP.NET MVC em que, se eu tiver mais de um formulário em uma página que usa o mesmo nome em cada um, mas como diferentes tipos (rádio / oculto / etc), então , quando os postos de primeiros formulário (I escolher o botão de opção 'Data', por exemplo), se o formulário é re-processado (digamos, como parte da página de resultados), me parece ter o problema que o valor oculto do Tipo de pesquisa sobre as outras formas é alterado para o valor último botão de rádio (neste caso, SearchType.Name).

A seguir é um exemplo de formulário para fins de redução.

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.RadioButton("SearchType", SearchType.Date, true) %>
  <%= Html.RadioButton("SearchType", SearchType.Name) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.Hidden("SearchType", SearchType.Colour) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.Hidden("SearchType", SearchType.Reference) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>

Resultando fonte da página (isso seria parte da página de resultados)

<form action="/Search/Search" method="post">
  <input type="radio" name="SearchType" value="Date" />
  <input type="radio" name="SearchType" value="Name" />
  <input type="submit" name="submitForm" value="Submit" />
</form>

<form action="/Search/Search" method="post">
  <input type="hidden" name="SearchType" value="Name" /> <!-- Should be Colour -->
  <input type="submit" name="submitForm" value="Submit" />
</form>

<form action="/Search/Search" method="post">
  <input type="hidden" name="SearchType" value="Name" /> <!-- Should be Reference -->
  <input type="submit" name="submitForm" value="Submit" />
</form>

Por favor, alguém pode ter com confirm RC1 isso?

Talvez seja porque eu estou usando um enum. Eu não sei. Devo acrescentar que eu posso contornar esse problema usando entrada 'manual' () etiquetas para os campos ocultos, mas se eu usar tags MVC (<% = Html.Hidden (...)%>), .NET MVC substitui-los de cada vez.

Muito obrigado.

Update:

Eu vi esse erro novamente hoje. Parece que este surge a sua cabeça quando você voltar uma página postada e uso MVC set escondido marcas de formulário com o HTML helper. Entrei em contato com Phil Haack sobre isso, porque eu não sei para onde me virar, e eu não acredito que este deve ser o comportamento esperado, conforme especificado pelo David.

Foi útil?

Solução

Sim, este comportamento é atualmente pelo design. Mesmo que você está definindo explicitamente valores, se você enviar de volta para o mesmo URL, olhamos no estado de modelo e usar o valor lá. Em geral, isso nos permite exibir o valor apresentado em postagem, em vez do valor original.

Existem duas soluções possíveis:

Solução 1

Use nomes exclusivos para cada um dos campos. Note que, por padrão, usamos o nome especificado como o id do elemento HTML. É HTML inválido ter vários elementos têm o mesmo id. Então, usando nomes exclusivos é uma boa prática.

Solução 2

Não use o ajudante escondido. Parece que você realmente não precisa. Em vez disso, você poderia fazer isso:

<input type="hidden" name="the-name" 
  value="<%= Html.AttributeEncode(Model.Value) %>" />

Claro que, como eu penso sobre isso mais, alterando o valor com base em uma nova postagem faz sentido para Textboxes, mas faz menos sentido para as entradas escondidas. Nós não podemos mudar isso para v1.0, mas vou considerá-lo para v2. Mas precisamos pensar cuidadosamente as implicações de tal mudança a.

Outras dicas

O mesmo que outros que eu teria esperado o ModelState para ser usado para preencher o modelo e como usamos explicitamente o modelo em expressões no ponto de vista, ele deve usar o modelo e não ModelState.

É uma escolha de design e eu entendo por que: se validações falhar, o valor de entrada pode não ser analisável para o tipo de dados no modelo e você ainda deseja processar qualquer valor errado o usuário digitou, por isso é fácil de corrigir.

A única coisa que eu não entendo é: porque não é por design que o modelo é usado, que é definido explicitamente pelo desenvolvedor e se ocorreu um erro de validação, o ModelState é usado

Eu vi muitas pessoas usando soluções como

  • ModelState.Clear (): Limpa todos os valores ModelState, mas basicamente desabilita o uso de validação padrão no MVC
  • ModelState.Remove ( "SomeKey"): O mesmo que ModelState.Clear (), mas precisa de microgestão de chaves ModelState, o que é muito trabalho e que não se sente bem com o recurso de ligação automática de MVC. Sente como 20 anos atrás, quando nós também foram gerenciamento de chaves Formulário e QueryString.
  • Rendering HTMLthemselves: muito trabalho, detalhes e joga fora os métodos de ajuda HTML com os recursos adicionais. Um exemplo:. Substituir @ Html.HiddenFor por m.Name)" id = "@ Html.IdFor (m => m.Name)" value = "@ Html.AttributeEncode (Model.Name)"> Ou substitua @Html. DropDownListFor por ...
  • Criar personalizado HTML Helpers para substituir padrão MVC HTML Helpers para evitar o problema by-design. Esta é uma abordagem mais genérica, em seguida, tornar o seu HTML, mas ainda requer mais conhecimento de HTML + MVC ou Decompiling System.Web.Mvc ainda manter todos os outros recursos, mas desativar precedência ModelState sobre o modelo.
  • Aplicar o padrão POST-REDIRECT-GET: isso é fácil em alguns ambientes, mas mais difícil em aqueles com mais interação / complexidade. Este padrão tem suas vantagens e desvantagens e você não deve ser forçado a aplicar este padrão por causa de uma escolha por-design de ModelState sobre o modelo.

Edição

Assim, a questão é que o modelo está cheio de ModelState e na visão que definir explicitamente para usar o modelo. Todo mundo espera que o valor do modelo (no caso de ele mudou) a ser utilizado, a menos que há um erro de validação; em seguida, o ModelState pode ser usado.

Actualmente as extensões MVC Helper o valor ModelState recebe precedência sobre o valor do modelo.

Solução

Assim, a correção real para este problema deve ser: para cada expressão para puxar o valor Modelo do valor ModelState deve ser removido se não houver nenhum erro de validação para esse valor. Se há um erro de validação para que o controle de entrada o valor ModelState não deve ser removido e ele será usado como normal. Eu acho que isso resolve o problema exatamente, o que é melhor, em seguida, a maioria das soluções alternativas.

O código é aqui:

    /// <summary>
    /// Removes the ModelState entry corresponding to the specified property on the model if no validation errors exist. 
    /// Call this when changing Model values on the server after a postback, 
    /// to prevent ModelState entries from taking precedence.
    /// </summary>
    public static void RemoveStateFor<TModel, TProperty>(this HtmlHelper helper,  
        Expression<Func<TModel, TProperty>> expression)
    {
        //First get the expected name value. This is equivalent to helper.NameFor(expression)
        string name = ExpressionHelper.GetExpressionText(expression);
        string fullHtmlFieldName = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

        //Now check whether modelstate errors exist for this input control
        ModelState modelState;
        if (!helper.ViewData.ModelState.TryGetValue(fullHtmlFieldName, out modelState) ||
            modelState.Errors.Count == 0)
        {
            //Only remove ModelState value if no modelstate error exists,
            //so the ModelState will not be used over the Model
            helper.ViewData.ModelState.Remove(name);
        }
    }

E, em seguida, criamos nossas próprias extensões auxiliar HTML Todo este antes de chamar as extensões MVC:

    public static MvcHtmlString TextBoxForModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        string format = "",
        Dictionary<string, object> htmlAttributes = null)
    {
        RemoveStateFor(htmlHelper, expression);
        return htmlHelper.TextBoxFor(expression, format, htmlAttributes);
    }

    public static IHtmlString HiddenForModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression)
    {
        RemoveStateFor(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression);
    }

Esta solução elimina o problema, mas não exige que você descompilar, analisar e reconstruir tudo o MVC está lhe oferecendo normalmente (não se esqueça mudanças também gerir ao longo do tempo, as diferenças de navegador, etc.).

Eu acho que a lógica do "valor Modelo, a menos erro de validação, em seguida, ModelState" deve ter sido by-design. Se fosse, não teria mordido tantas pessoas, mas ainda coberto o MVC foi destinado TODO.

Eu apenas correu em mesmo problema. Html ajudantes como TextBox () precedência para valores passados ??parecem se comportar exatamente o oposto que eu inferida a partir da Documentação onde diz:

O valor do elemento de entrada de texto. Se este valor é referência nula (Nada no Visual Basic), o valor do elemento é obtida a partir o objecto ViewDataDictionary. Se nenhum valor existe lá, o valor é recuperada do objeto ModelStateDictionary.

Para mim, eu li que o valor, se aprovada for utilizada. Mas TextBox leitura () fonte:

string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);

parece indicar que a ordem real é exatamente o oposto do que está documentado. ordem real parece ser:

  1. ModelState
  2. ViewData
  3. Valor (passou para TextBox () pelo chamador)

Heads-up - esse bug ainda existe em MVC 3. Eu estou usando a navalha marcação sintaxe (como o que realmente importa), mas eu encontrei o mesmo erro com um loop foreach que produziu o mesmo valor para uma propriedade do objeto cada vez.

Este seria o behavoir esperado - MVC não usar um viewstate ou outro atrás de seus truques de volta para passar informações extra na forma, por isso não tem nenhuma idéia que formam você enviou (o nome do formulário não faz parte dos dados submetido, apenas uma lista de pares nome / valor).

Quando MVC torna o formulário de volta, é simplesmente verificar se um valor enviado com o mesmo nome existe - mais uma vez, ele não tem como saber que formam um valor chamado veio, ou mesmo que tipo de controle foi de (se você usar um rádio, texto ou oculto, é tudo apenas name = valor quando seu enviado através de HTTP).

foreach (var s in ModelState.Keys.ToList())
                if (s.StartsWith("detalleProductos"))
                    ModelState.Remove(s);

ModelState.Remove("TimeStamp");
ModelState.Remove("OtherOfendingHiddenFieldNamePostedToSamePage1");
ModelState.Remove("OtherOfendingHiddenFieldNamePostedToSamePage2");

return View(model);

Exemplo para reproduzir o "problema de projeto", e uma possível workaroud. Não existe nenhuma solução para as 3 horas perdidas tentando encontrar o "bug" embora ... Note-se que este "design" ainda está em ASP.NET MVC 2.0 RTM.

    [HttpPost]
    public ActionResult ProductEditSave(ProductModel product)
    {
        //Change product name from what was submitted by the form
        product.Name += " (user set)";

        //MVC Helpers are using, to find the value to render, these dictionnaries in this order: 
        //1) ModelState 2) ViewData 3) Value
        //This means MVC won't render values modified by this code, but the original values posted to this controller.
        //Here we simply don't want to render ModelState values.
        ModelState.Clear(); //Possible workaround which works. You loose binding errors information though...  => Instead you could replace HtmlHelpers by HTML input for the specific inputs you are modifying in this method.
        return View("ProductEditForm", product);
    }

Se o formulário contém originalmente esta: <%= Html.HiddenFor( m => m.ProductId ) %>

Se o valor original de "Nome" (quando o formulário foi prestado) é "fictício", depois que o formulário é enviado você espera ver "dummy (conjunto de utilizador)" prestados. Sem ModelState.Clear() você ainda vai ver "dummy" !!!!!!

solução alternativa correta:

<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />

Eu sinto que este não é um bom design em tudo, como toda forma MVC desenvolvedor precisa manter isso em mente.

Esse problema ainda existe em MVC 5, e claramente não é considerado um bug que é bom.

Estamos descobrindo que, embora pelo design, este não é o comportamento esperado para nós. Ao contrário, nós queremos sempre o valor do campo oculto para operar de forma semelhante a outros tipos de campos e não ser tratados especial, ou puxar seu valor de alguma coleção obscura (que nos lembra de ViewState!).

Alguns resultados (valor correto para nós é o valor do modelo, incorreto é o valor ModelState):

  • Html.DisplayFor() exibe o valor correto (ele puxa a partir Model)
  • Html.ValueFor não (ele puxa a partir ModelState)
  • faz
  • ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model puxa o valor correto

A nossa solução é implementar simplesmente a nossa própria extensão:

        /// <summary>
        /// Custom HiddenFor that addresses the issues noted here:
        /// http://stackoverflow.com/questions/594600/possible-bug-in-asp-net-mvc-with-form-values-being-replaced
        /// We will only ever want values pulled from the model passed to the page instead of 
        /// pulling from modelstate.  
        /// Note, do not use 'ValueFor' in this method for these reasons.
        /// </summary>
        public static IHtmlString HiddenTheWayWeWantItFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                    Expression<Func<TModel, TProperty>> expression,
                                                    object value = null,
                                                    bool withValidation = false)
        {
            if (value == null)
            {
                value = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model;
            }

            return new HtmlString(String.Format("<input type='hidden' id='{0}' name='{1}' value='{2}' />",
                                    htmlHelper.IdFor(expression),
                                    htmlHelper.NameFor(expression),
                                    value));
        }

Isto pode ser 'por design', mas não é o que está documentado:

Public Shared Function Hidden(  

  ByVal htmlHelper As System.Web.Mvc.HtmlHelper,  
  ByVal name As String, ByVal value As Object)  
As String  

Member of System.Web.Mvc.Html.InputExtensions

Resumo:. Retorna uma tag escondida

Parâmetros:
HtmlHelper:. O HTML helper
Nome: O nome do campo de formulário e chave System.Web.Mvc.ViewDataDictionary usado para procurar o valor
. Valor: O valor da entrada escondida. Se nulo, olha para o System.Web.Mvc.ViewDataDictionary e, em seguida, System.Web.Mvc.ModelStateDictionary para o valor.

Isto parece sugerir que somente quando o parâmetro valor é nulo (ou não especificado) faria o olhar HtmlHelper em outra parte para um valor.

Em meu aplicativo, eu tenho um formulário onde: html.Hidden ( "remoto", True) está rendendo como <input id="remote" name="remote" type="hidden" value="False" />

Observe o valor está sendo suplantada por que é no dicionário ViewData.ModelState.

Ou estou faltando alguma coisa?

Assim, em MVC 4 o "problema de projeto" ainda está lá. Aqui está o código que eu tinha que usar, a fim de definir os valores escondidos corretos em uma coleção desde independentemente do que eu faço no controlador, a visão sempre mostrou valores incorretos.

Código OLD

for (int i = 0; i < Model.MyCollection.Count; i++)
{
    @Html.HiddenFor(m => Model.MyCollection[i].Name) //It doesn't work. Ignores what I changed in the controller
}

código atualizado

for (int i = 0; i < Model.MyCollection.Count; i++)
{
    <input type="hidden" name="MyCollection[@(i)].Name" value="@Html.AttributeEncode(Model.MyCollection[i].Name)" /> // Takes the recent value changed in the controller!
}

Será que eles fixa isso em MVC 5?

Não há solução alternativa:

    public static class HtmlExtensions
    {
        private static readonly String hiddenFomat = @"<input id=""{0}"" type=""hidden"" value=""{1}"" name=""{2}"">";
        public static MvcHtmlString HiddenEx<T>(this HtmlHelper htmlHelper, string name, T[] values)
        {
            var builder = new StringBuilder(values.Length * 100);
            for (Int32 i = 0; i < values.Length; 
                builder.AppendFormat(hiddenFomat,
                                        htmlHelper.Id(name), 
                                        values[i++].ToString(), 
                                        htmlHelper.Name(name)));
            return MvcHtmlString.Create(builder.ToString());
        }
    }

Como já foi sugerido, eu fui com o uso de código html direto em vez de usar os HtmlHelpers (TextBoxFor, CheckBoxFor, HiddenFor etc.).

O problema, porém com esta abordagem é que você precisa colocar o nome e id atributos como strings. Eu queria manter meus Propriedades modelo fortemente tipado então eu usei o NameFor e IdFor HtmlHelpers.

<input type="hidden" name="@Html.NameFor(m => m.Name)" id="@Html.IdFor(m=>m.Name)" value="@Html.AttributeEncode(Model.Name)">

Update: Aqui está uma extensão HtmlHelper acessível

    public static MvcHtmlString MyHiddenFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, object htmlAttributes = null)
    {
        return new MvcHtmlString(
            string.Format(
                @"<input id=""{0}"" type=""hidden"" value=""{1}"" name=""{2}"">",
                helper.IdFor(expression),
                helper.NameFor(expression),
                GetValueFor(helper, expression)
            ));
    }

    /// <summary>
    /// Retrieves value from expression
    /// </summary>
    private static string GetValueFor<TModel, TValue>(HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression)
    {
        object obj = expression.Compile().Invoke(helper.ViewData.Model);
        string val = string.Empty;
        if (obj != null)
            val = obj.ToString();
        return val;
    }

Você pode então usá-lo como

@Html.MyHiddenFor(m => m.Name)
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top