ASP.NET MVC with Scaffolding and Razor template engine are great to get your LOB web application up and running shortly. But every once in a while, you find you need to do some extra work to add some features. Having a photo upload template was one of them.
My goals
- To be able to edit user profile (name, email, etc.) as well as photo
- To be able to view the uploaded photo before submit (similar to profile image upload of all social media apps)
- To be built in compliance with ASP.NET MVC (i.e. no server-side controls)
- To avoid writing extra code unless needed (no need for overkill)
Step 1:
You need in your form to have an
<input type="file" />
but this will not attach the file to your form data until you also set both
<form method="POST" enctype="multipart/form-data">
By now you can check your HTTP message (using Fiddler or any browser add-on for network monitoring) to find out that the POST message containing your form data and the attached image. This works similarly like a email message with an attachment.
Keep those two pieces of info in mind because we are using them later on.
Step 2:
You have to decide how you will save the image physically, either as an array of bytes in DB or as a string URL to where the image will be saved on the server File System. I have chosen the second option. So I will build the model accordingly with a string property to represent Photo URL.
public string Photo { get; set; }
P.S: Decorating the above property with any DataType
attribute like [DataType(DataType.Upload)]
or [DataType(DataType.ImageUrl)]
did not change how the image is displayed or edited and you have to specifically create your corresponding templates (Correct me if I am wrong).
Step 3:
Now I use scaffolding to build controller and view. All is ready except that Photo is rendered as a text field, while I need a file input field to be able to get the image and send it to the Controller. Realizing that I cannot build my View based on Model alone, I had to change my View to represent a ViewModel, which will be used as a:
- DTO between the Controller and the View
- A closer representation of how I want to show my Model on the View
All what I needed was to copy all Model properties in ViewModel, add one extra property of type HttpPostedFileBase
which is the class used to map file attachments in multipart/form-data,
(assuming you already changed the enctype
), or you can simply use
@using (Html.BeginForm("Edit", "Profile", FormMethod.Post, htmlAttributes: new { @enctype = "multipart/form-data" }))
Step 4
The HttpPostedFileBase
property is fine for reading form posted data. It will hold the attached image, and in your Controller [HttpPost]
Action you can process the image, save it to desk, get its URL and assign to the string property of the Model and/or ViewModel.
But to do so, you need to add the file input manually to view like any other HTML tag with input name = property name, and you cannot use the helper method @Html.EditorFor
as there is no standard Editor Template for HttpPostedFileBase
, and you will have to create one.
Step 5
The above approach is fine, but I believe it has many flaws:
- The Controller is doing much more than its role when it gets into the way of storing an image and getting back their URL to assign to the Model
- The idea that both image URL and image content are two separate properties in the ViewModel requires to be revisited as they are often used together
- In order to build an advanced Editor Template for
HttpPostedFileBase
, you need also to pass the URL of the previously saved image on server
Here I introduced two new classes for the below purposes:
PhotoUpload
holds both properties; URL and ContentPhotoUploader
is responsible for storing the Content and returning back the URL- I can easily create an Editor Template for
PhotoUpload
, displaying the URL as image and the file input - They are general purpose classes, independent of any Model or Controller
The complete code for PhotoUpload
class is below, while I will cover PhotoUploader
later:
public class PhotoUpload { public string Url { get; set; } public HttpPostedFileBase Content { get; set; } }
Step 6
Now we can change the ViewModel to be identical to the Model, except for the Photo property to be of type PhotoUpload
public PhotoUpload Photo { get; set; }
The Controller will be modified as well to copy code from the Model.Photo to ViewModel.Photo in GET and back again in POST, but in case of POST, the image should be saved first. You can call the uploading code and storing code directly from the Controller, but I preferred to separate persistence layer from the Controller, and that is what PhotoUploader
is for. Controller just assigns the VirtualPath
to store the photo and the Mapper
to map that virtual path, which will be the Controller.Server object.
The code of PhotoUploader
class is below
public class PhotoUploader { private string[] validTypes = new[] { "image/gif", "image/jpeg", "image/pjpeg", "image/png" }; public string[] ValidTypes { get { return validTypes; } set { validTypes = value; } } public string VirtualPath { get; set; } public HttpServerUtilityBase Mapper { get; set; } public string Upload(PhotoUpload photo) { if (photo.Content == null || photo.Content.ContentLength == 0) { return null; } if (!ValidTypes.Contains(photo.Content.ContentType)) { throw new InvalidDataException("Please upload only an image of type GIF, JPG or PNG."); } else { var fileName = Path.GetFileName(photo.Content.FileName) ?? photo.Content.ContentType.Replace('/', '.'); var physicalPath = Path.Combine(Mapper.MapPath(VirtualPath)); if (!Directory.Exists(physicalPath)) { var directorySecurity = new DirectorySecurity(); directorySecurity.AddAccessRule(new FileSystemAccessRule(@"everyone", FileSystemRights.Read, AccessControlType.Allow)); directorySecurity.AddAccessRule(new FileSystemAccessRule(@"everyone", FileSystemRights.FullControl, AccessControlType.Allow)); Directory.CreateDirectory(physicalPath, directorySecurity); } photo.Content.SaveAs(physicalPath + fileName); photo.Url = Path.Combine(VirtualPath, fileName); return photo.Url; } } }
Step 7
Now to the fun part; displaying the PhotoUpload
property in CSHTML.
I have built a design template with the same class name PhotoUpload.cshtml
under /Views/Shared/EditorTemplates/
which will be used by convention to display the property. This was easy thanks to Jasny Bootstrap extension, which also inspired me to come with the class design above. The code is below:
@model EMR.Models.PhotoUpload <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jasny-bootstrap/3.1.3/css/jasny-bootstrap.min.css"> <div class="fileinput fileinput-exists" data-provides="fileinput"> <div class="fileinput-new thumbnail" data-trigger="fileinput" style="width: 150px; height: 200px;"> </div> <div class="fileinput-preview fileinput-exists thumbnail" data-trigger="fileinput" style="max-width: 150px;"> <img src="@Model.Url" alt="..."> </div> <div> <span class="btn btn-default btn-file"> <span class="fileinput-new">Select image</span> <span class="fileinput-exists">Change</span> @Html.HiddenFor(model => model.Url) <input type="file" name="@Html.NameFor(model => model.Content)"> </span> <a href="#" class="btn btn-default fileinput-exists" data-dismiss="fileinput">Remove</a> </div> </div> <script src="//cdnjs.cloudflare.com/ajax/libs/jasny-bootstrap/3.1.3/js/jasny-bootstrap.min.js"></script>
You can see how it looks here Jasny Bootstrap – File Input
This is by far, the best and simplest implementation of photo upload I could see after reading many articles and stackoverflow questions.
Below are some references that helped me as well: