A modular profile photo upload using ASP.NET MVC

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 Content
  • PhotoUploader 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: