28

Having issues with the view state on a series of page views -- On the initial view of a page in Razor I am using Html.HiddenFor as follows:

    @Html.HiddenFor(x => Model.err)
    @Html.HiddenFor(x => Model.errField)
    @Html.HiddenFor(x => Model.errMessage)
    @Html.HiddenFor(x => Model.IsMove)

which seems to work fine. My hidden input tags contain the correct values. However when I submit the form [HTTPPost] and update the model in my controller action with..

       model.err = transHelper.err;
       model.errField = transHelper.errField;
       model.errMessage = transHelper.errMessage;
       return View(model);

The hidden fields do not seem to update, they contain the original values from the initial view. However When I use these fields in another context within the same razor view like this...

     @*      
        this seems to not update correctly...

    @Html.HiddenFor(x => Model.err)
    @Html.HiddenFor(x => Model.errField)
    @Html.HiddenFor(x => Model.errMessage)
    @Html.HiddenFor(x => Model.IsMove)

        *@
        <input type="hidden" id="err" value="@Model.err" />
        <input type="hidden" id="errField" value="@Model.errField" />
        <input type="hidden" id="errMessage" value="@Model.errMessage" />
        <input type="hidden" id="IsMove" value="@Model.IsMove" />

    </div>

Then the input fields update correctly. I even created a View Helper to help debug, and in all cases, the Model seems to have correct data in HtmlHelper<TModel> -- I even returned the Model as return Json(model); and the data was fine.

At this point I am running with the work around, but does anybody know why @Html.HiddenFor is dirty.

Update: here is my controller actions

  [HttpPost]
   public ActionResult Index(HomePageModel model)
   {


       // process transaction
       Transactionr transr = new Transactionr();
       transr.Process(model);

       model.err = transr.err;
       model.errField = transr.errField;
       model.errMessage = transr.errMessage;

       return View(model);
   }

Here is my view:

        @model App.Models.HomePageModel
    @{
        ViewBag.Title = "Product Categorizer";
    }
    <form id="formData" method="post" action="/Home/Index">
        @Html.AntiForgeryToken()
        <fieldset>
            <div>

            @Html.HiddenFor(model => model.err)
            @Html.HiddenFor(model => model.errField)
            @Html.HiddenFor(model => model.errMessage)
            @Html.HiddenFor(model => model.IsMove)

            <input type="hidden" id="myerr" value="@Model.err" />
            <input type="hidden" id="myerrField" value="@Model.errField" />

            </div>

           <div class="section group">
                <div class="col span_2_of_2">
                     <div class="message" id ="message">
                         @if (Model.err < 0)
                         {
                             <span style="color: purple;">@Model.errMessage (@Model.err) - (@Model.errField)</span>
                         }
                         else if (Model.err > 0)
                         {
                             <span style="color:red;">@Model.errMessage (@Model.err) (@Model.errField)</span>
                         } else {
                            <span>@Model.errMessage (@Model.err) (@Model.errField)</span>
                         }
                         </div>
                     </div>
            </div>

            <div class="section group" id="workspace">
                  @Html.Partial("_WorkspacePartial", Model)
            </div>
              <div class="section group" id="details">
                  @Html.Partial("_DetailPartial", Model)
              </div>


        </fieldset>
        </form>

Here is my model:

 public class HomePageModel
 {
    public int FromStore { get; set; }

    //  the "To" part of the copy/move transaction
    public int ToStore { get; set; }

    // a list of the copy/move transaction
    public List<int> Details { get; set; }


    // true is move false is copy
    public bool IsMove { get; set; }

    // current message
    public int err { get; set; }
    public int errField { get; set; }
    public string errMessage { get; set; }
0

7 Answers 7

40

The default HtmlHelpers behavior (@Html.HiddenFor, etc) is to behave exactly as you have described.

i.e. any changes you make to the ViewModel on a post are actioned, any changes you return from the Post are received by the view, but on re-rendering WITH HTMLHELPERS, the previous Post-values take precedence over the changed ViewModel values.

Want to "fix" this behavior in a quick + dirty way, clear the ModelState.Clear() prior to returning from the HttpPost ActionMethod !

Sign up to request clarification or add additional context in comments.

5 Comments

This is very dirty because all the Validation messages are cleared then as well, so be aware!
I thought I was going crazy.
I don't get it. There's no Required attribute on any properties of the model. Why returning the view was getting the HiddenFor empty for ONE property ? After doing the ModelState.Clear I was getting the value all right !!!
WHY _ clear the ModelState.Clear() prior to returning from the HttpPost ActionMethod_ ?
Odd behavior (still persists in Razor Pages). It seems to occur because By default, the validation system treats non-nullable parameters or properties as if they had a [Required] attribute. according to the docs (learn.microsoft.com/en-us/aspnet/core/mvc/models/…), but even a single string type (nullable) model property used as hidden-input (via asp-for) is affected by this behavior, and its value is silently reverted to the previous value.
14

As mentioned by joedotnot this is intended behaviour. Another 'quick fix' for this is to code the html for the hidden field and update only the value from the model eg:

<input type="hidden" id="ErrMessage" name="ErrMessage" value="@Model.ErrMessage">

Use the same id and name as your model property and the updated value will be rendered after postback.

Comments

7

I've faced similar problem recently and ended up with writing new simple helper method + 2 overloads. I'm sharing it here in case anybody is still looking for some workaround cause this "feature" is sometimes annoying.

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;

        if (modelState.ContainsKey(fullName))
        {
            ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            ValueProviderResult currentValue = modelState[fullName].Value;
            modelState[fullName].Value = new ValueProviderResult(metadata.Model, Convert.ToString(metadata.Model), currentValue.Culture);
        }
    }
}

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.

Comments

3

I think you should be using them like this instead:

@Html.HiddenFor(x => x.Err)
@Html.HiddenFor(x => x.ErrField)
@Html.HiddenFor(x => x.ErrMessage)
@Html.HiddenFor(x => x.IsMove)

Without seeing your model, I am assuming it looks something like this:

public class ErroViewModel
{
  public string Err { get; set; }
  public string ErrField { get; set; }
  public string ErrMessage { get; set; }
  public bool IsMove { get; set; }
}

If not it should be similar with public properties as above.

Update

In your get do you have the following?

public ActionResult Index(HomePageModel model)
{
   var model = new HomePageModel();
   return View(model);
}

I would also change your form from this:

 <form id="formData" method="post" action="/Home/Index">

To this:

@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
  // rest of form
}

6 Comments

My Model is using public properties. In terms of the expression, I've tried everything including what you suggest, and it doesn't seem to have any impact.
Yes, in my get am creating an instance of HomePageModel.. as for the form.. I am doing some form manipulation with jquery so I've coded it like that.
Hmmm, not sure what the problem could be as it all looks good for me. Still an issue?
Its the ModelState that is causing it. See the other answer.
Why not use [HttpPost] attribute ?
|
0

I had a similar issue and solved it like this.

@Html.TextBoxFor(m => m.Email, new { onclick = "this.select()", Value = Model.Email, Placeholder = "E-Mail" })

Comments

-1

You can try

<input type="hidden" id="SomeFieldID" name="SomeFieldID" value="@Model.SomeFieldID" />

1 Comment

This looks like it was already covered it maxscan's answer posted two years earlier
-1

You need a key in your view model. If you don't have any property named as Id in your view model, set one of them (expected to be uniqe identifiyer) as key with [Key].

[Key]
Myproperty {get;set;}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.