Why is string sent down on Model and in HiddenFor, Not coming back up in form 'Resubmit' (post manual validation error)

821 views Asked by At

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"

2

There are 2 answers

4
AudioBubble On BEST ANSWER

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");
3
bilpor On

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.