Yes, I know, the multiple file uploading in ASP.NET issue has been exhaustively covered already elsewhere. There are examples of multiple file uploading using ASP.NET user controls, there are examples using jQuery, there are examples using flash to provide progress feedback, and even examples in Silverlight.
However, what I wanted was a simple solution which allows the user to select a variable number of files, didn’t rely on postbacks in order to increase or decrease the number of files selected, and also allows the user to enter a description for each uploaded file. The description was to be saved to the database, and the file saved to the server’s file system (with the path stored in the database so the file can be subsequently retrieved).
The way the page should work was also straightforward: the user should initially be presented with a single file upload control and file description textbox, but they should have the option to add and remove additional rows. Here’s a quick screengrab to illustrate what we’re aiming for:

It is very possible to create an upload page of this type using a ListView or Repeater control, using similar code to that I outlined in an earlier article, however this would require additional postbacks for the user to add or remove files, which is not what I wanted.
The alternative is, of course, to use javascript to create the additional upload controls and textboxes. Not in itself a hugely tricky task, but there is one usability issue which needs to be resolved. As any ASP.NET developer already knows, any changes made to the page client-side are not persisted by the ASP.NET viewstate, so will disappear after the page posts back.
This is not particularly desirable behaviour in this case. Let’s say after the user adds several files and clicks the upload button, and I then want to validate the file extensions server side, in order to display a message to the user if they have uploaded a file with an invalid extension (I may want to exclude all .exe files, for example). If all the file input fields have been created using javascript, after the postback they will have disappeared, so the user will have to input the whole lot again if the validation fails. This would be a hugely irritating thing to force users to do.
The solution is to manually re-create the controls on each page load. This involves keeping track of the number of files which have been created each time the page posts back, and ensuring that the appropriate javascript is invoked to rebuild the table.
So, enough with the rambling: let’s get to the code.
I started off with some page markup and a little CSS:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
<title>Upload Multiple Files</title>
<style type="text/css">
#tblFilesContainer
{
width: 80%;
border: solid 1px black;
}
#tblFilesContainer tr td
{
border: dashed 1px black;
}
#tblFilesContainer tr.noborder td
{
border: none;
}
#tblFilesContainer tr td input
{
width: 99%;
}
</style>
</head>
<body>
<form id="form1" runat="server" enctype="multipart/form-data" method="post">
<h3>Upload Files</h3>
<br/>
<table id="tblFilesContainer" style="width:80%; border: solid 1px black">
<tr class="noborder">
<td style="width: 10%"></td>
<td style="width: 30%"></td>
<td style="width: 10%"></td>
<td style="width: 30%"></td>
<td style="width: 20%"><a href="#" onclick="return addFile();" >Add File</a></td>
</tr>
</table>
<br />
<asp:Button ID="btnUpload" runat="server" Text="Upload" OnClick="btnUpload_Click" />
</form>
</body>
</html>
In the markup above, I have created a table which will act as the container for the file upload controls and description textboxes, a link which will act as the trigger for adding an additional file input control row to the table, and some very basic styles to add some borders to the table. Note that the link is executing a javascript function named addFiles() onclick (and also passing itself in as a parameter value): we’ll return to this shortly.
Finally, there is a button which will be used to submit the page and kick off the uploading- this is hooked up to a server-side event handler.
One thing to watch out for: you must remember to add the enctype="multipart/form-data" attribute to the form tag on the page. ASP.NET only adds this when one or more FileUpload controls exist on a given page. As all the file input fields are being created by javascript in this case, it won’t get added unless you do so manually.
It was then time to start with the javascript. The table which contains the upload controls has five cells on each row, so we’re going to need a bit of code which can create a table row with five cells, and insert it into the existing table, but leaving the last table row in the same position (as it contains the ‘Add File’ link). I started by placing the HTML markup for each cell into a javascript array in a script block:
<script type = "text/javascript">
var cellTemplates = new Array();
cellTemplates[0] = 'Description:';
cellTemplates[1] = '<input id="txtDescription{counter}" name="txtDescription{counter}" type="text" value="{value}" />';
cellTemplates[2] = 'Select File:';
cellTemplates[3] = '<input id="file{counter}" name="file{counter}" type="file" />';
cellTemplates[4] = '<a href="#" id="lnkRemoveFile{counter}" onclick="return removeFile(this);">Remove</a>';
var counter = 0;
ript>
This was to prevent having to do a whole bunch of messy string manipulation in the function which will add new rows to the table.
There is also a variable named ‘counter’ which is used to track the current file input row count.
Note the above HTML markup contains a couple of placeholders: {counter} and {value}. These placeholders are replaced at runtime with variable values. The {counter} placeholder is replaced with the value of the counter variable. This is to ensure all the controls are named sequentially to make it easier to retrieve their values server-side. The {value} placeholder is replaced with the file description- and this only occurs when the page is reloaded after a postback.
Now for the functions to add and remove file input rows. Here’s the full code:
//adds a file input row
function addFile(description) {
//increment counter
counter++;
var tbl = document.getElementById("tblFilesContainer");
var rowCount = tbl.rows.length;
var row = tbl.insertRow(rowCount - 1);
var cell;
var cellText;
for (var i = 0; i < cellTemplates.length; i++) {
cell = row.insertCell();
//format the cell template text
cellText = cellTemplates[i].replace(/\{counter\}/g, counter).replace(/\{value\}/g,
(description == null) ? '' : description);
cell.innerHTML = cellText;
}
}
//removes a file input row
function removeFile(ctrl) {
var tbl = document.getElementById("tblFilesContainer");
if (tbl.rows.length > 2)
tbl.deleteRow(ctrl.parentNode.parentNode.rowIndex);
}
The addFile() function starts by retrieving the current file input row count and incrementing it- it then adds the additional row to the table, by retrieving the template HTML markup from the cellTemplates array and replacing the placeholders with the appropriate values, as outlined above. It makes use of javascript’s regular expression find-and-replace functionality to inject the counter and description values. And don’t forget the ‘g’ modifier at the end of the regular expression; this results in a global find-and-replace (i.e. all matching instances of the search pattern are replaced- the default behaviour is just to replace the first instance found).
As mentioned above, when a ‘Remove File’ link is clicked, the link passes itself as the parameter value to the removeFile method. The method uses this to select the link’s parent node (i.e. the table cell it is in), and then the parent of that node (the table row) and removes it,
I added one additional javascript function to reinitialise all the textboxes after a page reload: here it is:
//creates all file inputs on page load
function createFiles(descriptions) {
for (var i = 0; i < descriptions.length; i++) {
addFile(descriptions[i]);
}
}
The method simply accepts in an array of file descriptions, and uses this to recreate each of the file input rows.
Then to the server-side code. Firstly, the handler for the upload button’s click event: this needed to extract the files and descriptions from the HttpRequest and pass all this information through to a business logic class for validation and saving. In my code, I am collecting all the files and descriptions first, and passing these through to the business logic layer in a single hit, but to keep the example concise, I have removed all this extraneous code to concentrate on getting the file and description. Here is the button click event handler in full:
protected void btnUpload_Click(object sender, EventArgs e)
{
int counter;
string description;
HttpPostedFile file;
foreach (string key in Request.Files.Keys)
{
//get a reference to the file
file = Request.Files[key];
if (file != null && file.ContentLength > 0)
{
//get the value of the description textbox
counter = int.Parse(key.Replace("file", string.Empty));
description = Request.Form["txtDescription" + counter.ToString()];
//save file and description here...
}
}
}
Because the file upload controls are description textboxes were named with matching integer values appended to their names, it is possible to iterate over the file input IDs and use each ID to find the relevant description in the Request.Form collection.
So everything is now in place to allow users to upload multiple files, but we still haven’t solved the original problem of maintaining state between postbacks.
The final code example fills in the last missing piece of functionality:
protected void Page_Load(object sender, EventArgs e)
{
//register the client scripts
this.RegisterFileInputScripts();
}
/// <summary>
/// Registers the startup scripts to create the file input controls.
/// </summary>
private void RegisterFileInputScripts()
{
if (!Page.IsPostBack)
{
//JS: create the initial file input control
Page.ClientScript.RegisterStartupScript(this.GetType(),
"CreateFileInput", "addFile();", true);
}
else
{
StringBuilder text = new StringBuilder();
int counter;
SortedList<int, string> descriptions = new SortedList<int,string>();
foreach (string key in Request.Files.Keys)
{
counter = int.Parse(key.Replace("file", string.Empty));
descriptions.Add(counter, (Request.Form["txtDescription" + counter.ToString()]));
}
//JS: add the file descriptions to a javascript array
text.Append("var descriptions = new Array(");
int i = 0;
foreach (KeyValuePair<int, string> item in descriptions)
{
text.AppendFormat("'{0}'", item.Value ?? string.Empty);
if (++i < descriptions.Keys.Count)
text.Append(",");
}
//JS: close the array declaration &invoke the createFiles method
text.Append("); createFiles(descriptions);");
//register the javascript
Page.ClientScript.RegisterStartupScript(this.GetType(), "RecreateFileInputs",
text.ToString(), true);
}
}
The code snippet above ensures that the addFile() method is called once when the page first loads, to create the initial file input control. And on each postback it iterates over the posted files collection, building up an array of file descriptions corresponding to each posted file, and finally registers a call to the createFiles javascript method, passing in an array containing all the descriptions.
And that's a wrap: a basic multiple file upload system, driven by client-side javascript rather than server-side code. As ever, you can download the source code using the link at the top of the article.