Search code examples
asp.net-mvcentity-frameworkviewcontroller

ASP.NET MVC : view and controller problem when letting the view upload an image


Solved by using a view model that has HttpPostedFileBase property bound to the view, so that the form only requires a single post method to upload the image and then add the entity to the database.

Here is the form tag:

@using (Html.BeginForm("Create", "Lessons", FormMethod.Post, new {enctype = "multipart/form-data" }))

and the view control bound to the view model property (happens by default if the names match, in my case the property is HttpPostedFileBase file:

<input type="file" name="file" />

This is far easier than the path I was on below, trying to have two different post actions for a single view (which should be possible from everything I read, but I think my problem was trying to have a form within a form):

My controller for Lesson has two post methods that can be called:

The first is to create a new Lesson:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Create(Lesson lesson)//, WebImage Photo)
    {
        if (ModelState.IsValid && Request.Form["Create"]!=null) 
            //Request.Form check because the image file function is calling back to this handler
            //Once that is sorted out the check can be removed and simply use ModelState.IsValid
        {
            db.Lessons.Add(lesson);
            await db.SaveChangesAsync();
            return RedirectToAction("Index");
        }
        ViewBag.DivID = new SelectList(db.Divisions, "ID", "DivName", lesson.DivID);
        ViewBag.ProjectID = new SelectList(db.Projectinformations, "ID", "Code", lesson.ProjectID);
        ViewBag.PeopleID = new SelectList(db.Peoples, "ID", "Firstname", lesson.PeopleID);
        return View(lesson);
    }

On the page this is the form (shortened) with the submit button that calls the controller.Create() action:

@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
 //. . . for each Lesson property there is a form group with @Html Label
 //@Html.EditorFor, etc. and then at the bottom is this:

        <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Create" name="Create" class="btn btn-default" />
        </div>
    </div>

The image path field is part of the main form, and I left the generated code for it's text box etc. This is the part of the form where I'm trying to insert an image upload that calls a different controller action:

    <div class="form-group">
        <div class="col-md-10">
            <form action="UploadImage" name="ImageFileForm" id="imageForm" method="post" enctype="multipart/form-data">
                <input type="file" name="file" />
                <input type="submit" value="OK" name="OK" />
            </form>
        </div>
        @Html.LabelFor(model => model.Image, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Image, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Image, "", new { @class = "text-danger" })
        </div>
    </div>

This is the contoller action for UploadImage

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult UploadImage(HttpPostedFileBase file)
    {
        string imagePath ="";
        if (file != null)
        {
            var newFileName = Path.GetFileName(file.FileName);
            imagePath = @"LessonsLearnedImages\" + newFileName;
            file.SaveAs(@"~\" + imagePath);
        }
        ViewBag.DivID = new SelectList(db.Divisions, "ID", "DivName", lesson.DivID);
        ViewBag.ProjectID = new SelectList(db.Projectinformations, "ID", "Code", lesson.ProjectID);
        ViewBag.MOAPeopleID = new SelectList(db.MOApeoples, "ID", "Firstname", lesson.MOAPeopleID);
        ViewBag.Image = imagePath;
        return View();
    }

When I press the ImageUpload button however, the controller Create (form default) function gets called. I can potentially use the Create action in the controller to do everything with a Request.Form check, but this has failed for me when adding a HttpPostedFileBase parameter to it.


Solution

  • With image upload there are two things that you need to save. The first part is saving the actual image, eg. saving onto a folder on your computer. The second part is saving info about the image, eg. saving its full path in a database. Doing both parts are essential so that you can retrieve the image later.

    One way to capture images (or uploaded files in general) is by using HttpPostedFileBase as the type of parameter:

    [HttpPost]
    public async Task<ActionResult> Upload(HttpPostedFileBase myFile)
    {
        // myFile contains several properties which you need to be able to save the file
    }
    

    The myFile parameter has several properties that you need for saving. Examples are the path from which you are uploading the image and the actual image content which you can extract as a byte[].

    The corresponding form on the web page, trimmed down to the essential bits, would be:

    <form action="Upload" method="post" enctype="multipart/form-data">
        <input type="file" name="myFile" />
        <input type="submit" value="Click me to upload" />
    </form>
    

    You need to make sure the following line up so that the correct controller action gets called:

    • The method of the form should match the verb of the controller method (post in the form corresponding to HttpPost in the controller)
    • The action of the form should match the name of the controller method (Upload in this case)
    • The name of the input should match the name of the controller method's parameter (myFile in this case)

    You also need to make sure that the enctype is multipart/form-data in order for the myFile parameter to actually have a value. If not, you may execute the correct controller method, but the parameter may be null.

    After all of that, you should have a proper myFile parameter whose properties you can use to save. For example, after saving the image, you can store its full path in an ImagePath column in your Lessons table.

    As you mentioned, the image (and anything else for that matter) gets lost once the controller method finishes executing. But that's okay, because we already have the image and image path saved in persistent storage. To retrieve the image at a later time, you can just look at the associated lesson. For example, assuming you have a Model object that corresponds to a lesson that you got from the database, displaying the image could look like this:

    <img src="@Model.ImagePath" />
    

    EDIT

    It seems that you want to save multiple fields on the create page in one go, with the image file only being one of the fields. In that case, only a single method is needed on the controller and only a single form element is needed on the page.

    To capture the file, add the HttpPostedFileBase as a property in your lesson view model, for example:

    public class Lesson
    {
        public HttpPostedFileBase File { get; set; }
    }