Usman Khan
Usman Khan

Reputation: 143

HttpPostedFileBase Null in controller - Want to save file path to database

I want to save the file path to my database reports table. I have a column of type: string FilePath.

The end goal is that I want to be able to download the file from a report details view. Obviously the report download link would be different depending on the report ID.

Currently it doesn't seem that the controller is receiving anything as before I had Object reference not set to an instance of an object exception. I then added file != null in my if statement so I don't get the error anymore. However clearly the underlying issue is still present. Here is my controller save action:

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "AdminManager")]
public ActionResult Save(Report report, HttpPostedFileBase file)
{
    if (!ModelState.IsValid)
    {
        var viewModel = new ReportFormViewModel
        {
            Report = report,
            Members = _context.Members.ToList(),
            Subjects = _context.Subjects.ToList()
        };
        return View("ReportForm", viewModel);
    }

    if (file != null && file.ContentLength > 0)
    {
        string filePath = Path.Combine(
            Server.MapPath("~/App_Data/Uploads"),
            Path.GetFileName(file.FileName));
        file.SaveAs(filePath);
    }

    if (report.Id == 0)
        _context.Reports.Add(report);

    else
    {
        var reportInDb = _context.Reports.Single(e => e.Id == report.Id);

        reportInDb.Name = report.Name;
        reportInDb.MemberId = report.MemberId;
        reportInDb.SubjectId = report.SubjectId;
        reportInDb.Date = report.Date;
        reportInDb.FilePath = report.FilePath;
    }
    _context.SaveChanges();
    return RedirectToAction("Index", "Report");
}

Here is my form view:

<h2>@Model.Title</h2>

@using (Html.BeginForm("Save", "Report", new {enctype = "multipart/form-data"}))
{
<div class="form-group">
    @Html.LabelFor(r => r.Report.Name)
    @Html.TextBoxFor(r => r.Report.Name, new { @class = "form-control" })
    @Html.ValidationMessageFor(r => r.Report.Name)
</div>
<div class="form-group">
    @Html.LabelFor(r => r.Report.Date) e.g. 01 Jan 2000
    @Html.TextBoxFor(r => r.Report.Date, "{0: d MMM yyyy}", new { @class = "form-control" })
    @Html.ValidationMessageFor(r => r.Report.Date)
</div>

<div class="form-group">
    @Html.LabelFor(m => m.Report.MemberId)
    @Html.DropDownListFor(m => m.Report.MemberId, new SelectList(Model.Members, "Id", "Name"), "Select Author", new { @class = "form-control" })
    @Html.ValidationMessageFor(m => m.Report.MemberId)
</div>

<div class="form-group">
    @Html.LabelFor(m => m.Report.SubjectId)
    @Html.DropDownListFor(m => m.Report.SubjectId, new SelectList(Model.Subjects, "Id", "Name"), "Select Subject", new { @class = "form-control" })
    @Html.ValidationMessageFor(m => m.Report.SubjectId)
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Report.FilePath)
    <input type="file" name="file" id="file"/>
</div>

@Html.HiddenFor((m => m.Report.Id))
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-primary">Save</button>

}

Current code doesn't seem to send file data to action.

Upvotes: 2

Views: 768

Answers (1)

Racil Hilan
Racil Hilan

Reputation: 25361

It is recommended to add the file to your model:

public class Report {
    [Required]
    [Display(Name = "Report File")]
    public HttpPostedFileBase ReportFile { get; set; }
    //... The other fields
}

Usually I would append ViewModel, so ReportViewModel instead of Report. This makes it easier to distinguish between view models and business/data models.

And in your Razor:

<div class="form-group">
    @Html.LabelFor(m => m.Report.ReportFile)
    @Html.TextBoxFor(m => m.ReportFile, new { type = "file" })
    <!--You can also use <input type="file" name="ReportFile" id="ReportFile"/>-->
</div>

Note that the name that you use in the LabelFor must match the ID of the control. In your code FilePath and file didn't match.

And finally in the controller:

public ActionResult Save(Report report)
{
    //...some code
    string filePath = Path.Combine(Server.MapPath("~/App_Data/Uploads"),
                                   Path.GetFileName(report.ReportFile.FileName));
    report.ReportFile.SaveAs(filePath);
    //...other code
}

I wouldn't use the name of the uploaded file. Instead, I would give it a name according to my project's naming convention. I often use the ID as the name, perhaps with some prefix. Example:

var fileName = "F" + report.Id + ".jpg"; //You can get the extension from the uploaded file
string filePath = Path.Combine(Server.MapPath("~/App_Data/Uploads"), fileName);

Obviously, when you're inserting a new object, you won't have an ID until you insert it into the database, so the code to save the physical file must be placed after the code to insert it into the database. If you follow this logic, you don't need to save the path in the database, because the path can be always calculated from the ID. So you save a column in the database, gain performance in your code as you don't need to handle another string column, and you have a clear and simply file naming convention that is safe without user input risk.

Another way I follow, especially when the type of the file may vary (i.e. you can upload files with different extensions), is using a GUID for the file name. In this case, the file name must be saved in the database, but the GUID can be generated before inserting the object into the database. Example:

string ext = report.ReportFile.FileName.Substring(
             report.ReportFile.FileName.LastIndexOf('.')).ToLower();
var fileName = Guid.NewGuid().ToString() + ext;
string filePath = Path.Combine(Server.MapPath("~/App_Data/Uploads"), fileName);

Upvotes: 1

Related Questions