Tuesday, December 14, 2010

File Management with Searching, Uploading, Copying and Email function

Sample Image


This is a fully functional file management application with basic searching function provided by Microsoft Indexing Service exposed via HTTP Web service. To unify the approach of file objects presented to UI tier, I follow the design pattern to define an abstract class to imitate the file object returned either from mounted folder or searching result. Using a customized ICollection implementation, DataGrid can use it as data source and list items with paging. In addition, file and folder copying function, email file as attachment and web caching function are included.


Features of this file management application include:

  • Any folder under web server is mounted by configuration setting in AppSetting section setting.
  • Searching is provided by Microsoft Indexing Service exposed via HTTP Web service.
  • DataGrid uses data source from an ICollection implementation with item of an abstract base class providing further extensible possibility.
  • Both list and iconic view are provided.
  • A simple uploading function is included.
  • Navigation freely under the folder tree with mouse click.
  • File items are categorized by file type in a File Handler - FileSender.aspx. So response action for each file category is highly customizable, for example, image stream is sent when GIF file is selected but ZIP file is sent as an attachment to the browser.
  • Application cache is supported to improve list browsing performance. The cache expiry time is configurable in WEB.CONFIG.
  • Email via SMTP server function gives user option to email file item without the need of downloading it first from server to client PC.
  • Copy files and whole subfolder tree from source to destination.

File Management Web Page 1

Figure 1 - File Management Web Page in List View

File Management Web Page 2

Figure 2 - File Management Web Page in Iconic View

Send File as Attachment

Figure 3 - Send File as attachment via SMTP server


Setup steps are as below:

  • Run setup.exe.
  • As searching is provided by Microsoft Indexing Service, use Computer Management and select Indexing Service to setup the required catalog. If you do not want to create your own catalog, the default catalog - system can be used. Just check your intended exposing folder is already included under Directories property of system catalog or not and add it if it is not already included. But I suggest you create a new catalog and include the intended serving root folder under the new catalog's Directories property for performance purpose. In doing so, it will improve response time when narrowing down the searching scope for your application.

    Indexing Service Setup

    Figure 4 - Indexing Service Setup

  • After setup, there will be two virtual directories created under your web server and they are:
    1. filemanagement Web Application- it is where the main file management application resides.
    2. SearchingService Web Application- it is where the Http Web service for searching resides.

    For the filemanagement Web Application, please uncheck the Anonymous access and use only Integrated Windows Authentication to protect your folder as when accessing via Internet. It relies on NTFS authorization checking, so make sure that you have set proper access rights under your intended exposing folder.

    Authentication Setup

    Figure 5 - Authentication Setup under IIS

  • In the WEB.CONFIG file under the virtual folder of SearchingService Web Application, modify the key IndexCatalog under appSettings section to match with the new catalog you have created in the previous step:
    <appSettings> <add key="IndexCatalog" value="system" /> </appSettings> 
  • In the WEB.CONFIG file under the virtual folder of filemanagement Web Application, modify the key Root under appSettings section to match with the folder path you intended to serve over your web site. You can also setup your company name here:
    <appSettings> <add key="Company" value="Ever-Rising System (HK) Ltd" /> <add key="Root" value="C:\" /> </appSettings> 
  • Normally, when setup as Integrated Windows Authentication, web server only prompt your password when accessing your web server via Internet or you have not sign-on to the network (Assumed that you are using Domain Network or using the same user name and password in your PC and web server).

    If however, you are accessing your Intranet web server and still have an annoying popup login dialog, you shall check your IE zone settings. In the Security tab, Custom Level setting, there is a parameter as below:

    Custom Zone Level setting

    Figure 6 - Custom Zone Level setting

    The default setup should allow automatic logon when using Intranet Zone resources. However, you shall also check the web server name is under Intranet Zone or not, for example, when browsing your web server, the IE status bar will indicate in which Zone does it reside:

    IE Zone type

    Figure 7 - IE Zone type

    Of course, it is normal if you are browsing your web server from remote and it indicates internet. But if you are using the same local network and it does not reside in Intranet Zone, you can still add the web server to the Intranet Zone as below by using the Local Intranet Sites setup and clicking Advanced button under the Security tab of IE Option menu to add it:

    Intranet Zone setting

    Figure 8 - Intranet Zone setting

  • As the application uses Windows Authentication, the default setting of WEB.CONFIG for authentication is:
    <AUTHENTICATION mode="Windows" /> <IDENTITY impersonate="true" /> 

    Impersonate is set to true, such that the current Windows user at client is impersonated when accessing resources in web server.

  • Email service is optional and configured in appSettings section of WEB.CONFIG as below:
    <ADD value="true" key="HasEmailService" /> <!-- Control email sending thread (use main thread or thread pool) --> <ADD value="true" key="SendByThreadInPool" /> <ADD value="true" key="LogNeeded" /> <ADD value="info@yourdomain.com" key="EmailFromUser" /> <ADD value="smtp.yourdomain.com" key="SMTPServer" /> <ADD value="SMTPUserID" key="SMTPUser" /> <ADD value="SMTPPAssword" key="SMTPPwd" /> <ADD value="log/FileManagementSiteLog.txt" key="FileManagementSiteLog" /> 

    The settings are explained as below:

    1. The parameter HasEmailService controls whether email service is provided.
    2. The parameter SendByThreadInPoll controls whether email is sent by .NET provided thread pool. This can increase response time for UI as another thread is used when email is to be sent. I suggest you leave it as true and only set to false for debugging purpose when email sending has problem.
    3. EmailFromUser is the default user filled in the From Address entry when a file item is needed to be sent. That can be overridden in the SendMail.aspx page by the user.
    4. STMPServer, SMTPUser and SMTPPwd are optional settings for your SMTP server location, SMTP Authentication User and Password respectively. If they are not set, localhost is assumed and default authentication with SMTP server is used.
    5. FileManagementSiteLog is the location of the log file and default to log folder under application root.
  • As application will write log entries (now is email log only) in the log file set at previous step, please make sure ASPNET user have WRITE access right in this folder (e.g. C:\Inetpub\wwwroot\filemanagement\log).
  • All lists of items sent to browser are cached by using ASP.NET Application Cache to improve performance especially large number of users are using the web server. The cached item expiry time in minutes can be set in the appSettings section of WEB.CONFIG with the parameter ApplicationCacheTimeOut. Default is five minutes.


Define An Abstract Class

As each file or folder object will have certain properties in common, an abstract base class SimpleFileInfoBase is created as below to provide necessary fields when binding with UI elements (e.g. DataGrid):

abstract public class SimpleFileInfoBase {     public abstract string Name { get ; }     public abstract string FullName { get ; }     public abstract DateTime LastWriteTime { get ; }     public abstract long Size { get ; } } 

In this class, Name denotes the short name, FullName denotes the full path for the file or folder object. The actual implementation is to derive class FileSystemInfoExtend from SimpleFileInfoBase and it is straight forward, and just wrapping the original FileSystemInfo class is enough.:

public class FileSystemInfoExtend : SimpleFileInfoBase {     private FileSystemInfo _file ;      public FileSystemInfoExtend(FileSystemInfo file)     {         _file = file ;     }      override public string Name     {         get { return _file.Name ; }     }      override public string FullName     {         get { return _file.FullName ; }     }      public bool IsDirectory     {         get         {              return (_file.Attributes & FileAttributes.Directory)                  ==FileAttributes.Directory ;         }     }      public string Type     {         get { return this.IsDirectory?"Dir":"File" ; }     }      override public long Size     {         get         {               if ( this.IsDirectory )                 return 0L ;             else                 return ((FileInfo)_file).Length  ;         }     }      override public DateTime LastWriteTime     {         get { return _file.LastWriteTime ; }     } }

Why I decide to define an abstract class SimpleFileInfoBase first before implementing the actual class is because the searching result item type from indexing service shares the same abstract class SimpleFileInfoBase with folder browsing returned item type FileSystemInfoExtend by deriving class SearchResultItem from it as below:

public class SearchResultItem : SimpleFileInfoBase {     private string _Name ;     private string _FullName ;     private DateTime _LastWriteTime ;     private long _Size ;      // Interface to Indexing Service used OleDb       //which returns System.Data.DataSet type object.      // By consuming the DataRow object, the data can be       //transformed before presenting to UI.      public SearchResultItem(DataRow row)     {         _Name=           row["Filename"]==DBNull.Value?string.Empty:(string)row["Filename"];         _FullName=           row["Path"]==DBNull.Value?string.Empty:(string)row["Path"] ;         _LastWriteTime =            row["Write"]==DBNull.Value?DateTime.MinValue:(DateTime)row["Write"];         _Size = row["Size"]==DBNull.Value?0L:(long)row["Size"] ;     }      override public string Name { get {return _Name ; } }     override public string FullName { get { return _FullName; } }     override public DateTime LastWriteTime {                   get { return _LastWriteTime ; } }     override public long Size { get { return _Size ; } }  } 

As I have use two different ways to list items, folder browsing function using classes from System.IO and searching function using classes from System.Data.OleDb (when accessing Indexing Service), both results shall be factorized as common base class before they are presented to DataGrid as the data source.

The System.IO classes create array of FileSystemInfo type objects when we use them to list files or subfolders from the requested path, for example, in the page WebFolder.aspx:

System.IO.DirectoryInfo CurrentRoot = new DirectoryInfo(this.RootPath); FileSystemInfo[] files ; 

On the other hand, result from Indexing Service using ADO.NET OleDb classes produces items in System.Data.DataTable object, for example:

string connstring = "Provider=MSIDXS;Data Source=" + _Catalog ; using ( OleDbConnection conn = new OleDbConnection(connstring) ) {     OleDbDataAdapter DataAdapter = new OleDbDataAdapter(_Query, conn);     DataSet DataSetSearchResult = new DataSet();     DataAdapter.Fill(DataSetSearchResult, "SearchResults");     return DataSetSearchResult  ; } 

After implementing two ICollection classes with collection of items from these two classes, they can share common properties which can be accessed in UI (DataGrid) consistently.

// ICollection implementation for FileSystemInfoExtend items  public class FileSystemInfosExtend : ICollection {     private FileSystemInfoExtend[] _files ;     // ... Other stuffs  }   // ICollection implementation for SearchResultItem items  public class SearchResultItems : ICollection {     private ArrayList _SearchResultItems ;     public SearchResultItems(DataTable ResultDataTable)     {         _SearchResultItems = new ArrayList() ;         _SearchResultItems.Clear() ;         foreach ( DataRow row in ResultDataTable.Rows )             _SearchResultItems.Add( new SearchResultItem(row) ) ;     }         // ... Other stuffs  } 

Class design diagram

Figure 9 - Class design diagram

After we implemented ICollection classes, we can set the data source for the DataGrid to these ICollection class objects, for example:

// In WebFolder.aspx, bind the DataGrid as below  FileSystemInfosExtend FileInfosEx = new FileSystemInfosExtend(files) ; DataGrid1.DataSource = FileInfosEx ; DataGrid1.DataBind() ; 
// In Search.aspx, bind the DataGrid as below  localhost.FileSearch FileSearcherInst = new localhost.FileSearch() ; FileSearcherInst.Credentials = System.Net.CredentialCache.DefaultCredentials ; DataTable results =  FileSearcherInst.Search(RootPath, SearchText).Tables[0] ; SearchResultItems searchResultItems = new SearchResultItems(results) ; DataGrid1.DataSource = searchResultItems  ; DataGrid1.DataBind(); 

Separate Cases for Requesting Folder and File Item

Obviously, requesting for folder and file item are two different things; they need to be handled separately. Folder request will trigger recursive link to same ASPX page (WebFolder.aspx or WebFolderTNView.aspx) with different request path parameter (Query String Parameter) but file request will be linked to another ASPX page (FileSender.aspx) where file item requested will be handled. To address the two situations, a function is created as below:

protected string FormatLink(object file) {     FileSystemInfoExtend FileSystemInfoEx = file as FileSystemInfoExtend ;     if ( FileSystemInfoEx == null )         return "" ;      string FileFullName = Server.UrlEncode(FileSystemInfoEx.FullName) ;      if ( FileSystemInfoEx.IsDirectory )        return string.Format("{0}?path={1}"        , this.Request.Path, FileFullName ) ;     else        return string.Format("{0}?file={1}"        , "FileSender.aspx", FileFullName ) ; } 

This function will be used by the Name column of DataGrid as below:

<ASP:HYPERLINK Text='<%# DataBinder.Eval(Container, "DataItem.Name") %>' navigateurl='<%# FormatLink(DataBinder.Eval(Container, "DataItem")) %>' runat="server"> </ASP:HYPERLINK> 

File Item Requested Handler

Actually, the file item handling page is very simple and I am sure making it as an IHttpHandler handler is even better to improve performance. ASPX page derived from IHttpHandler too but provides more than needed service in this case! Here is the listing from the FileSender.aspx source:

private void Page_Load(object sender, System.EventArgs e) {     string FileFullPath = string.Format("{0}", Request["file"]) ;     if ( FileFullPath == "" )         return ;      FileInfo fileInfo =  new FileInfo(FileFullPath) ;     if ( !fileInfo.Exists )        return ;      Response.ClearHeaders() ;     Response.ClearContent() ;      switch ( fileInfo.Extension.ToLower()  )     {         case ".htm" :         case ".html" :         case ".asp" :         case ".aspx" :         case ".xml":             goto Send_File;         case ".txt":         case ".ini":         case ".log":             Response.ContentType = "text/plain" ;             goto Add_Disposition_Inline;          case ".jpg":             Response.ContentType =              string.Format("image/JPEG;name=\"{0}\"", fileInfo.Name) ;             goto Add_Disposition_Inline;          case ".gif":         case ".png":         case ".bmp":             Response.ContentType = string.Format("image/{0};name=\"{1}\""                 , fileInfo.Extension.TrimStart('.')                 , fileInfo.Name) ;             goto Add_Disposition_Inline;          case ".tif":             Response.ContentType =              string.Format("image/tiff;name=\"{0}\"", fileInfo.Name) ;             goto Add_Disposition_Inline;          case ".doc":             Response.ContentType = "Application/msword";             goto Add_Disposition_Inline;          case ".xls":             Response.ContentType = "Application/x-msexcel";             goto Add_Disposition_Inline;          case ".pdf":             Response.ContentType = "Application/pdf";             goto Add_Disposition_Inline;          case ".ppt":         case ".pps":             Response.ContentType = "Application/vnd.ms-powerpoint";             goto Add_Disposition_Inline;          case ".zip":             Response.ContentType = "application/x-zip-compressed" ;             goto Add_Disposition_Attachment;          // Others as attachment only!          default:             goto Add_Disposition_Attachment;     }       Add_Disposition_Attachment:         Response.AppendHeader("Content-Disposition"           , string.Format("attachment;filename=\"{0}\"", fileInfo.Name)) ;         goto Send_File;      Add_Disposition_Inline:         Response.AppendHeader("Content-Disposition"           , string.Format("inline;filename=\"{0}\"", fileInfo.Name)) ;         goto Send_File;      Send_File:         try         {             Response.WriteFile(FileFullPath) ;         }         catch (UnauthorizedAccessException)         {             string query = Request.UrlReferrer.Query ;             int i = query.ToLower().IndexOf("error=") ;             if ( i > -1 )             {                 int j = query.IndexOf("&", i) ;                 if ( j > -1 )                     query = query.Remove(i, j-i+1) ;                 else                     query = query.Remove(i, query.Length-i) ;             }              if ( query == "" )                 query = "?" ;             else if (!query.EndsWith("&"))                 query += "&" ;              Response.Redirect( Request.UrlReferrer.LocalPath                 + query                 + string.Format("Error=You are not allow to access file {0}."                 , fileInfo.Name)) ;        } } 

Iconic View

After all, to view the list as iconic items, we need to get the associated icons first and in page ShowFileIcon.aspx, it will handle icons retrieval and listing is as below:

icon = IconHandler.IconHandler.GetAssociatedIcon(fileinfo.FullName,                                                                  IconSizeUsed) ; if ( icon != null ) {     Response.ContentType = "image/x-icon" ;     string TempFileName =        fileinfo.Extension != ""            ? fileinfo.Name.Replace(fileinfo.Extension,".ico")            : fileinfo.Name+".ico" ;     Response.AppendHeader("Content-Disposition"        , string.Format("inline;filename=\"{0}\"", TempFileName)) ;     icon.Save(Response.OutputStream) ;     icon.Dispose() ; } 

The function IconHandler.GetAssociatedIcon() does the hard job to get icon resource for most file types registered in Windows and details are as below:

public enum IconSize : uint {     Small = 0x0, //16x16      Large = 0x1  //32x32  }  public class IconHandler {     // Filename - the file name to get icon from      public static IntPtr GetAssociatedIconHandle(string Filename, IconSize size)     {         IntPtr hImgSmall; //the handle to the system image list          IntPtr hImgLarge; //the handle to the system image list          SHFILEINFO shinfo = new SHFILEINFO();          if ( size == IconSize.Small )             hImgSmall = Win32.SHGetFileInfo(Filename, 0                 , ref shinfo,(uint)Marshal.SizeOf(shinfo)                 , Win32.SHGFI_ICON Win32.SHGFI_SMALLICON);         else             hImgLarge = Win32.SHGetFileInfo(Filename, 0                 , ref shinfo, (uint)Marshal.SizeOf(shinfo)                 , Win32.SHGFI_ICON  Win32.SHGFI_LARGEICON);          return shinfo.hIcon ;     }      // Filename - the file name to get icon from      public static Icon GetAssociatedIcon(string Filename, IconSize size)     {         return Icon.FromHandle(GetAssociatedIconHandle(Filename, size)) ;     }  }  [StructLayout(LayoutKind.Sequential)] public struct SHFILEINFO {     public IntPtr hIcon;     public IntPtr iIcon;     public uint dwAttributes;     [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]     public string szDisplayName;     [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]     public string szTypeName; };   internal class Win32 {     public const uint SHGFI_ICON = 0x100;     public const uint SHGFI_LARGEICON = 0x0; // 'Large icon      public const uint SHGFI_SMALLICON = 0x1; // 'Small icon          [DllImport("shell32.dll", CharSet=CharSet.Unicode)]     public static extern IntPtr SHGetFileInfo(         [MarshalAs(UnmanagedType.LPWStr)]  // Use wide chars          string pszPath         , uint dwFileAttributes         , ref SHFILEINFO psfi         , uint cbSizeFileInfo         , uint uFlags); } 

It makes use of the Win32 Shell Api functions and no existing Managed class provides such file information for us. So that is the only way to go and hopefully, we can get over it in the coming release of .NET.

File Upload

File upload function is too simple to discuss, just be aware that multiple files can be handled in server codes although I have not changed UI to allow it. Below is the function source:

private void buttonSubmit_ServerClick(object sender, System.EventArgs e) {     for(int i = 0; i < Request.Files.Count ; ++i)     {        HttpPostedFile file = Request.Files[i] as HttpPostedFile;        string path = string.Format(@"{0}\{1}"            , this.RootPath.TrimEnd('\\'), Path.GetFileName(file.FileName)) ;        file.SaveAs(path) ;     }      Response.Redirect(Request.Url.PathAndQuery) ;  } 

Email Service

Email service is provided with an wrapper class Email which mainly packages all functions provided by System.Web.Mail. Although implementation is straight forward, some key features are still worth to discuss.

Use Thread Pool to Send Email

I think one of the most ignored features of .NET framework is threading. Actually using threads in .NET is much easier than a lot of people expect. To send email via background thread is a very good candidate for such applicable area and codes for this feature are as below:

//  // In class Email send method  //  public void Send(string sTo, string sFrom, string sSubject , string sBody, string sCc, string sBcc, bool SendByThreadInPool) {     MailMessage mailMessage = new MailMessage();      // Other stuffs ...       SmtpMail.SmtpServer = _mailServer ;      if (!SendByThreadInPool)     {         SmtpMail.Send( mailMessage ) ;          if (this._Logger != null)         {             string messageInfo = string.Format(               "From:{0}\tTo:{1}\tSubject:{2}"               , mailMessage.From               , mailMessage.To               , mailMessage.Subject) ;              this._Logger.Write(messageInfo               + " completed successfully.") ;         }     }     else     {         // Use Thread pool to give immediate UI response          if (!ThreadPool.QueueUserWorkItem(new WaitCallback(Start)                , mailMessage))              throw new ApplicationException(               "Cannot queue task to send email.") ;     }      // Other stuffs ...  }  //  // In class Email Start method  //  private void Start(object mailMessage) {     MailMessage message = mailMessage as MailMessage ;     if (message != null)     {         string messageInfo = string.Format("From:{0}\tTo:{1}\tSubject:{2}"            , message.From, message.To, message.Subject) ;         try         {             SmtpMail.Send( message  );              if (this._Logger != null)                 this._Logger.Write(messageInfo + " completed successfully.") ;         }         catch(Exception excpt)         {             if (this._Logger != null)             {                this._Logger.Write(messageInfo + " failed.") ;                this._Logger.Write(excpt.ToString()) ;             }             else             // Re-throw the exception ;                 throw ;         }     } } 

The ThreadPool class is implemented in System.Threading assembly and has a method QueueUserWorkItem which gives us a handy feature to queue our task for processing by threads picked up from the .NET provided system thread pool. To have the function running by thread in the thread pool, we need to create a delegate WaitCallBack and to have the function layout matched with this delegate. The layout of the function required is:

void FunctioName(object state) ; 

The method Start of Email class actually conforms this standard and I have passed the MailMessage object to the called function when I queue the batch task.


When developing the background task program, we need to pay attention to error logging. Obviously, when problem happens, background task which does not have a UI, is not easy to alert user. So make sure to implement a proper logging mechanism with background task. As I want to decouple the logging mechanism from Email class, I define an Interface to indicate how logging is provided as below:

public interface ILogger {     void Write(string MessageLine) ; } 

The actual implementation goes to the class FileLogger which simply uses text file to provide logging. But as any class that implements ILogger can do the same job, you can easily extend the application to log information to other data store.

Cache Service

One of the nice features given by ASP.NET is caching and you can specify WebForm to be cached automatically by adding declarative statement in the page source. This is the simplest way to have caching in your ASP.NET application. But to explore the real power of ASP.NET caching feature, you need to do more.

I have defined a class CacheManager to provide caching in my application and implementation is as below:

public class CacheManager {     static public FileSystemInfosExtend              GetFileItems(string CurrentRootPath, bool RefreshCache)     {         // Format cache key as FileSystemInfosExtend:{CurrentRootPath}          string keyName =              string.Format("FileSystemInfosExtend:{0}",  CurrentRootPath) ;         if ( RefreshCache  HttpContext.Current.Cache[keyName] == null )         {             // Remove previous cached item              if ( HttpContext.Current.Cache[keyName] != null )                     HttpContext.Current.Cache.Remove(keyName) ;              DirectoryInfo CurrentRoot =                                new DirectoryInfo(CurrentRootPath) ;             FileSystemInfo[] files = CurrentRoot.GetFileSystemInfos() ;             FileSystemInfosExtend FileInfosEx =                                  new FileSystemInfosExtend(files) ;              // Create cache item              HttpContext.Current.Cache.Insert(                 keyName                 , FileInfosEx                 , null                 , DateTime.Now.Add(TimeSpan.FromMinutes(                     AppSetting.ApplicationCacheTimeOut))                 , Cache.NoSlidingExpiration) ;         }           return HttpContext.Current.Cache[keyName] as FileSystemInfosExtend ;     } } 

You should pay attention to the cache key because, if we want to have multiple application objects cached, we need to define some way to distinguish the objects and retrieve them later. As my application objects are lists of folder items, the logical naming convention shall be the path name of the current folder requested plus the object type which is FileSystemInfosExtend.

I have defined a special parameter RefreshCache to explicitly refresh cache. When Refresh button is pressed, cache will be refreshed by calling the Cache Manager with parameter RefreshCache set to true.

.NET framework FileSystemWatcher component gives us access to the following events:

  • Created � raised whenever a directory or file is created.
  • Deleted � raised whenever a directory or file is deleted.
  • Renamed � raised whenever the name of a directory or file is changed.
  • Changed � raised whenever changes are made to the size, system attributes, last write time, last access time, or security permissions of a directory or file.

We can make use this component to refresh the application cache and then user of the application will always have latest copies of folder lists.

File and Folder Copying

The file and folder copying function is implemented with System.IO classes. After selecting files or subfolders on the main file list screen, clicking the COPY button at the bottom of screen, you will see the main File Copying screen at FileCopy.aspx page:

Figure 10 - File(s) copy to destination directory selection

Clicking on the Subfolder link button will bring you to the subfolders of the selected folder and clicking on the selected folder itself will copy the files shown on the top list to it.

Store the Selected Items in ViewState

As I have provided paging in the list of file items screen, when user clicks the checkbox to select some of the items and switch to another page, we need to remember the items selected. Save the selected item list in session state may be the option but I see it is better to store them in VeiwState because this information attaches to this particular page only and shall not make it available to other pages by making the scope larger. This is the basic rule of thumb when considering defining scope of variables in the first lesson of programming.

A method SaveSelectedItemKey and a property SelectedKey is defined in the page code-behind class as below:

// Property to stored selected datagrid   //key (File/directory item full path) to ViewState  private ArrayList SelectedKey {     set     {        ViewState["SelectedKey"] = value ;     }     get     {         if ( ViewState["SelectedKey"] == null )             return new ArrayList() ;         else             return ViewState["SelectedKey"] as ArrayList ;     } }   // Method to store datagrid keys to page property SelectedKey  private void SaveSelectedItemKey() {     ArrayList arrayList =  this.SelectedKey  ;      foreach(DataGridItem listItem in this.DataGrid1.Items)     {        if (listItem.ItemType == ListItemType.AlternatingItem              listItem.ItemType == ListItemType.Item)        {             CheckBox selected = listItem.FindControl("Select") as CheckBox ;             string keyItem = DataGrid1.DataKeys[listItem.ItemIndex] as string ;              if (selected.Checked && !arrayList.Contains(keyItem))                 arrayList.Add(keyItem) ;              if (!selected.Checked && arrayList.Contains(keyItem) )                 arrayList.Remove(keyItem) ;         }     }      this.SelectedKey = arrayList ; } 

As you can see the CheckBox items will be checked and all the selected DataKey will be stored in the ArrayList that is to be retrieved later when Copy button is clicked to kick-off copying.

Copying the Subfolder Tree

Copying list of files is easy but not so for directory with tree of subdirectories under it. Any help? There is a Move method in System.IO.Directory class to help us to move directory tree but no Copy method is found!

So for us, the poor guys need to develop the function ourselves. Fortunately, it is not difficult with bunch of System.IO classes helping us to achieve the task. Following list of codes is how I do it in a single method:

private int CopyFile(string SourceFile, string DestinationPath) {     int FileCount = 0 ;     // Is SourceFile file or folder(directory) name?      if (File.Exists(SourceFile))     {         File.Copy(SourceFile             , DestinationPath             + Path.DirectorySeparatorChar             + Path.GetFileName(SourceFile)             , true) ;             ++FileCount ;     }     else if (Directory.Exists(SourceFile))     {         // Create the detsination sub-folder(subdirectory) first          DirectoryInfo directoryInfo  = new DirectoryInfo(SourceFile) ;         string DestinationSubDirectory = DestinationPath               + Path.DirectorySeparatorChar               +  directoryInfo.Name ;          if (!Directory.Exists(DestinationSubDirectory))                 Directory.CreateDirectory(DestinationSubDirectory) ;          // Copy all items under this folder(directory)          //  to detsination sub-folder(subdirectory)          FileSystemInfo[] fileinfos = directoryInfo.GetFileSystemInfos() ;         foreach(FileSystemInfo fileinfo in fileinfos)                 FileCount +=                     this.CopyFile(fileinfo.FullName, DestinationSubDirectory) ;      }      else        throw new System.IO.FileNotFoundException("File not found!", SourceFile) ;      return FileCount ; } 

The key point to this CopyFile method implementation is to use recursive calls to achieve copying of subdirectories. When source is not a file, the program will list all the items beneath it and create the necessary destination subdirectories before proceeding to call recursively the function.

Other Goodies

  • Separate Header and Footer in User Controls, for your convenience to do customization.
  • Use Cascading Style Sheets (CSS), also for your convenience to do customization.
  • WebUI.cs utility for injecting client JavaScript. Although I do not use a lot in this project, it is useful when you add more client-side features. Anyway, I use it in another project intensively.


There is a long way to go before we can have more features for this file management application. I hope when I have time, will add more functions like move and delete or file items, automatic Application Cache updating by using events from FileSystemWatcher and other advanced features like file versioning. Anyway, I hope this is the start and anyone can help me to make it perfect, based on my rudimentary work is welcomed.

No comments :

Post a Comment