Monday, January 7, 2013
0 comments

An Example ASP.NET MVC Web Project to Upload and Download Files

5:06 PM

Introduction

This article introduces an example ASP.NET MVC web project to upload and download files.

Background

This is a short article to introduce an example ASP.NET MVC web project to upload and download files. If you need to know how ASP.NET MVC works, you can refer to one of my earlier articles "A Simple Tutorial on Developing ASP.Net Applications in MVC Pattern". This project is developed in Visual Studio 2008.

The Project in the Solution Explorer

The important files in this example project are shown in the solution explorer:
SolutionExplorer.jpg We will first take a look at the application configuration information added to the "Web.config" file in the root folder. After that, we will look at a small utility class, and we will then look into the models, the views, and the controllers of this ASP.NET MVC project.

The Web Application's Configuration

This web project has two "web.config" files. The one in the "Views" folder is MVC specific. It is used to block direct access to the "aspx" views implemented in the "Views" folder. The application's configuration information should be written in the "Web.config" file in the root folder. The configuration information added to the application is the following:
<appSettings>
    <add key="ApplicationName"
	value="An ASP.NET File Upload Example Project in MVC Pattern"/>
    <add key="Author" value="Song Li"/>
    <add key="DevelopmentTime" value="5/4/2010"/>
    <add key="DeploymentVirtualDirectory" value=""/>
</appSettings>
In the configuration information, the "DeploymentVirtualDirectory" needs to be set as an empty string at development time. But the correct virtual directory name will needed at the deployment time. The application will use the configuration information through one of the model classes "ApplicationSettings" implemented in the "ApplicationSettings.cs" file in the "Models" folder.
using System;
using System.Configuration;

namespace ASPNetMVCDemo.Models
{
    public class ApplicationSettings
    {
        private static object _threadLock = new Object();
        private static ApplicationSettings _Instance = null;

        private string _ApplicationName;
        private string _Author;
        private string _DevelopmentTime;
        private string _DeploymentVirtualDirectory;

        public string ApplicationName { get { return _ApplicationName; } }
        public string Author { get { return _Author; } }
        public string DevelopmentTime { get { return _DevelopmentTime; } }
        public string DeploymentVirtualDirectory {
            get { return _DeploymentVirtualDirectory; } }

        private ApplicationSettings()
        {
            _ApplicationName = ConfigurationManager.AppSettings["ApplicationName"];
            _Author = ConfigurationManager.AppSettings["Author"];
            _DevelopmentTime = ConfigurationManager.AppSettings["DevelopmentTime"];
            _DeploymentVirtualDirectory
                = ConfigurationManager.AppSettings["DeploymentVirtualDirectory"];
        }

        public static ApplicationSettings GetInstance()
        {
            lock (_threadLock)
                if (_Instance == null)
                    _Instance = new ApplicationSettings();

            return _Instance;
        }
    }
}
This is a thread safe singleton class to read configuration information from the "Web.config" file. The application can access the application configuration information by the public properties of this class.

A Utility Class

In order to make further development easier, a static utility class "ApplicationUtility" is implemented in the "ApplicationUtility.cs" file in the "Utilities" folder.
using System;
using System.Text;
using ASPNetMVCDemo.Models;

namespace ASPNetMVCDemo.Utilities
{
    public static class ApplicationUtility
    {
        public static string FormatURL(string PathWithoutVirtualDirectoryName)
        {
            ApplicationSettings appInfomation
                = ApplicationSettings.GetInstance();
            string DeploymentVirtualDirectory
                = appInfomation.DeploymentVirtualDirectory;

            if (DeploymentVirtualDirectory == "")
            {
                return PathWithoutVirtualDirectoryName;
            }

            StringBuilder SB = new StringBuilder();
            SB.Append("/");
            SB.Append(appInfomation.DeploymentVirtualDirectory);
            SB.Append("/");
            SB.Append(PathWithoutVirtualDirectoryName);

            return SB.ToString();
        }

        public static string jQueryLink()
        {
            StringBuilder SB = new StringBuilder();
            SB.Append("<script src=\"");
            SB.Append(ApplicationUtility.FormatURL("/Scripts/jquery-1.4.1.min.js"));
            SB.Append("\" type=\"text/javascript\"></script>");

            return SB.ToString();
        }

        public static string AppStylelink()
        {
           StringBuilder SB = new StringBuilder();
            SB.Append("<link href=\"");
            SB.Append(ApplicationUtility.FormatURL("/Styles/AppStyles.css"));
            SB.Append("\" rel=\"stylesheet\" type=\"text/css\" />");

            return SB.ToString();
        }
    }
}
The class exposes three public methods to format the URLs. Two of them are used to create the link to the CSS file and the link to the jQuery script library. To format the URLs, the correct virtual directory name needs to be configured in the "Web.config" file in the root directory. At development time, this value need to be an empty string.

The Model

The class "ExistingFilesModel" implemented in the "ExistingFiles.cs" file in the "Models" folder represents the MVC application's model.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Web;

namespace ASPNetMVCDemo.Models
{
    public class ExistingFilesModel
    {
        private DataTable UploadedFiles;

        public ExistingFilesModel()
        {
            UploadedFiles = new DataTable();
            DataColumn IDColumn = UploadedFiles.Columns.Add
				("ID", Type.GetType("System.Int32"));
            IDColumn.AutoIncrement = true;
            IDColumn.AutoIncrementSeed = 1;
            IDColumn.AutoIncrementStep = 1;
            DataColumn[] keys = new DataColumn[1];
            keys[0] = IDColumn;
            UploadedFiles.PrimaryKey = keys;

            UploadedFiles.Columns.Add("File Name", Type.GetType("System.String"));
            UploadedFiles.Columns.Add("File Size", Type.GetType("System.Int32"));
            UploadedFiles.Columns.Add("Context Type", Type.GetType("System.String"));
            UploadedFiles.Columns.Add("Time Uploadeded", Type.GetType("System.DateTime"));
            UploadedFiles.Columns.Add("File Data", Type.GetType("System.Byte[]"));
        }

        public DataTable GetUploadedFiles() { return UploadedFiles; }

        public void AddAFile(string FileName,
		int Size, string ContentType, Byte[] FileData)
        {
            DataRow row = UploadedFiles.NewRow();
            UploadedFiles.Rows.Add(row);

            row["File Name"] = FileName;
            row["File Size"] = Size;
            row["Context Type"] = ContentType;
            row["Time Uploadeded"] = System.DateTime.Now;
            row["File Data"] = FileData;
        }

        public void DeleteAFile(int ID)
        {
            DataRow RowToDelete = UploadedFiles.Rows.Find(ID);
            if (RowToDelete != null)
            {
                UploadedFiles.Rows.Remove(RowToDelete);
            }
        }
    }
}
This class stores the application's data, which is a list of uploaded files in a "DataTable". It also exposes public methods to add a file, delete a file, and retrieve the list of the files. An instance of this class will be saved in the application's web session.

The View

An ASP.NET MVC application will use a master page by default. For simplicity, this application chooses not to use one. The "FileUpload.aspx" view implemented in the "Views\FileUploadExample" folder is the major functional view for this application.
<%@ Page Language="C#"
    CodeBehind="~/Views/FileUploadExample/FileUpload.aspx.cs"
    AutoEventWireup="true" EnableViewState="false"
    Inherits="ASPNetMVCDemo.Views.FileUploadExample.FileUpload" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>File Upload Example by ASP.NET in MVC Pattern</title>
    <asp:Literal ID="litLinkTojQuery" runat="server" />
    <asp:Literal ID="litAppStyle" runat="server" />
    <script language="javascript" type="text/javascript">
        $(document).ready(function() {
            $("a").hover(function() {
                this.style.color = "red";
            }, function() {
                this.style.color = "";
            });

            $.mouseX = function(e) {
                if (e.pageX) return e.pageX;
                else if (e.clientX)
                    return e.clientX + (document.documentElement.scrollLeft ?
                    document.documentElement.scrollLeft :
                    document.body.scrollLeft);
                else return null;
            };

            $.mouseY = function(e) {
                if (e.pageY) return e.pageY;
                else if (e.clientY)
                    return e.clientY + (document.documentElement.scrollTop ?
                    document.documentElement.scrollTop :
                    document.body.scrollTop);
                else return null;
            };

            $("#spanStartUpload").hover(function() {
                this.style.color = "red";
            }, function() {
                this.style.color = "blue";
            });

            $("#spanStartUpload").click(function(e) {
                e.preventDefault();
                $("#divFileUpload").slideToggle("fast");
                $file = $("#spanFileUpload");
                $file.html($file.html());
            });

            $("#btnCancel").click(function(e) {
                $file = $("#spanFileUpload");
                $file.html($file.html());
                $("#divFileUpload").css("display", "none");
            });

            $("#btnUpload").click(function(e) {
                $file = $("#FileToLoad");
                var $filePath = $.trim($file.val());
                if ($filePath == "") {
                    alert("Please browse a file to upload");
                    return;
                }

                var $ext = $filePath.split(".").pop().toLowerCase();
                var $allow = new Array("gif", "png", "jpg", "jpeg");
                if ($.inArray($ext, $allow) == -1) {
                    alert("Only image files are accepted, please browse a image file");
                    return;
                }

                var $uploadaction = $("#hidFileUploadAction").val();
                $("#MainForm").attr("action", $uploadaction);

                try {
                    $("#MainForm").submit();
                } catch (err) {
                    alert("Upload file failed, make sure to browse a valid file path,
			and make sure the file is not openned by any programs.");
                }

            });

            $(".ImagePopLink").click(function(e) {
                e.preventDefault();
                var $ID = $(this).html();
                var $imagebase = $("#hidImageBase").val();
                var $imagesource = $imagebase + "?ID=" + $ID;
                var $imagelink = "<img style=\"width: 400px\" src=\""
				+ $imagesource + "\" />";
                $("#divImg").html($imagelink);
                $("#divImg").load();
                $("#PopWindow").css({ "left": $.mouseX(e), "top": $.mouseY(e) + 5 });
                $("#PopWindow").show();
            });

            $("#Close").click(function(e) {
                e.preventDefault();
                $("#PopWindow").css({ "left": "0px", "top": "0px" });
                $("#PopWindow").hide();
            });

        });
    </script>
</head>

<body class="DocumentDefault">
<input type="hidden" id="hidFileUploadAction" runat="server" />
<input type="hidden" id="hidImageBase" runat="server" />
<form name="MainForm" method="post" action="" id="MainForm"
    enctype="multipart/form-data" runat="server">

    <div class="DocumentTitle">
        <asp:Literal id="litApplicationName" runat="server" />
    </div>

    <div class="DocumentAuthor">
        <asp:Literal id="litAuthorInformation" runat="server" />
    </div>

    <div style="text-align: left">
        <span id="spanStartUpload" class="BoldLink">
            Click to upload a new file</span>
    </div>

    <div id="divFileUpload" style="display: none; text-align: left">
        <span style="font-weight: bold">Please browse a file to upload&nbsp;</span>
        <span id="spanFileUpload">
            <input type="file" name="FileToLoad" id="FileToLoad" style="width:350px;" />
        </span>
        <span>
            <input type="button" id="btnUpload" value="Submit"
			style="width: 75px; height: 21px" />
        </span>
        <span>
            <input type="button" id="btnCancel" value="Cancel"
			style="width: 75px; height: 21px" />
        </span>
    </div>

<div id="MainContent" class="MainContent">
    <asp:Literal ID="litExistingFiles" runat="server" />
</div>
<div class="Copyright">Copy right: The Code Project Open License (CPOL)</div>

<div id="PopWindow" class="HiddenPopup">
<div>
    <div style="background-color:
		Transparent; position:absolute; top: 1px; left: 380px">
        <a id="Close" style="font-weight: bold" href="#">X</a></div>
    <div id="divImg"></div>
</div>
</div>

</div>
</form>
</body>
</html>
The code behind file for this view is implemented as the following:
using System;
using System.Text;
using System.Data;
using System.Web;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using ASPNetMVCDemo.Utilities;

namespace ASPNetMVCDemo.Views.FileUploadExample
{
    public class FileUpload : System.Web.Mvc.ViewPage
    {
        protected Literal litApplicationName;
        protected Literal litAuthorInformation;
        protected Literal litExistingFiles;
        protected Literal litLinkTojQuery;
        protected Literal litAppStyle;
        protected HtmlInputHidden hidFileUploadAction;
        protected HtmlInputHidden hidImageBase;

        protected void Page_Load(object sender, EventArgs e)
        {
            Response.Cache.SetCacheability(HttpCacheability.NoCache);

            Models.ApplicationSettings appsettings =
			Models.ApplicationSettings.GetInstance();

            litApplicationName.Text = appsettings.ApplicationName;
            hidFileUploadAction.Value
                = ApplicationUtility.FormatURL("/FileUploadExample/SaveFile");
            hidImageBase.Value = ApplicationUtility.FormatURL
				("/FileUploadExample/GetAFile");

            litLinkTojQuery.Text = ApplicationUtility.jQueryLink();
            litAppStyle.Text = ApplicationUtility.AppStylelink();

            StringBuilder SB = new StringBuilder();
            SB.Append("Developed by ");
            SB.Append(appsettings.Author);
            SB.Append(" on ");
            SB.Append(appsettings.DevelopmentTime);
            litAuthorInformation.Text = SB.ToString();

            SB.Remove(0, SB.Length);
            DataView FileView = (DataView)ViewData["ExistingFileList"];
            SB.Append("<table style=\"width: 99%;\" ");
            SB.Append("rules=\"all\" border=\"1px\" ");
            SB.Append("cellspacing=\"0px\" cellpadding=\"4px\">");

            SB.Append("<tr style=\"background-color: Silver; color: white; ");
            SB.Append("font-weight: bold\">");
            foreach (DataColumn aColumn in FileView.Table.Columns)
            {
                if (aColumn.ColumnMapping == MappingType.Hidden)
                {
                    continue;
                }

                SB.Append("<td>");
                SB.Append(aColumn.ColumnName);
                SB.Append("</td>");
            }
            SB.Append("<td>&nbsp;</td>");
            SB.Append("<td>&nbsp;</td>");
            SB.Append("</tr>");

            foreach (DataRowView aRowView in FileView)
            {
                SB.Append("<tr>");
                foreach (DataColumn aColumn in FileView.Table.Columns)
                {
                    if (aColumn.ColumnMapping == MappingType.Hidden)
                    {
                        continue;
                    }

                    SB.Append("<td>");
                    if (aColumn.ColumnName == "ID")
                    {
                        SB.Append("<span class=\"ImagePopLink\">");
                        SB.Append(aRowView[aColumn.ColumnName].ToString());
                        SB.Append("</span>");
                    }
                    else
                    {
                        SB.Append(aRowView[aColumn.ColumnName].ToString());
                    }

                    SB.Append("</td>");
                }

                string ID = aRowView["ID"].ToString();
                SB.Append("<td>");
                SB.Append("<a href=\"");
                SB.Append(ApplicationUtility.FormatURL("/FileUploadExample/GetAFile"));
                SB.Append("?ATTACH=YES&ID=");
                SB.Append(ID);
                SB.Append("\">Download this file</a>");
                SB.Append("</td>");

                SB.Append("<td>");
                SB.Append("<a href=\"");
                SB.Append(ApplicationUtility.FormatURL("/FileUploadExample/DeleteFile"));
                SB.Append("?ID=");
                SB.Append(ID);
                SB.Append("\">Delete this file</a>");
                SB.Append("</td>");

                SB.Append("</tr>");
            }

            SB.Append("</table>");

            litExistingFiles.Text = SB.ToString();
        }
    }
}
There is a hot discussion on whether we should use code-behind files for the views. If we do not use the code-behind files, we will need to use in-line server scripts in the views. Regardless of whether the code-behind files are used, application data in the model classes should never be modified in the views to better conform to the MVC methodology.

The Controller

The controller for this simple application is implemented in the "FileUploadExampleController" class in the ""FileUploadExampleController.cs" file in the "Controllers" folder.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Web;
using System.IO;
using System.Web.Mvc;
using ASPNetMVCDemo.Models;

namespace ASPNetMVCDemo.Controllers
{
    public class FileUploadExampleController : Controller
    {
        private ExistingFilesModel GetModelFromSession()
        {
            ExistingFilesModel theModel =
		(ExistingFilesModel)Session["ExistingFilesModel"];
            if (theModel == null)
            {
                theModel = new ExistingFilesModel();
                DataTable aTable = theModel.GetUploadedFiles();
                aTable.Columns["File Data"].ColumnMapping = MappingType.Hidden;

                Session["ExistingFilesModel"] = theModel;
            }

            return theModel;
        }

        public ActionResult FileUpload()
        {
            ExistingFilesModel theModel = GetModelFromSession();
            DataTable aTable = theModel.GetUploadedFiles();

            ViewData["ExistingFileList"] = aTable.DefaultView;
            return View();
        }

        public ActionResult DeleteFile()
        {
            string ID = Request.QueryString["ID"];
            int intID;

            if (!Int32.TryParse(ID, out intID))
            {
                ViewData["ERROR"] = "Please provide a valid student ID";
                return View("../Shared/Error");
            }

            ExistingFilesModel theModel = GetModelFromSession();
            theModel.DeleteAFile(intID);

            return RedirectToAction("FileUpload");
        }

        public ActionResult SaveFile()
        {
            ExistingFilesModel theModel = GetModelFromSession();
            HttpPostedFileBase File = Request.Files["FileToLoad"];

            if (File != null)
            {
                int Size = File.ContentLength;

                if (Size <= 0)
                {
                    ViewData["ERROR"] = "You uploaded an empty file,
			please browse a valid file to upload";
                    return View("../Shared/Error");
                }

                string FileName = File.FileName;
                int Position = FileName.LastIndexOf("\\");
                FileName = FileName.Substring(Position + 1);
                string ContentType = File.ContentType;
                byte[] FileData = new byte[Size];
                File.InputStream.Read(FileData, 0, Size);

                theModel.AddAFile(FileName, Size, ContentType, FileData);
            }

            return RedirectToAction("FileUpload");
        }

        public ActionResult GetAFile()
        {
            string Attachment = Request.QueryString["ATTACH"];
            string ID = Request.QueryString["ID"];
            int intID;

            if (!Int32.TryParse(ID, out intID))
            {
                ViewData["ERROR"] = "Please provide a valid student ID";
                return View("../Shared/Error");
            }

            ExistingFilesModel theModel = GetModelFromSession();
            DataTable aTable = theModel.GetUploadedFiles();
            DataRow FileRow = aTable.Rows.Find(intID);
            if (FileRow == null)
            {
                ViewData["ERROR"] = "Please provide a valid student ID";
                return View("../Shared/Error");
            }

            string FileName = (string) FileRow["File Name"];
            int Size = (int) FileRow["File Size"];
            string ContentType = (string) FileRow["Context Type"];
            Byte[] Data = (Byte[]) FileRow["File Data"];


            Response.ContentType = ContentType;
            StringBuilder SB = new StringBuilder();
            if (Attachment == "YES")
            {
                SB.Append("attachment; ");
            }
            SB.Append("filename=");
            SB.Append(FileName);

            Response.AddHeader("Content-Disposition", SB.ToString());
            Response.BinaryWrite(Data);
            Response.Flush();
            Response.End();

            return new EmptyResult();
        }
    }
}
This controller class performs the following tasks:
  • Retrieve the uploaded files from the web session
  • Save a new file
  • Delete an existing file
  • Send the file data to the web browser for download
If an error is encountered when working on the tasks, the action methods will choose the view "Error.aspx" implemented in the "View\Shared" folder to display the error message to the user.

Run the Application

Now we completed the development of this example application. With the Visual Studio in focus, we can press "F5" to debug run the application. Click on the "Click to upload a new file" link, the application will display the file upload control to allow the user to browse a file to upload to the server. After uploading several image files, the application looks like the following:
RunApp.jpg Click the "Delete this file" link, you can delete the selected file. Click on the "ID" of the file, the image will be displayed in a pop-up window:
RunAppShowPicture.jpg Click on the "Download this file" link, you will be allowed to download the chosen file:
RunAppDownload.jpg Click the "Open" button, the image file will be downloaded and shown as the following:
RunAppDownloadResult.jpg

Points of Interest

  • There are many ways to upload and download files to and from the web server. This article simply shows how it is done in an ASP.NET MVC application in one of the MVC controllers.
  • There is a hot discussion on whether we should use code-behind files for the views in ASP.NET MVC applications. If we do not use the code-behind files, we will need to use in-line server scripts in the views.
  • The files uploaded to the server are saved in a "DataTable" in the web session, so Visual Studio is the only thing needed to develop and debug the application. In any practical web applications, persistent data storage is needed to save the files.
  • For demonstration purposes, the file upload and download functionalities are built into a standalone web application, in more comprehensive IT applications, these functionalities are more likely built in a sub-system of a bigger application.
  • In order to program the client side JavaScript program easier, the jQuery library is used. With the help of the jQuery library, browser compatibility issue is no longer a major concern. I have tested this application in "Internet Explorer", "FireFox", and "Google Chrome". The application functions well in all the three major browsers.
  • This application uses the utility class "ApplicationUtility" to address the "relative path problem" related to the ASP.NET MVC applications. The "FormatURL" method is to use the "DeploymentVirtualDirectory" information configured in the "Web.config" file to make sure the correct URLs are generated regardless of where the application is deployed. When debug run the application, the "DeploymentVirtualDirectory" needs to be an empty string. At deployment time, the correct virtual directory name needs to be written into the "Web.config" file.

0 comments:

 
Toggle Footer