Introduction
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
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 anICollection
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.
Figure 1 - File Management Web Page in List View
Figure 2 - File Management Web Page in Iconic View
Figure 3 - Send File as attachment via SMTP server
Setup
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'sDirectories
property for performance purpose. In doing so, it will improve response time when narrowing down the searching scope for your application.Figure 4 - Indexing Service Setup
- After setup, there will be two virtual directories created under your web server and they are:
- filemanagement Web Application- it is where the main file management application resides.
- 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.
Figure 5 - Authentication Setup under IIS
- In the WEB.CONFIG file under the virtual folder of SearchingService Web Application, modify the key
IndexCatalog
underappSettings
section to match with the new catalog you have created in the previous step:Collapse<appSettings> <add key="IndexCatalog" value="system" /> </appSettings>
- In the WEB.CONFIG file under the virtual folder of filemanagement Web Application, modify the key
Root
underappSettings
section to match with the folder path you intended to serve over your web site. You can also setup your company name here:Collapse<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:
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:
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:
Figure 8 - Intranet Zone setting
- As the application uses Windows Authentication, the default setting of WEB.CONFIG for authentication is: Collapse
<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:Collapse<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:
- The parameter
HasEmailService
controls whether email service is provided. - 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 astrue
and only set tofalse
for debugging purpose when email sending has problem. 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.STMPServer
,SMTPUser
andSMTPPwd
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.FileManagementSiteLog
is the location of the log file and default to log folder under application root.
- The parameter
- 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 parameterApplicationCacheTimeOut
. Default is five minutes.
Design
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 }
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.
Logging
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.
Conclusion
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