Search   Articles   Dev Forums   Personalize   Favorites   Member Login   ASP.Net Hosting
DevASP.NET for ASP.NET, VB.NET, XML and C# (C-Sharp) Developers Sunday, November 23, 2008

Dev Articles
Search Directory
ASP.NET
VB.Net
C-Sharp
SQL Server
 

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:

 


 

 


DevASP.Net - Disclaimer - Privacy
Copyright © 2008 DevASP.net