|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis 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 FeaturesFeatures of this file management application include:
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 SetupSetup steps are as below:
DesignDefine An Abstract ClassAs each file or folder object will have certain properties in common, an abstract base class 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, 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 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 The System.IO.DirectoryInfo CurrentRoot = new DirectoryInfo(this.RootPath);
FileSystemInfo[] files ;
On the other hand, result from Indexing Service using 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 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 // 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 ItemObviously, 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 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 <ASP:HYPERLINK
Text='<%# DataBinder.Eval(Container, "DataItem.Name") %>'
navigateurl='<%# FormatLink(DataBinder.Eval(Container, "DataItem")) %>'
runat="server">
</ASP:HYPERLINK>
File Item Requested HandlerActually, the file item handling page is very simple and I am sure making it as an 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 ViewAfter all, to view the list as iconic items, we need to get the associated icons first and in page 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 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 UploadFile 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 ServiceEmail service is provided with an wrapper class Use Thread Pool to Send EmailI 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 void FunctioName(object state) ;
The method LoggingWhen 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 public interface ILogger
{
void Write(string MessageLine) ;
}
The actual implementation goes to the class Cache ServiceOne 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 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 I have defined a special parameter .NET framework
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 CopyingThe file and folder copying function is implemented with
Figure 10 - File(s) copy to destination directory selection Clicking on the Store the Selected Items in ViewStateAs I have provided paging in the list of file items screen, when user clicks the A method // 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 Copying the Subfolder TreeCopying list of files is easy but not so for directory with tree of subdirectories under it. Any help? There is a So for us, the poor guys need to develop the function ourselves. Fortunately, it is not difficult with bunch of 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 Other Goodies
ConclusionThere 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
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||