Modifying the Attributes of Files and Directories
Earlier in the chapter we added a column to show the
attributes of the items, by reading the Attributes property. That property can be read and set, so why not add functionality to edit the
attributes? Now that we know the trick to pop up a prompt dialog and
raise server events, this is a no-brainer problem.
Let's add the new JavaScript to the <script>
section, and the respective LinkButton control, in the BrowseFiles.aspx
page:
...
<script
language="javascript">
function CreateDir()
{
... }
function CreateFile()
{
... }
function Rename(path)
{
... }
function SetAttributes(path)
{
attribs = prompt('Type the new attributes for the
file/folder.\nA=Archive, R=ReadOnly, H=Hidden, S=System:','');
if ((attribs) && (attribs!=""))
{
document.forms['BrowseFiles'].elements['funcParam'].value = path;
document.forms['BrowseFiles'].elements['funcExtraParam'].value +
= attribs;
__doPostBack('SetAttributes',
'');
}
}
</script>
</head>
<body>
...
<asp:LinkButton ID="SetAttributes" runat="server"
OnClick="SetAttributes_Click"
Visible="False" />
...
The funcParam hidden control is used to
store the path of the item to update, while funcExtraParam stores the new
attributes. Attributes are specified as a string of characters, where 'A'
means Archive, 'R' Read-only, 'H' Hidden, and 'S'
System. So, for example, "RS" means Read-only + System.
The column for the attributes is already present in the rows
for both directories and files, so the additions for the FillFoldersAndFilesTable
procedure in the code-behind are limited:
// create the required cells and controls
...
TableCell cellAttributes;
Label labelAttributes;
HyperLink linkSetAttribs;
...
foreach (DirectoryInfo childDir in
childDirs)
{
//
create the required cells and controls
...
cellAttributes = new TableCell();
labelAttributes = new Label();
linkSetAttribs = new HyperLink();
//
set the other columns and controls
...
//
set the attributes cell: description label and the icon link
labelAttributes.Text = GetAttributesDescription(childDir.Attributes);
labelAttributes.Font.Name = "Courier";
linkSetAttribs.Text = "<img src=\"./Images/Edit.gif\"
border=\"0\ +
" height=\"16\" width=\"16\" Alt=\"Edit
attributes\">";
linkSetAttribs.NavigateUrl = "javascript:SetAttributes('" +
location + "');";
cellAttributes.Controls.Add(labelAttributes);
cellAttributes.Controls.Add(linkSetAttribs);
// add
the cells to the new row and the row to the table
...
}
// now add each child file
foreach (FileInfo childFile in childFiles)
{
//
create the required cells
...
cellAttributes = new TableCell();
labelAttributes = new Label();
linkSetAttribs = new HyperLink();
//
set the other columns and controls
...
//
set the attributes cell: description label and the icon link
labelAttributes.Text = GetAttributesDescription(childFile.Attributes);
labelAttributes.Font.Name = "Courier";
linkSetAttribs.Text = "<img src=\"./Images/Edit.gif\"
border=\"0\ +
" height=\"16\" width=\"16\" Alt=\"Edit
attributes\">";
linkSetAttribs.NavigateUrl = "javascript:SetAttributes('" +
location + "');";
cellAttributes.Controls.Add(labelAttributes);
cellAttributes.Controls.Add(linkSetAttribs);
//
add the cells to the new row and the row to the table
...
}
There is nothing new to explain here, as the code is very
similar to the code we added for the Rename command. There is a difference
in the JavaScript we call and we just pass the plain item path. There is no
need to prefix a 'D' or 'F' to identify the path as a directory or a file
we'll see the reason for this in a moment. Look at the code called on the
server when the user confirms the new attributes:
protected void SetAttributes_Click(object sender,
EventArgs e)
{
string path = Server.MapPath(funcParam.Value);
string attribs = funcExtraParam.Value.ToUpper();
FileAttributes fileAttribs = FileAttributes.Normal;
// search the A (Archive) attribute in the
descriptive string
if
(attribs.IndexOf("A") > -1)
fileAttribs |= FileAttributes.Archive;
//
search the R (ReadOnly) attribute in the descriptive string
if (attribs.IndexOf("R") > -1)
fileAttribs |= FileAttributes.ReadOnly;
// search the H (Hidden) attribute in the
descriptive string
if (attribs.IndexOf("H") > -1)
fileAttribs |= FileAttributes.Hidden;
// search the S (System) attribute in the
descriptive string
if (attribs.IndexOf("S") > -1)
fileAttribs |= FileAttributes.System;
try
{
// set the new attributes. This works with
directories as well
File.SetAttributes(path, fileAttribs);
//
refresh the page
Response.Redirect("BrowseFiles.aspx?Folder=" + folderPath);
}
catch (Exception exc)
{
StatusMessage.Text = exc.Message;
StatusMessage.Visible = true;
}
}
The procedure checks if the 'A', 'R', 'H', and 'S' letters
are present in the user-specified string, and the respective attribute for each
occurrence found is added to a variable, of type FileAttributes.
As a last step the procedure sets the new attributes by calling the File's
SetAttributes
shared method. As you have probably guessed, there was no need to prefix the
path with a 'D' or 'F' letter because the SetAttributes method works fine with both directories and
files, so there is no need to differentiate our code according to the item
type.
The following screenshot shows how the file manager looks
after these additions. You can see the new icons in the new columns, and the
dialog for specifying the new attributes for a file or directory:
Deleting Files
What we want to implement now is a delete command. We could
easily add a further link to the right of each item and associate a server
procedure that deletes that item. However, so far we've implemented commands
that work on individual files: at the moment we can create one file at time, rename one
file at time, change the attributes of one
directory at time, etc. Let's say that we want to delete twenty files it
would take twenty reloads to complete the
operation, and that would be quite boring. If we take any file manager as a model, we can see that commands like
rename work on an individual item, since they are based on the current
properties (name, attributes, etc.) of the item. But if we want to delete,
copy, or move a set of files, we can select more than one file and delete them
in a single step, saving time.
We want to follow the general guidelines of any good file
manager, so we will do things that way as well. First problem: allowing the
user (administrator) to select the files or directories they want to delete.
The solution is to add a checkbox for each item, so that when the user presses
the Delete
link all the items whose checkboxes are selected are deleted.
In our application this simply translates to dynamically
adding a checkbox in the first column (before the item icon) in the FillFoldersAndFilesTable
procedure in the code-behind. In the BrowseFiles.aspx page we add
JavaScript that asks the user to confirm the operation, the link on the
toolbar, and a new invisible LinkButton for the server event
handler:
<script language="javascript">
function CreateDir()
{ ... }
// other javascript functions
...
function Delete()
{
if (confirm('Do you want to delete the
selected folder(s) ' +
'and/or file(s)?'))
__doPostBack('Delete','');
}
</script>
</head>
<body>
...
<table class="MenuTable"
border="0" width="100%">
<tr>
<td>
...
<a
href="javascript:Delete();">
<img border="0"
src="./Images/Delete.gif"
Alt="Delete the selected
files/directories"
height="28"
width="28">
</a>
<asp:LinkButton
ID="Delete" runat="server"
OnClick="Delete_Click"
Visible="False" />
...
</td>
</tr>
</table>
...
In the FillFoldersAndFilesTable procedure in
the code-behind, first of all
we add the code for creating a checkbox for each item:
// create the required cells and controls
...
TableCell cellItemIcon;
CheckBox checkItem;
...
foreach (DirectoryInfo childDir in
childDirs)
{
//
create the required cells and controls
...
cellItemIcon = new TableCell();
checkItem = new CheckBox();
//
set the other columns and controls
...
//
add the checkbox, and store path and item type (dir) as attributes
checkItem.Attributes["Path"] = location;
checkItem.Attributes["IsFile"] = "false";
cellItemIcon.Controls.Add(checkItem);
imgItemIcon.ImageUrl =
"./Images/ClosedFolder.gif";
cellItemIcon.Controls.Add(imgItemIcon);
cellItemIcon.HorizontalAlign =
HorizontalAlign.Right;
// add
the cells to the new row and the row to the table
...
}
foreach (FileInfo childFile in childFiles)
{
//
create the required cells and controls
...
cellItemIcon = new TableCell();
checkItem = new CheckBox();
//
set the other columns and controls
...
//
add the checkbox, and store path and item type (file) as attributes
checkItem.Attributes["Path"] = location;
checkItem.Attributes["IsFile"] = "true";
cellItemIcon.Controls.Add(checkItem);
cellItemIcon.Controls.Add(imgItemIcon);
cellItemIcon.HorizontalAlign = HorizontalAlign.Right;
//
add the cells to the new row and the row to the table
...
}
You will notice that the item's path and type (file or
directory) are stored as checkbox Attributes. The code in the Delete_Click
procedure will get this information from all the selected checkboxes, and will
accordingly use the Directory or File classes to delete those items.
Here's this procedure:
protected void Delete_Click(object sender, EventArgs e)
{
bool redir = true;
foreach (TableRow tr in FoldersAndFiles.Rows)
{
if (tr.Cells[0].Controls.Count==2)
{
// get a reference to the checkbox
CheckBox checkItem = (CheckBox)tr.Cells[0].Controls[0];
if (checkItem.Checked)
{
try
{
string path = Server.MapPath(
checkItem.Attributes["Path"].ToString());
// is this item a file?
if (Convert.ToBoolean(checkItem.Attributes["IsFile"])==true)
File.Delete(path);
else
Directory.Delete(path, true);
}
catch (Exception exc)
{
StatusMessage.Text = exc.Message;
StatusMessage.Visible = true;
redir = false;
}
}
}
}
// refresh the page
if (redir)
Response.Redirect("BrowseFiles.aspx?Folder=" + folderPath);
}
For each row in the table the procedure checks if the first
cell has two child controls. If there is only one, it means that this row has
only the icon and link to jump to the parent directory. Therefore it does not
represent a file system item, and so the code does nothing. If there are two
controls, the procedure gets a reference to the checkbox control, and looks to
see if the control is checked. If it is checked then it goes ahead, by
extracting the path of the item and the attribute specifying if the item is a
file or a directory, in order to use the appropriate class to delete the item.
Note that here we don't refresh the page after the operation
is performed for just one item, but only when we're finished with all the selected items. Therefore we
have to redirect just before the end of the procedure, based on the value of
the redir
variable, which is true when it is declared and set to false
if an exception is thrown.
Copying and Moving Files
We're close to completing the features of our file manager,
but what's still missing is the ability to copy or move a set of files and
folders. We already have the infrastructure for selecting multiple items what
we need to add are the JavaScript function that asks for the destination path,
a LinkButton
control, and the respective event handler. Copying and moving items are very
similar operations, thus we'll handle both of them with the same event handler and the
same JavaScript.
As usual, we start by showing the additions to the BrowseFiles.aspx
page:
<script
language="javascript">
function CreateDir()
{
... }
//
other javascript functions
...
function
CopyMove(op)
{
destPath = prompt('Type the destination virtual path:','');
if ((destPath) && (destPath!=""))
{
document.forms['BrowseFiles'].elements['funcParam'].value = destPath;
document.forms['BrowseFiles'].elements
['funcExtraParam'].value = op;
__doPostBack('CopyMove',
'');
}
}
</script>
</head>
<body>
...
<table class="MenuTable" border="0"
width="100%">
<tr>
<td>
...
<a href="javascript:CopyMove('copy');">
<img border="0"
src="./Images/Copy.gif"
Alt="Copy the selected files/directories"
height="28" width="28">
</a>
<a href="javascript:CopyMove('move');">
<img border="0" src="./Images/Move.gif"
Alt="Move the selected
files/directories"
height="28" width="28">
</a>
<asp:LinkButton ID="CopyMove" runat="server"
OnClick="CopyMove_Click"
Visible="False" />
...
</td>
</tr>
</table>
...
In the event handler we can find out if the user wants to
copy or move the file by looking at the value of the funcExtraParam
hidden control, which can be "copy" or "move".
The other control is used to pass the destination path. Here is the event
handler code:
protected void CopyMove_Click(object sender,
EventArgs e)
{
bool redir = true;
//
extract the destination directory
string folder = funcParam.Value;
//
extract the operation to perform: can be "copy" or "move"
string op = funcExtraParam.Value;
if
(!folder.StartsWith("/"))
{
if (folderPath.EndsWith("/"))
folder = folderPath + folder;
else
folder = folderPath + "/" + folder;
}
folder = Server.MapPath(folder);
foreach (TableRow tr in FoldersAndFiles.Rows)
{
if (tr.Cells[0].Controls.Count==2)
{
// go ahead only if the checkbox is selected
CheckBox checkItem = (CheckBox)tr.Cells[0].Controls[0];
if (checkItem.Checked)
{
string destPath = Path.Combine(folder,
((HyperLink)tr.Cells[1].Controls[0]).Text);
string path = Server.MapPath(
checkItem.Attributes["Path"].ToString());
try
{
if (Convert.ToBoolean(checkItem.Attributes["IsFile"])==true)
{
// copy or move this file
if (op=="move")
File.Move(path, destPath);
else
File.Copy(path, destPath, true);
}
else
{
// copy or move this directory
if(op=="move")
Directory.Move(path, destPath);
else
CopyDirectory(path, destPath, true);
}
}
catch (Exception exc)
{
StatusMessage.Text = exc.Message;
StatusMessage.Visible = true;
redir = false;
}
}
}
}
// refresh the page
if (redir)
Response.Redirect("BrowseFiles.aspx?Folder=" + folderPath);
}
The user can specify a relative or an absolute virtual
directory as the destination. If the specified path begins with "/"
it means that the path is absolute, so nothing extra needs to be done.
Otherwise it is a relative path, and it is added to the current path. Then we
get the respective physical path, which is the one we work with.
The other point to note is that both the File
and Directory
classes expose the Move method, but only the File
class exposes a Copy method. Honestly, I don't know why the Directory
class does not
have a Copy
method, but this is not a problem that can scare us we can always write our
custom CopyDirectory
function, right?
No sooner said than done. Here's the code for CopyDirectory:
private void CopyDirectory(string
sourcePath, string destPath,
bool overwrite)
{
DirectoryInfo sourceDir = new DirectoryInfo(sourcePath);
DirectoryInfo destDir = new DirectoryInfo(destPath);
//
the source directory must exist, if not raise an exception
if
(sourceDir.Exists)
{
// if destination dir does not exist throw an exception
if (!destDir.Parent.Exists)
new AppException("Destination
directory does not exist: " +
destDir.Parent.FullName, new DirectoryNotFoundException());
if (!destDir.Exists) destDir.Create();
// copy all the files of the current directory
foreach (FileInfo file in sourceDir.GetFiles())
{
if (overwrite)
file.CopyTo(Path.Combine(destDir.FullName, file.Name), true);
else
{
// if overwrite = false, copy the file only if it
// does not exist. This is done to avoid an IOException
// if a file already exists. This way the other files
// can be copied anyway...
if (!File.Exists(Path.Combine(destDir.FullName, file.Name)))
file.CopyTo(Path.Combine(destDir.FullName, file.Name), false);
}
}
// recursively call this procedure to copy the subdirectories
foreach (DirectoryInfo dir in sourceDir.GetDirectories())
CopyDirectory(dir.FullName, Path.Combine(destDir.FullName,
dir.Name), overwrite);
}
else
new AppException("Source directory does not exist: " +
sourceDir.FullName, new DirectoryNotFoundException());
}
The procedure accepts the source and destination directories
as input, plus a third parameter that specifies whether the files with the same
name in the destination directory will be overwritten. The procedure first
copies all the child files, and then uses recursion for each subdirectory. If
the destination directory does not exist, an exception is thrown through the AppException
class, which we built in Chapter 2. In order to use this class we have to add a
reference to the Core.dll assembly, or the Core
project if it is part of the solution. To add a reference, click the References
command under the Project menu, and select from the list.
Our file manager is finally feature-complete! In the
following screenshot you can see the final version, with the checkboxes for all
items, the new command links in the toolbar, and the dialog asking for the
destination directory for a copy operation: