ASP.Net MVC Html.HiddenFor with wrong value

2019-01-03 01:49发布

I'm using MVC 3 in my project, and I'm seeing a very strange behavior.

I'm trying to create a hidden field for a particular value on my Model, the problem is that for some reason the value set on the field does not correspond to the value in the Model.

e.g.

I have this code, just as a test:

<%:Html.Hidden("Step2", Model.Step) %>
<%:Html.HiddenFor(m => m.Step) %>

I would think that both hidden fields would have the same value. What I do is, set the value to 1 the first time I display the View, and then after the submission I increase the value of the Model field by 1.

So, the first time I render the page both controls have the value 1, but the second time the values rendered are these:

<input id="Step2" name="Step2" type="hidden" value="2" />
<input id="Step" name="Step" type="hidden" value="1" />

As you can see, the first value is correct, but the second value seems to be the same as the first time I display the View.

What am I missing? Are the *For Html helpers caching the values in some way? If so, how can I disable this caching?.

Thanks for your help.

4条回答
老娘就宠你
2楼-- · 2019-01-03 02:17

I encountered the same problem when writing a Wizard that shows different parts of a larger model at every step.
Data and/or Errors from "Step 1" would become mixed up with "Step 2", etc, until I finally realized that ModelState was to 'blame'.

This was my simple solution:

if (oldPageIndex != newPageIndex)
{
    ModelState.Clear(); // <-- solution
}

return View(model[newPageIndex]);
查看更多
看我几分像从前
3楼-- · 2019-01-03 02:19

I am too struggling with the same situation I think, where I use the same model state between calls, and when I alter a model property on backend. Though, it does not matter for me, if I use textboxfor or hiddenfor.

I just bypass the situation by using page scripts to store the model value as a js variable, because I need the hiddenfield for that purpose in the beginning.

Not sure if this helps but just consider..

查看更多
Fickle 薄情
4楼-- · 2019-01-03 02:29

This code will not work

// remove the Step variable from the model state
// if you want the changes in the model to be
// taken into account
ModelState.Remove("Step");
model.Step = 2;

...because HiddenFor always (!) reads from ModelState not the model itself. And if it doesn't find the "Step" key it will produce the default for that variable type which will be 0 in this case

Here is the solution. I wrote it for myself but don't mind sharing it cause I see many people are struggling with this naughty HiddenFor helper.

public static class CustomExtensions
{
    public static MvcHtmlString HiddenFor2<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
    {
        ReplacePropertyState(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression);
    }

    public static MvcHtmlString HiddenFor2<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
    {
        ReplacePropertyState(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression, htmlAttributes);
    }

    public static MvcHtmlString HiddenFor2<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
    {
        ReplacePropertyState(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression, htmlAttributes);
    }

    private static void ReplacePropertyState<TModel, TProperty>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
    {
        string text = ExpressionHelper.GetExpressionText(expression);
        string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(text);
        ModelStateDictionary modelState = htmlHelper.ViewContext.ViewData.ModelState;
        ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);

        if (modelState.ContainsKey(fullName))
        {                
            ValueProviderResult currentValue = modelState[fullName].Value;
            modelState[fullName].Value = new ValueProviderResult(metadata.Model, Convert.ToString(metadata.Model), currentValue.Culture);
        }
        else
        {
            modelState[fullName] = new ModelState
            {
                Value = new ValueProviderResult(metadata.Model, Convert.ToString(metadata.Model), CultureInfo.CurrentUICulture)
            };
        }
    }
}

Then you just use it as usual from within you view:

@Html.HiddenFor2(m => m.Id)

It worth to mention it works with collections too.

查看更多
干净又极端
5楼-- · 2019-01-03 02:36

That's normal and it is how HTML helpers work. They first use the value of the POST request and after that the value in the model. This means that even if you modify the value of the model in your controller action if there is the same variable in the POST request your modification will be ignored and the POSTed value will be used.

One possible workaround is to remove this value from the model state in the controller action which is trying to modify the value:

// remove the Step variable from the model state 
// if you want the changes in the model to be
// taken into account
ModelState.Remove("Step");
model.Step = 2;

Another possibility is to write a custom HTML helper which will always use the value of the model and ignore POST values.

And yet another possibility:

<input type="hidden" name="Step" value="<%: Model.Step %>" />
查看更多
登录 后发表回答