Thursday, August 19, 2010

How to upload multiple files using custom web control like asp.net DropDownList

‘How to handle multi file upload?’ is a frequently asked a question through the web developing community. There are numerous solutions being published. In this blog I have posted another post to demonstrate how to upload multiple files at the same time using third party plug-in called file uploadify. However this post aims to answer the same question in different approach. Idea is to have a file list control like asp.net DropDownList control. This is a very basic implementation but this code has holes for template.  This example uses JQuery.

Demo:


Page:

Markup:
Mark-up very simple, first we have to register the asp.net control with tag prefix. In this case, this control is hosted in the name space called ActiveTest and in an assembly ActiveTest.dll. Then we can add FileList control as same as we add a DropDownList to the page.
<%@ Page Language="C#" %>
<%@ Register Assembly="ActiveTest" Namespace="ActiveTest" TagPrefix="asp" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head2" runat="server">
    <script type="text/javascript" src="Scripts/jquery-1.4.1.min.js"></script>
</head>
<body>
    <form id="form2" runat="server">
        <asp:FileList runat="server" ID="flFileList" DataNameField="Name" DataIdField="Id" />
        <asp:Button runat="server" ID="Button1" Text="Save" />
    </form>
</body>
</html>
Code
At the end of the section, you will be able to find a class called Document, which I use to DataBind the FileList control. I hold list of Documents in the ViewState for the purpose of demonstration. You might get this from database.  In the page load method, if page is not a postback, I do create an initial list of fake documents and bind it to the file list. Then again in real world this should come from the database or relevant source.
Then the Save event, probably the most important bit that we may consider. Save event happens as a consequence of save button click, please see the mark-up to locate save button.  In the save button I repopulate the document list that I have created in the initial page load. Then I loop through all the items in the FileList. FileList Item (definition can be found at the end of this post) has four major properties.
  1. IsNew - A new file
  2. IsEmpty - User has clicked add more but has not selected a file
  3. IsEdited - User has edited the existing file
  4. IsExisting  - Not edited existing file
Then I check the state of the item. If file is new or edited then we have to save the file.  Then again before I save the file for the safety, I check if the item has a file or not.  At the same time I populate new Document object and add to my document list. If the file is existing file, we don’t have to save the file but of course we have to add the document object created based on existing file to my document list in-order to bind the FileList.
public partial class Test : Page
{
    private List<Document> documents
    {
        get { return ViewState["Documents"as List<Document>; }
        set { ViewState["Documents"] = value; }
    }
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        if (!this.IsPostBack)
        {
            documents = new List<Document>();
            for (int i = 0; i < 10; i++)
                documents.Add(new Document() { Name = "Document Title " + i, FileName = "Doc.doc", Id = "Id" + i });
            this.flFileList.DataSoruce = documents;
            this.flFileList.DataBind();
        }
    }
    protected void Save(object sender, EventArgs e)
    {
        this.documents.Clear();
        foreach (FileListItem item in this.flFileList.Items)
        {
            ///
            /// Save files 
            /// Re-Populate items list
            ///
            if (item.IsEdited || item.IsNew)
            {
                Document d = new Document();
                if (item.HasFile)
                {
                    d.Name = item.File.FileName;
                    d.FileName = item.File.PostedFile.FileName;
                    d.Id = item.IsNew ? "Id" + this.flFileList.Items.IndexOf(item) : item.Id;
                    ///
                    /// item.File.PostedFile.SaveAs("pathToSave");
                    ///                            
                }
                this.documents.Add(d);
            }
            else if (item.IsExisting)
                this.documents.Add(new Document() { Name = item.Name, Id = item.Id });
        }
        ///
        /// Databind
        ///
        this.flFileList.DataSoruce = documents;
        this.flFileList.DataBind();
    }
}
 
[Serializable]
public class Document
{
    public string Id { getset; }
    public string Name { getset; }
    public string FileName { getset; }
}

Control Implementation:
FileList control is probably the most important bit of the post. If you in a hurry and need to enjoy the rest of the day, you may use the control implementation as it is, and get the example running. But for the people who are curious it works, I will explain how it works.
  1. Control has a property called MaxFiles. When it does not have initial list of files to display (i.e. fresh file list), control shows only a single file upload and add more button right next to file upload control.
  2. By clicking add new button you will be able to add another file upload input without a postback, so it is quick and will NOT destroy your excising selected files.
  3. You will be able to add more file uploads up to the number you specify in the MaxFiles property. By default MaxFiles is 5.
  4. When you save selected files, and then in the save event if you rebind the list with saved document list, control will show file names in read-only text boxes and edit button next to each text box to edit them.  By clicking edit button textbox and edit button get converted in to file upload control where you may chose a different file as an edit.
  5. Next to existing files there is a empty input field and add more button which behaves exactly same as we discussed in the point 1 and 2.
Private script variable:
Control uses java scripts to show additional file input fields without using postbacks. Script variable holds the script for this. In the runtime we inject couple of values to the script like currentRow which depends on bounded data source and maxRowCount which corresponds to MaxFile.  Then in the control’s pre-render method, we register the this script with the type of FileList control and ClientID as the key so that each FileList control in the page will have separate script.
Private style variable:
File list control specific styles which will get added to the page header. Those who want to control the disabled colour, textbox disabled colour get injected to styles at the runtime and get registered with the page in the pre-render method.
CreateControlHeirarchy() Method:
The CreateControlHeirarchy() method is the master beast which get called in the page init method. In this method execution loops through the existing items and add necessary controls to the control. Please note the ItemTemplate property. Please add more logic here to get this control customized with template. Each row of control will have a designated index and control classes will get appended with index to provide unique css class name so that jQuery can pick them up.
DataBind() Method:
This uses IEnumarable DataSource property. In the DataBind() method, execution loops through each item in the IEnumarable DataSource and it uses reflection to grill through the object and find the values. This reflection uses DataNameField property and DataIdField property to grab values to FileListItem. Then after populating all the file list items corresponds to data source then it call CreateControlHeirarchy() method to regenerate control collection.

[ParseChildren(true)]
[DefaultProperty("ItemCount")]
public class FileList : WebControlINamingContainerIPostBackDataHandler
{
    #region Attributes
 
    private List<FileListItem> items;
    private List<FileListRow> rows = new List<FileListRow>();
    private string script = @"
        var currentRow = {0};
        var maxRowCount = {1};
        function ShowEditFile(index) {{
            $("".FileName"" + index).hide();
            $("".EditButton"" + index).hide();
            $("".FileUpload"" + index).show();
            return false;
        }}
        function ShowAddFile() {{
            $("".FileListRow"" + currentRow).show();
            currentRow++;
            if (currentRow == maxRowCount)
                $("".AddMore"").attr(""disabled"", ""disabled"");            
            return false;            
        }}
    ";
    private string styles = @"
        <style type=""text/css"">
            .FileList .Hide {{ display:none; }}
            .FileList .FileName {{ background-color: {0}; }}        
        </style>
    ";
 
    #endregion
 
    #region Properties
 
    public List<FileListItem> Items { get { return this.items; } }
    public List<FileListRow> Rows { get { return this.rows; } }
    public int MaxFiles { getset; }
    public IEnumerable DataSoruce { getset; }
    public string DataNameField { getset; }
    public string DataIdField { getset; }
    public int ItemCount { getset; }
    public string ReadOnlyColor { getset; }
 
    [PersistenceMode(PersistenceMode.InnerProperty)]
    [TemplateContainer(typeof(FileListRow))]
    public ITemplate ItemTemplate { getset; }
 
    #endregion
 
    #region Constructors
 
    public FileList()
        : base(HtmlTextWriterTag.Div)
    {
        this.Initialize();
    }
    public void Initialize()
    {
        this.MaxFiles = 5;
        this.ReadOnlyColor = "#ccc";
    }
 
    #endregion
 
    #region Methods
 
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        this.CreateControlHeirarchy();
    }
    public void CreateControlHeirarchy()
    {
        this.Controls.Clear();
        this.TrackViewState();
        this.CssClass = string.Format("{0} {1}"this.CssClass, this.GetType().Name).Trim();
        if (this.items != null)
        {
            this.ItemCount = this.items.Count;
            foreach (FileListItem item in this.items)
                this.AddItem(this.items.IndexOf(item), item);
        }
        else
        {
            int itemCount = 0;
            if (int.TryParse(HttpContext.Current.Request.Form[this.UniqueID], out itemCount))
                this.ItemCount = itemCount;
            for (int i = 0; i < this.ItemCount; i++)
                this.AddItem(i, null);
        }
        for (int i = this.ItemCount; i < this.MaxFiles + this.ItemCount; i++)
        {
            FileListRow row = new FileListRow()
            {
                CssClass = string.Format("{0} {0}{1} {2}",
                    typeof(FileListRow).Name,
                    i, i == this.ItemCount ? string.Empty : "Hide").Trim()
            };
            this.Controls.Add(row);
            this.rows.Add(row);
            if (this.ItemTemplate != null)
                this.ItemTemplate.InstantiateIn(row);
            row.Controls.Add(new FileUpload()
            {
                ID = string.Format("FileUpload{0}", i),
                CssClass = string.Format("FileUpload{0}", i)
            });
            row.Controls.Add(new HiddenField()
            {
                ID = string.Format("FileId{0}", i)
            });
        }
        this.Controls.Add(new Button()
        {
            Text = "Add More",
            OnClientClick = "javascript:return ShowAddFile()",
            CssClass = "AddMore"
        });
    }
    private void AddItem(int index, FileListItem item)
    {
        FileListRow row = new FileListRow() { CssClass = typeof(FileListRow).Name };
        this.Controls.Add(row);
        this.rows.Add(row);
        if (this.ItemTemplate != null)
            this.ItemTemplate.InstantiateIn(row);
        TextBox txtFile = new TextBox()
        {
            ID = string.Format("FileName{0}", index),
            CssClass = string.Format("FileName{0} FileName", index)
        };
        if (item != null) txtFile.Text = item.Name;
        txtFile.Attributes.Add("readonly""readonly");
        row.Controls.Add(txtFile);
        row.Controls.Add(new Button()
        {
            Text = "Edit",
            ID = string.Format("EditButton{0}", index),
            CssClass = string.Format("EditButton{0} EditButton", index),
            OnClientClick = string.Format("javascript:return ShowEditFile({0})", index)
        });
        row.Controls.Add(new FileUpload()
        {
            ID = string.Format("FileUpload{0}", index),
            CssClass = string.Format("FileUpload{0} Hide FileUpload", index)
        });
        HiddenField fileId = new HiddenField() { ID = string.Format("FileId{0}", index) };
        if (item != null) fileId.Value = item.Id;
        row.Controls.Add(fileId);
    }
    public override void DataBind()
    {
        base.DataBind();
        this.items = new List<FileListItem>();
        if (this.DataSoruce != null)
        {
            foreach (object item in this.DataSoruce)
            {
                if (!string.IsNullOrEmpty(this.DataNameField))
                {
                    FileListItem i = new FileListItem();
                    string fn = string.Empty;
                    string fi = string.Empty;
                    foreach (PropertyInfo p in item.GetType().GetProperties())
                    {
                        if (p.Name.Equals(this.DataNameField))
                        {
                            fn = (p.GetValue(item, null) ?? string.Empty).ToString();
                            i.Id = i.Name = fn;
                        }
                        if (!string.IsNullOrEmpty(this.DataIdField) && p.Name.Equals(this.DataIdField))
                        {
                            fi = (p.GetValue(item, null) ?? string.Empty).ToString();
                            i.Id = string.IsNullOrEmpty(fi) ? fn : fi;
                        }
                    }
                    this.items.Add(i);
                }
                else
                {
                    string value = (item ?? string.Empty).ToString();
                    this.items.Add(new FileListItem() { Name = value, Id = value });
                }
            }
        }
        this.CreateControlHeirarchy();
    }
    protected override void RenderContents(HtmlTextWriter writer)
    {
        base.RenderContents(writer);
        writer.Write("<input type=\'Hidden\' value=\'{0}\' id=\'{1}\' name=\'{2}\' />"this.ItemCount, this.ClientID, this.UniqueID);
    }
    protected override void OnPreRender(EventArgs e)
    {
        base.OnPreRender(e);
        if (this.Page != null)
        {
            this.Page.ClientScript.RegisterClientScriptBlock(
                this.GetType(),
                this.ClientID,
                string.Format(this.script, this.ItemCount + 1, this.ItemCount + this.MaxFiles),
                true);
            Literal styles = new Literal() { Text = string.Format(this.styles, this.ReadOnlyColor) };
            if (this.Page.Header != null)
                this.Page.Header.Controls.Add(styles);
        }
    }
 
    #endregion
 
    #region IPostBackDataHandler Members
 
    public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
    {
        this.ItemCount = int.Parse(postCollection[postDataKey]);
        this.items = new List<FileListItem>();
        foreach (FileListRow row in this.rows)
        {
            int index = this.rows.IndexOf(row);
            TextBox txtFile = row.FindControl(string.Format("FileName{0}", index)) as TextBox;
            HiddenField hdnId = row.FindControl(string.Format("FileId{0}", index)) as HiddenField;
            FileUpload fuFile = row.FindControl(string.Format("FileUpload{0}", index)) as FileUpload;
            FileListItem item = new FileListItem();
            if (txtFile != null) item.Name = txtFile.Text;
            if (hdnId != null) item.Id = hdnId.Value;
            if (fuFile != null) item.File = fuFile;
            this.items.Add(item);
        }
        return true;
    }
    public void RaisePostDataChangedEvent()
    {
 
    }
 
    #endregion
}


Support Classes:
FileListRow and FileListItem are the two support classes for the FileList control. There are no descriptive logic but solely there are there to provide data structures to the FileList control.
[Serializable]
public class FileListRow : WebControlINamingContainer
{
    public FileListRow()
        : base(HtmlTextWriterTag.Div)
    {
 
    }
}
[Serializable]
public class FileListItem
{
    #region Attributes
 
    private bool hasIdAndName
    {
        get { return !string.IsNullOrEmpty(this.Name) && !string.IsNullOrEmpty(this.Id); }
    }
 
    #endregion
 
    #region Properties
 
    public string Name { getset; }
    public string Id { getset; }
    public FileUpload File { getset; }
    public bool HasFile
    {
        get
        {
            return this.File != null && this.File.HasFile;
        }
    }
    public bool IsEdited
    {
        get { return this.hasIdAndName && this.HasFile; }
    }
    public bool IsNew
    {
        get { return !this.hasIdAndName && this.HasFile; }
    }
    public bool IsEmpty
    {
        get { return !this.hasIdAndName && !this.HasFile; }
    }
    public bool IsExisting
    {
        get { return this.hasIdAndName; }
    }
 
    #endregion
}

3 comments:

adilahmedmd said...

Hi
Can u explain about this code and how to create control fieldlist

Charith Shyam Gunasekara said...

Updated on 30-08-2010 with much explanation as a response to adilahmedmd

Anonymous said...

nvmd i see there is a text box in solution thanks again

iPhone Launch Screen Sizes

iPhone Portrait iOS 8 Retina HT 5.5 = 1242 X 2208 Retna HD 4.7 = 750 X 1134 iPhone Landscape iOS 8 Retina HD 5.5  2208 X 1242 iPho...