1

I don't understand why a variable that I am setting on the model is coming down to the view, but not coming back up. Its 'feels' like a strange bug in HiddenFor()

I've written a simple example that reproduces the problem:

Model:

public class SampleModel
{
    public string SpecialString { get; set; }
    public string FileString { get; set; }
}

View:

@model FormResubmitTest.Test.SampleModel
....
@using (Html.BeginForm())
{
    @Html.ValidationSummary(false)
    if (@Model.FileString != null)
    {
        <p>@Model.FileString file exists</p>
    }
    <div>
        @(Html.Kendo().Upload()
            .Name("uploadDocument")
            .Multiple(false)
            .ShowFileList(true)
            .Messages(o => o.Select("Select File To upload"))
        )
    </div>
    @Html.HiddenFor(model => model.FileString)
    @Html.TextBoxFor(model => model.SpecialString)
    <input type="submit" name="submit" value="Submit" />
}

Controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new SampleModel(){});
    }

    [HttpPost]
    public ActionResult Index(SampleModel model, HttpPostedFileBase uploadDocument)
    {
        if (uploadDocument != null)
        {
            model.FileString = SaveToTemporaryFile(uploadDocument);
        }
        if (model.SpecialString != "Special")
        {
            ModelState.AddModelError("SpecialString", "Special string was not special");
        }
        if (uploadDocument == null && model.FileString == null)
        {
            ModelState.AddModelError("FileString", "You have not uploaded a file");
        }
        if (ModelState.IsValid)
        {
            return RedirectToAction("Success");
        }
        return View(model);
    }

    public string SaveToTemporaryFile(HttpPostedFileBase file)
    {
        if (file == null)
        {
            return null;
        }
        var path = Path.GetTempPath();
        var folder = Path.Combine(path, Guid.NewGuid().ToString());
        Directory.CreateDirectory(folder);
        var fileName = file.FileName;
        fileName = System.IO.Path.GetFileName(fileName) ?? "file.txt";
        var fullFileName = Path.Combine(folder, fileName);
        using (FileStream fileStream = System.IO.File.Create(fullFileName, (int)file.InputStream.Length))
        {
            byte[] bytesInStream = new byte[file.InputStream.Length];
            file.InputStream.Read(bytesInStream, 0, bytesInStream.Length);
            fileStream.Write(bytesInStream, 0, bytesInStream.Length);
        }
        return fullFileName;
    }
}

To see error:

  • Upload a file
  • Enter a string that is not "Special"
  • Hit Submit
  • It will now display the FileString correctly
  • Enter "Special"
  • Hit Submit
  • The function Index will have a model with a blank FileString

I dont undertand why on the second call, the model has a blank filestring. Now if I look at the hidden for generated code its quite clear, the value is blank!

<input id="FileString" name="FileString" type="hidden" value="" />

I've put the full below, but why on earth is its value blank!!? We can see from the generated message; that the server is aware of it at generation.... I am so confused!

---EDIT---

I can make this work by doing this:

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

instead of

@Html.HiddenFor(model => model.FileString)

but it seems wrong that I have to do it like this

Does MVC (or the Html library I should say) somehow remember the original posted values? and uses them in the "Fors"

chrispepper1989
  • 2,100
  • 2
  • 23
  • 48
  • Note this is not specific to hiddenfor, it seems to happen to TextBoxFor, can I not edit fields on the model?? – chrispepper1989 Dec 18 '17 at 15:05
  • You only initialize ""FileString" if the file was uploaded, and on the second run you don't upload anything. Hence the empty value – Andrei Dec 18 '17 at 15:06
  • but it should be set via the first run? why is the second submit seemingly sending up the first state the model was in??? – chrispepper1989 Dec 18 '17 at 15:08
  • Note that you do "redirect", which pretty much wipes the model values – Andrei Dec 18 '17 at 15:10
  • only on success, if the validation fails (which it will do due to lack of "Special") it just does return View(model) and like I say, it does receive the correct version of the model, as my 'hacky' solution shows* – chrispepper1989 Dec 18 '17 at 15:11
  • *solution shows and the original string that gets displayed – chrispepper1989 Dec 18 '17 at 15:14
  • 1
    I usually declare hiddenFor fields at the top and outside beginform. – bilpor Dec 18 '17 at 15:29
  • @bilpor that fixes it! I would love to know why! but at the minute thats the best answer so I would mark as so if you post it :) – chrispepper1989 Dec 18 '17 at 15:35
  • it's because beginform forces everything inside to be re-rendered. – bilpor Dec 18 '17 at 15:37
  • So therefore its bad practice to put hiddenfor within the begin form? is this what caused the textboxfor to fail aswel? it makes so little sense to me though because you can certainly send down pre-filled values (at least from the first call) – chrispepper1989 Dec 18 '17 at 15:43
  • 1
    yes. Usually we use hiddenFor's as a way of passing values between forms. Often once populated, a bit of javascript is used to move this value from the hiddenfor to the item being seen by the user. It's also partly why you find many places using libraries like Anglar that also use an MVC pattern as it can be 'cleaner' if used correctly. – bilpor Dec 18 '17 at 15:48
  • 1
    @bilpor, would you mind post this as an elaborate answer? that's an interesting insight – Andrei Dec 18 '17 at 16:29
  • 1
    @Andrei I've consolidated my comments into an answer. Hope this helps – bilpor Dec 18 '17 at 16:37
  • 1
    What are you expecting to happen. You enter a value which is not Special" which results in adding a `ModelStateError` so you return the same view (i.e it will display exactly what you entered because the HtmlHelper` methods use the values from `ModelState` is they exist - refer [this answer](https://stackoverflow.com/questions/26654862/textboxfor-displaying-initial-value-not-the-value-updated-from-code/26664111#26664111) for a detailed explanation). –  Dec 18 '17 at 20:49

2 Answers2

1

The behavior your seeing is by design. All the HtmlHelper methods that generate form controls (except PasswordFor()) use the value from ModelState (if they exist), then from the ViewDataDictionary, and then from the model property.

When you first generate the view, no values have been added to ModelState, and the value of FileString is null so it generates <input ... value="" />

When you submit the form, the values of each property in your model are added to ModelState by the DefaultModelBinder (in the case of FileString, its value is null). You then modify the value of FileString and return the view.

The HiddenFor() method now finds a value of null in ModelState and again generates <input ... value="" /> (setting the value in the POST method does not override the value in ModelState).

If you want to return a different view, then you should be following the PRG pattern and redirecting, however you can solve this by removing the value from ModelState so the HiddenFor() method uses the value from the model.

You can clear all values from ModelState using

ModelState.Clear()

or remove ModelState for just one property using

ModelState.Remove("FileString");
  • Thank you stephen, this was really baking my noodle, the hidden for didnt really make sense but it did work, your explanation will help me sleep better :p I really like your explanation on the other question I think you should link to it on this answer in case people dont find the comment, the "im five next week" really helped expain it. Can you also provide a link for the PRG pattern? i'm looking at this one https://www.stevefenton.co.uk/2011/04/asp-net-mvc-post-redirect-get-pattern/ i believe i am using PRG but im open to correction :) – chrispepper1989 Dec 19 '17 at 09:31
  • @chrispepper1989. That article explains the PRG well. And you are redirecting when `ModelState` is invalid. But the purpose of using `return View(..)` is just that - to return the view (the same view that the user submitted). In your case your trying to modify the view (or at least the model that uses that view) so the correct approach would be to redirect to show the new view. –  Dec 21 '17 at 00:26
  • 1
    However in your case, where you wanting to return the view but maintain data about a file that's already been uploaded, I would suggest that the simplest approach is to include the following in the view `@if(Model.FileString != null) { @Html.HiddenFor(model => model.FileString) }` so that initially no input is generated, therefore a value is not submitted on the first submit, which means a value is not added to `ModelState` in the first place. Now when you return the view, the `HiddenFor()` method will then use the value from the model –  Dec 21 '17 at 00:31
  • 1
    That then allows you to still get the benefits of utilizing the `HtmlHelper` method. –  Dec 21 '17 at 00:33
0

You need to move the hiddenFor outside the beginform. I usually place all of these at the top. anything inside the beginform is re-rendered on a re-post which is why the hiddenfor's dont get populated because within this everything is treated as a refreshed form.

Hidden elements are often used as a way of passing values between forms (better than placing it on a querystring). If the form values to be passed start to be too many then usually we start to introduce other javascript libraries such as Angular which also uses an MVC pattern to move our objects around the forms as this is usually cleaner as long as Angular or other libraries are implemented correctly.

bilpor
  • 3,467
  • 6
  • 32
  • 77
  • This is nonsense and wrong. If an input is not inside the form its value is never submitted to the server, making having it pointless. –  Dec 18 '17 at 20:52
  • @StephenMuecke I wasn't talking about input's being outside the form. Only the hiddenFor's – bilpor Dec 19 '17 at 09:01
  • What do you think `HiddenFor()` generates - answer: an `` :) And if its outside the form then its value will not be sent in the request when the form is submitted and the value of `FileString` will always be `null` in the POST method. –  Dec 21 '17 at 00:19