I've done a little research on this and it always says the same thing, make sure you have enctype for "multipart/form-data" when trying to do an ajax post with a file to an mvc controller. As far as I can tell, I have done that yet still when I run through the debugger, HttpPostedFileBase always returns null. I'm not seeing where I'm missing something probably very obvious.
Here is the razor view form.
@using (Html.BeginForm("Edit", "Story", FormMethod.Post, new { encType = "multipart/form-data" }))
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(x => x.StoryId)
<div class="form-group">
<div class="row">
<div class="col-sm-4">
<div class="card">
<div class="card-body">
<p class="card-text">Add/Edit cover art to your story</p>
<img class="img-thumbnail" src="~/Imgs/@Model.CoverArt" />
<input type="file" id="file" name="file" class="form-control" />
</div>
</div>
</div>
<div class="col-sm-8"></div>
</div>
</div>
<div class="form-row">
<div class="col-6">
@Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label text-muted" })
@Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
</div>
<div class="col-3">
@Html.LabelFor(x => x.Genre, htmlAttributes: new { @class = "control-label text-muted" })
@Html.DropDownListFor(x => x.Genre, new SelectList(Model.Genres, "Id", "Name"), new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Genre, "", new { @class = "text-danger" })
</div>
<div class="col-3">
@Html.Label("Visibility", htmlAttributes: new { @class = "control-label text-muted" })
<i class="fa fa-question-circle-o" data-toggle="popover" title="What does visibility mean?" data-placement="right" data-content="Visibility determines whether other users can see this story or not. Public means any other user can read, vote, and review the story. You can change this to private, which will hide the story from other users. You, as the author, will be able to see all of your stories - both public and prviate - under your account."></i>
@Html.DropDownListFor(x => x.Visibility, new SelectList(Model.Visibilities, "Id", "Name"), new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Visibility, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-row mt-3">
<div class="col-6">
@Html.Label("Story Type", htmlAttributes: new { @class = "control-label text-muted" })
@Html.DropDownListFor(x => x.StoryType, new SelectList(Model.StoryTypes, "Id", "Name"), new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.StoryType, "", new { @class = "text-danger" })
</div>
<div class="col-6">
@Html.Label("Target Age Range", htmlAttributes: new { @class = "control-label text-muted" })
@Html.DropDownListFor(x => x.StoryAgeRange, new SelectList(Model.StoryAgeRanges, "Id", "Name"), new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.StoryAgeRange, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-row mt-5">
<div class="col-12">
@Html.Label("Write below", htmlAttributes: new { @class = "control-label text-muted" })
@Html.TextAreaFor(model => model.Content, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Content, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-row mt-3">
<div class="btn-group">
@Html.ActionLink("Cancel", "Index", "Story", null, new { @class = "btn btn-red" })
<input type="button" id="editStoryBtn" value="Edit story" class="btn btn-blue" />
<a class="btn btn-link ml-auto" href="@Url.Action("PublishingRules", "Legal")">Publishing Guidelines</a>
</div>
</div>
}
Here is the javascript for the ajax call (and the function I use to get the filename):
//get the filename of the uploaded file
var fullPath = document.getElementById('file').value;
if (fullPath) {
var startIndex = (fullPath.indexOf('\\') >= 0 ? fullPath.lastIndexOf('\\') : fullPath.lastIndexOf('/'));
var filename = fullPath.substring(startIndex);
if (filename.indexOf('\\') === 0 || filename.indexOf('/') === 0) {
filename = filename.substring(1);
}
}
//create the new story object
var editedStory = {
__RequestVerificationToken: token,
StoryId: storyId,
CoverArt: filename,
Title: $("input[name='Title']").val(),
Content: CKEDITOR.instances.Content.getData(),
Genre: $('#Genre').val(),
Visibility: $('#Visibility').val(),
StoryType: $('#StoryType').val(),
StoryAgeRange: $('#StoryAgeRange').val(),
WordCount: finalWordCount
}
//post the edited story to the controller
$.ajax({
url: "/story/edit/" + storyId,
type: 'POST',
data: editedStory,
success: function (data) {
window.location.href = data.Url;
},
error: function (error) {
console.log("error is " + error);
}
});
And here is the controller.
//POST story/edit/5
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Edit (EditStoryViewModel viewModel, HttpPostedFileBase file)
{
//confirm model state is valid
if (ModelState.IsValid)
{
//find the correct story by the id and include the reference tables
var StoryToEdit = dbContext.Stories.SingleOrDefault(x => x.Id == viewModel.StoryId);
//verify file upload
if (file != null)
{
StoryToEdit.CoverArt = StoryToEdit.Id + Path.GetExtension(file.FileName);
file.SaveAs(Server.MapPath("/Imgs/") + StoryToEdit.CoverArt);
}
StoryToEdit.Title = viewModel.Title;
StoryToEdit.Content = viewModel.Content;
StoryToEdit.GenreId = viewModel.Genre;
StoryToEdit.StoryAgeRangeId = viewModel.StoryAgeRange;
StoryToEdit.StoryTypeId = viewModel.StoryType;
StoryToEdit.VisibilityId = viewModel.Visibility;
StoryToEdit.WordCount = viewModel.WordCount;
StoryToEdit.UpdatedAt = DateTime.Now;
dbContext.Entry(StoryToEdit).State = EntityState.Modified;
dbContext.SaveChanges();
var redirectUrl = new UrlHelper(Request.RequestContext).Action("Details", "Story", new { id = StoryToEdit.Id });
return Json(new { Url = redirectUrl });
}
else
{
return View(viewModel);
}
}
What's going wrong because the file is never being saved (everything else works).