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:
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"
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");