The previous chapters have used console applications to demonstrate C# and the CLR. Although console applications can be implemented simply, it is time to turn your attention to the reason you’re learning the C# language in the first place: building Windows and web applications.
In the early days of Windows computing, an application ran on a desktop, in splendid isolation. Over time, developers found it beneficial to spread their applications across a network, with the user interface on one computer and a database on another. This division of responsibilities, or partitioning of an application, came to be called two-tier or client-server application development. Later, three-tier or n-tier approaches emerged as developers began to use web servers to host business objects that could handle the database access on behalf of clients.
When the Web first came along, there was a clear distinction between Windows applications and web applications. Windows applications ran on the desktop or a local area network (LAN), and web applications ran on a distant server and were accessed by a browser. This distinction is now being blurred somewhat as Windows applications reach out to the Web for services. Many new applications consist of logic running on a client, a database server, and remote third-party computers located on the Web. Traditional desktop applications such as Excel or Outlook are now able to integrate data retrieved through web connections seamlessly, and web applications can distribute some of their processing to client-side components.
The primary remaining distinction between a Windows application and a web application might be this: who owns the user interface. Will your application use a browser to display its user interface, or will the UI be built into the executable running on the desktop?
Even the distinction of “who owns the user interface” is somewhat arbitrary, as browser-based interfaces can have components running locally and desktop-based applications can have embedded web browsers!
There are enormous advantages to web applications, starting with the obvious: they can be accessed from any browser that can connect to the server. In addition, updates can be made at the server, without the need to distribute new DLLs to your customers.
On the other hand, if your application derives no benefit from being on the Web, you might find that you can achieve greater control over the look and feel of your application or that you can achieve better performance by building a desktop application.
.NET offers closely related, but distinguishable, suites of tools for building Windows or web applications. Both are based on the premise that many applications have user interfaces centered on interacting with the user through forms and controls, such as buttons, listboxes, text, and so forth.
The tools for creating web applications are called Web Forms and are considered in Chapter 15. The tools for creating Windows applications are called Windows Forms and are the subject of this chapter.
On the following pages, you will learn how to create Windows applications using the tools provided by Visual Studio. This application will bring together a number of C# techniques taught in earlier chapters.
A Windows Form is a tool for building a Windows application. The .NET Framework offers extensive support for Windows application development, the centerpiece of which is the Windows Forms framework. Not surprisingly, Windows Forms use the metaphor of a form. This idea was borrowed from the wildly successful VB environment and supports RAD. Arguably, C# is the first development environment to marry the RAD tools of VB with the object-oriented and high-performance characteristics of a C-family language.
While it is possible to build a Windows application using any editor (even Notepad!) and compiling from the command line, it is senseless to do so, when Visual Studio.NET makes life so much easier.
To begin work on a new Windows application, first open Visual Studio and choose File→ New→ Project. In the New Project window, create a new C# Windows application and name it ProgCSharpWindowsForm, as shown in Figure 13-1.
Visual Studio responds by creating a Windows Form application and, best of all, putting you into a design environment, as shown in Figure 13-2.
The Design window displays a blank Windows Form
(Form1
). A Toolbox window is also available, with
a selection of Windows widgets and controls. If the Toolbox is not
displayed, try clicking the word
"Toolbox,”
or selecting View→ Toolbox
on the Visual Studio menu. You can also use the keyboard shortcut
Ctrl-Alt-X to display the Toolbox.[1]
With the Toolbox displayed, you can drag a label and a button directly onto the form, as shown in Figure 13-3.
Before proceeding, take a look around. The Toolbox is filled with
controls that you can add to your Windows Form application. In the
upper-right corner, you should see the Solution Explorer, a window
that displays all the files in your projects. In the lower-right
corner is the Properties window, which displays all the properties of
the currently selected item. In Figure 13-3, the
label (label1
) is selected, and the Properties
window displays its properties.
You can use the Properties window to set the properties
of the various controls. For example, to add text to
label1
, you can type the words
“Hello World” into the box to the
right of its Text
property. If you want to change
the font for the lettering in the HelloWorld
label, click the Font
property shown in the
lower-right corner of Figure 13-4. (You can provide
text in the same way for your
button—button1
—by selecting it in the
Properties window and typing the word
“Cancel” into its
Text
property.)
Any one of these steps is often easier than modifying these properties in code (though that is certainly possible).
Once you have the form laid out the way you want, all that remains is to create the click handler for the Cancel button. Double-clicking the Cancel button will create the event handler, register it, and put you on the code page (the page that holds the source code for this form), in which you can enter the event-handling logic, as shown in Figure 13-5.
The cursor is already in place; you have only to enter one line of code:
Application.Exit();
You’ll find that as you try to enter this code,
Intellisense tries to help you. When you type A, the first possible
object that begins with A is shown. Continue typing through Appl and
then hit the period: the class Application
is
filled in for you,[2] and the methods and
properties of the Application
object are
available. Type Ex and then type the semicolon. Hey! Presto! Your
line of code is written.
In the IDE, the cursor flashes, making it easy to see where the code goes. For most readers, the cursor probably won’t flash in this book.
Visual Studio generates all code necessary to create and initialize the components.
Note that your code file (Form1.cs) has only the
using
directives and the constructor and event
handler. Those of you who programmed in previous versions of C# may
be wondering where the rest of the code is to initialize and set the
properties of your controls (which aren’t even
listed here!). Note, however, that the class definition contains the
keyword partial
. This indicates that the rest of
the class definition is contained in another file. If you click the
Show All Files button (circled in Figure 13-6), you
will see that the designer has added another file,
Form1.Designer.cs.
The file Form1.Designer.cs has all the code generated by Visual Studio.
To see how
Windows Forms can be used to create a
more realistic Windows application, in this section
you’ll build a utility named
FileCopier
that copies all files from a group of
directories selected by the user to a single target directory or
device, such as a floppy or backup hard drive on the company network.
Although you won’t implement every possible feature,
you can imagine programming this application so that you can mark
dozens of files and have them copied to multiple disks, packing them
as tightly as possible. You might even extend the application to
compress the files. The true goal of this example is for you to
exercise many of the C# skills learned in earlier chapters and to
explore the
Windows.Forms
namespace.
For the purposes of this example and to keep the code simple, focus on the user interface and the steps needed to wire up its various controls. The final application UI is shown in Figure 13-7.
The user interface for FileCopier
consists of the
following controls:
Labels (Source Files and Target Files)
Buttons (Clear, Copy, Delete, and Cancel)
An “Overwrite if exists” checkbox
A text box displaying the path of the selected target directory
Two large tree-view controls, one for available source directories and one for available target devices and directories
The goal is to allow the user to check files (or entire directories) in the left tree view (source). If the user clicks the Copy button, the files checked on the left side will be copied to the Target Files specified in the right-side control. If the user clicks Delete, the checked files will be deleted.
The rest of this chapter implements a number of
FileCopier
features to demonstrate the fundamental
features of Windows Forms.
The first task is to open a new
project named FileCopier. The IDE puts you into the Designer, in
which you can drag widgets onto the form. You can expand the form to
the size you want. Drag, drop, and set the Name
properties of labels (lblSource
,
lblTarget
, lblStatus
), buttons
(btnClear
, btnCopy
,
btnDelete
, btnCancel
), a
checkbox (chkOverwrite
), a text box
(txtTargetDir
), and tree-view controls
(tvwSource
, tvwTargetDir
) from
the Toolbox onto your form until it looks more or less like the one
shown in Figure 13-8.
You want checkboxes next to the directories and files in the source
selection window but not in the target (where only one directory will
be chosen). Set the CheckBoxes
property on the
left TreeView
control,
tvwSource
, to true
, and set the
property on the right TreeView
control,
tvwTargetDir
, to false
. To do
so, click each control in turn and adjust the values in the
Properties window.
Once this is done, double-click the Cancel button to create its event
handler; when you double-click a control, Visual Studio creates an
event handler for that object. Each object has a
“default” event that Visual Studio
will use if you double-click the object. For buttons, the default
event is Click
.
protected void btnCancel_Click (object sender, System.EventArgs e) { Application.Exit(); }
You can handle many different events for the various controls. An easy way to do so is by clicking the Events button in the Properties window. From there you can create new handlers, just by filling in a new event-handler method name or picking one of the existing event handlers. Visual Studio registers the event handler and opens the editor for the code, where it creates the header and puts the cursor in an empty method body.
So much for the easy part. Visual Studio generates code to set up the
form and initializes all the controls, but it
doesn’t fill the TreeView
controls. That you must do by hand.
The two
TreeView
controls work identically, except that the left control,
tvwSource
, lists the directories and files,
whereas the right control, tvwTargetDir
, lists
only directories. The CheckBoxes
property on
tvwSource
is set to true
, and
on tvwTargetDir
it is set to
false
. Also, although tvwSource
will allow multiselect, which is the default for
TreeView
controls, you will enforce single
selection for tvwTargetDir
.
You’ll factor the common code for both
TreeView
controls into a shared method
FillDirectoryTree
and pass in the control with a flag
indicating whether to get the files. You’ll call
this method from the Form’s constructor, once for
each of the two controls:
FillDirectoryTree(tvwSource, true); FillDirectoryTree(tvwTargetDir, false);
The FillDirectoryTree
implementation names the
TreeView
parameter tvw
. This
will represent the source TreeView
and the
destination TreeView
in turn.
You’ll need some classes from
System.IO
, so add a using
System.IO;
statement at the top of
Form1.cs
. Next, add the method declaration to
Form1.cs
:
private void FillDirectoryTree(TreeView tvw, bool isSource)
The TreeView
control has a property,
Nodes
, which gets a
TreeNodeCollection
object. The
TreeNodeCollection
is a collection of
TreeNode
objects, each of which represents a node
in the tree. Start by emptying that collection:
tvw.Nodes.Clear();
You are ready to fill the
TreeView
’s
Nodes
collection by recursing through the
directories of all the drives. First, get all the logical drives on
the system. To do so, call a static method of the
Environment
object, GetLogicalDrives( )
. The
Environment
class provides information about and
access to the current platform environment. You can use the
Environment
object to get the machine name, OS
version, system directory, and so forth, from the computer on which
you are running your program.
string[] strDrives = Environment.GetLogicalDrives();
GetLogicalDrives()
returns an array of strings,
each of which represents the root directory of one of the logical
drives. You will iterate over that collection, adding nodes to the
TreeView
control as you go.
foreach (string rootDirectoryName in strDrives) {
You process each drive within the foreach
loop.
The very first thing you need to determine is whether the drive is
ready. My hack for that is to get the list of top-level directories
from the drive by calling
GetDirectories()
on a
DirectoryInfo
object I created for the root
directory:
DirectoryInfo dir = new DirectoryInfo(rootDirectoryName); dir.GetDirectories();
The
DirectoryInfo
class exposes
instance methods for creating, moving, and enumerating through
directories, their files, and their subdirectories. The
DirectoryInfo
class is covered in detail in Chapter 21.
The GetDirectories()
method returns a list of
directories, but actually, this code throws the list away. You are
calling it here only to generate an exception if the drive is not
ready.
Wrap the call in a try
block and take no action in
the catch
block. The effect is that if an
exception is thrown, the drive is skipped.
Once you know that the drive is ready, create a
TreeNode
to hold the root directory of the drive
and add that node to the TreeView
control:
TreeNode ndRoot = new TreeNode(rootDirectoryName); tvw.Nodes.Add(ndRoot);
To get the + signs right in the TreeView
, you must
find at least two levels of directories (so that the
TreeView
knows which directories have
subdirectories and can write the + sign next to them). You
don’t want to recurse through all the
subdirectories, however, because that would be too slow.
The job of the
GetSubDirectoryNodes()
method is to recurse two levels deep,
passing in the root node, the name of the root directory, a flag
indicating whether you want files, and the current level (you always
start at level 1):
if ( isSource ) { GetSubDirectoryNodes( ndRoot, ndRoot.Text, true,1 ); } else { GetSubDirectoryNodes( ndRoot, ndRoot.Text, false,1 ); }
You are probably wondering why you need to pass in
ndRoot.Text
if you’re already
passing in ndRoot
. Patience—you will see why
this is needed when you recurse back into
GetSubDirectoryNodes
. You are now finished with
FillDirectoryTree()
. See Example 13-1
later in this chapter for a complete listing of this method.
GetSubDirectoryNodes( )
begins by once again calling GetDirectories()
,
this time stashing away the resulting array of
DirectoryInfo
objects:
private void GetSubDireoctoryNodes( TreeNode parentNode, string fullName, bool getFileNames) { DirectoryInfo dir = new DirectoryInfo(fullName); DirectoryInfo[] dirSubs = dir.GetDirectories( );
Notice that the node passed in is named
parentNode
. The current level of nodes will be
considered children to the node passed in. This is how you map the
directory structure to the hierarchy of the tree view.
Iterate over each subdirectory, skipping any that are marked
Hidden
:
foreach (DirectoryInfo dirSub in dirSubs) { if ( (dirSub.Attributes & FileAttributes.Hidden) != 0 ) { continue; }
FileAttributes
is an enum; other possible values include
Archive, Compressed, Directory, Encrypted, Hidden, Normal, ReadOnly,
etc.
The property
dirSub.Attributes
is the bit pattern of the current
attributes of the directory. If you logically AND
that value with the bit pattern
FileAttributes.Hidden
, a bit is set if the file
has the hidden
attribute; otherwise all the bits
are cleared. You can check for any hidden bit by testing whether the
resulting int
is something other than 0.
Create a TreeNode
with the directory name and add
it to the Nodes
collection of the node passed in
to the method (parentNode
):
TreeNode subNode = new TreeNode(dirSub.Name); parentNode.Nodes.Add(subNode);
Now you check the current level (passed in by the calling method) against a constant defined for the class:
private const int MaxLevel = 2;
so as to recurse only two levels deep:
if ( level < MaxLevel ) { GetSubDirectoryNodes( subNode, dirSub.FullName, getFileNames, level+1 ); }
You pass in the node you just created as the new parent, the full path as the full name of the parent, and the flag you received, along with one greater than the current level (thus, if you started at level one, this next call will set the level to two).
The call to the TreeNode
constructor uses the
Name
property of the DirectoryInfo
object, while the
call to
GetSubDirectoryNodes( )
uses the FullName
property. If your directory is
C:WindowsMediaSounds, the
FullName
property returns the full path, while the
Name
property returns just
Sounds . Pass in only
the name to the node because that is what you want displayed in the
tree view. Pass in the full name with the path to the
GetSubDirectoryNodes( )
method so that the method
can locate all the subdirectories on the disk. This answers the
question asked earlier as to why you need to pass in the root
node’s name the first time you call this method.
What is passed in isn’t the name of the node; it is
the full path to the directory represented by the node!
Once you’ve recursed
through the subdirectories, it is time to get the files for the
directory if the getFileNames
flag is
true
. To do so, call the
GetFiles( )
method on the
DirectoryInfo
object. An array of
FileInfo
objects is returned:
if (getFileNames) { // Get any files for this node. FileInfo[] files = dir.GetFiles( );
The FileInfo
class (covered in Chapter 21) provides instance methods for manipulating
files.
You can now iterate over this collection, accessing the
Name
property of the FileInfo
object and passing that
name to the constructor of a TreeNode
, which you
then add to the parent node’s
Nodes
collection (thus creating a child node).
There is no recursion this time because files don’t
have subdirectories:
foreach (FileInfo file in files) { TreeNode fileNode = new TreeNode(file.Name); parentNode.Nodes.Add(fileNode); }
That’s all it takes to fill the two tree views. See Example 13-1 for a complete listing of this method.
You
must handle a number of events in this
example. First, the user might click Cancel, Copy, Clear, or Delete.
Second, the user might click one of the checkboxes in the left
TreeView
, one of the nodes in the right
TreeView
, or one of the + signs in either view.
Let’s consider the clicks on the
TreeViews
first, as they are the more interesting,
and potentially the more challenging.
There are two TreeView
objects, each with its own
event handler. Consider the source TreeView
object
first. The user checks the files and directories he wants to copy
from. Each time the user clicks the checkbox indicating a file or
directory, a number of events are raised. The event you must handle
is AfterCheck
.
To do so, implement a custom event-handler method you will create and
name tvwSource_AfterCheck()
. Visual Studio will
wire this to the event handler, or if you aren’t
using the IDE, you must do so yourself.
tvwSource.AfterCheck += new System.Windows.Forms.TreeViewEventHandler (this.tvwSource_AfterCheck);
The implementation of AfterCheck( )
delegates the
work to a recursable method named SetCheck()
that
you’ll also write. The SetCheck
method will recursively set the check mark for all the contained
folders.
To add the AfterCheck
event, select the
tvwSource
control, click the Events icon in the
Properties window, then double-click AfterCheck. This will add the
event, wire it up, and place you in the code editor where you can add
the body of the method:
private void tvwSource_AfterCheck ( object sender, System.Windows.Forms.TreeViewEventArgs e) { SetCheck(e.Node,e.Node.Checked); }
The event handler passes in the sender
object and
an object of type TreeViewEventArgs
. It turns out
that you can get the node from this
TreeViewEventArgs
object (e
).
Call SetCheck()
, passing in the node and the state
of whether the node has been checked.
Each node
has a Nodes
property,
which gets a TreeNodeCollection
containing all the
subnodes. SetCheck()
recurses through the current
node’s Nodes
collection, setting
each subnode’s check mark to match that of the node
that was checked. In other words, when you check a directory, all its
files and subdirectories are checked, recursively, all the way down.
For each TreeNode
in the Nodes
collection, check to see if it is a leaf. A node is a leaf if its own
Nodes
collection has a count of 0. If it is a
leaf, set its check
property to whatever was
passed in as a parameter. If it isn’t a leaf,
recurse:
private void SetCheck(TreeNode node, bool check) { // find all the child nodes from this node foreach (TreeNode n in node.Nodes) { n.Checked = check; // check the node // if this is a node in the tree, recurse if (n.Nodes.Count != 0) { SetCheck(n,check); } } }
This propagates the check mark (or clears the check mark) down through the entire structure. In this way, the user can indicate that he wants to select all the files in all the subdirectories by clicking a single directory.
Each time you click a + sign next to a directory in the source (or in
the target), you want to
expand
that directory. To do so, you’ll need an event
handler for the BeforeExpand
event. Since the
event handlers will be identical for both the source and the target
tree views, you’ll create a shared event handler
(assigning the same event handler to both):
private void tvwExpand(object sender,TreeViewCancelEventArgs e) { TreeView tvw = ( TreeView ) sender; bool getFiles = tvw == tvwSource; TreeNode currentNode = e.Node; string fullName = currentNode.FullPath; currentNode.Nodes.Clear( ); GetSubDirectoryNodes( currentNode, fullName, getFiles, 1 ); }
The first line of this code casts the object passed in by the
delegate from object
to
TreeView
, which is safe since you know that only a
TreeView
can trigger this event.
Your second task is to determine whether you want to get the files in
the directory you are opening, and you do only if the name of the
TreeView
that triggered the event is
tvwSource
.
You determine which node’s + sign was checked by
getting the Node
property from the
TreeViewCancelEventArgs
that is passed in by the
event:
TreeNode currentNode = e.Node;
Once you have the current node you get its full pathname (which you
will need as a parameter to GetSubDirectoryNodes
)
and then you must clear its collection of subnodes, because you are
going to refill that collection by calling in to
GetSubDirectoryNodes
:
currentNode.Nodes.Clear();
Why do you clear the subnodes and then refill them? Because this time you will go another level deep so that the subnodes know if they in turn have subnodes, and thus will know if they should draw a + sign next to their subdirectories.
The second event handler for the target
TreeView
(in addition to
BeforeExpand
) is somewhat trickier. The event
itself is AfterSelect
. (Remember that the target
TreeView
doesn’t have
checkboxes.) This time, you want to take the one directory chosen and
put its full path into the text box at the upper-left corner of the
form.
To do so, you must work your way up through the nodes, finding the name of each parent directory and building the full path:
private void tvwTargetDir_AfterSelect ( object sender, System.Windows.Forms.TreeViewEventArgs e) { string theFullPath = GetParentString(e.Node);
We’ll look at GetParentString( )
in just a moment. Once you have the full path, you must lop off the
backslash (if any) on the end, and then you can fill the text box:
if (theFullPath.EndsWith("\")) { theFullPath = theFullPath.Substring(0,theFullPath.Length-1); } txtTargetDir.Text = theFullPath;
The
GetParentString( )
method takes a node and returns a
string with the full path. To do so, it recurses upward through the
path, adding the backslash after any node that is not a leaf:
private string GetParentString(TreeNode node) { if(node.Parent == null) { return node.Text; } else { return GetParentString(node.Parent) + node.Text + (node.Nodes.Count == 0 ? "" : "\"); } }
The
conditional operator
(?
) is the only ternary operator in C# (a ternary
operator takes three terms). The logic is “test
whether node.Nodes.Count
is 0; if so, return the
value before the colon (in this case, an empty string). Otherwise
return the value after the colon (in this case, a
backslash).”
The recursion stops when there is no parent; that is, when you hit the root directory.
Given the SetCheck( )
method developed earlier, handling the
Clear button’s
Click
event is trivial:
protected void btnClear_Click (object sender, System.EventArgs e) { foreach (TreeNode node in tvwSource.Nodes) { SetCheck(node, false); } }
Just call the SetCheck( )
method on the root nodes
and tell them to recursively uncheck all their contained
nodes.
Now
that you can check the files and pick
the target directory, you’re ready to handle the
Copy
button-click event. The very first thing you
need to do is to get a list of which files were selected. What you
want is an array of FileInfo
objects, but you have
no idea how many objects will be in the list. This is a perfect job
for ArrayList
. Delegate responsibility for filling
the list to a method called
GetFileList( )
:
private void btnCopy_Click ( object sender, System.EventArgs e) {List<FileInfo> fileList = GetFileList( );
Let’s pick that method apart before returning to the event handler.
Start
by instantiating a new
List
object to hold the strings representing the
names of all the files selected:
privateList<FileInfo> GetFileList() { // create an unsorted array list of the full file names List<string> fileNames = new List<string>( );
To get the selected filenames, you can walk through the source
TreeView
control:
foreach (TreeNode theNode in tvwSource.Nodes) { GetCheckedFiles(theNode, fileNames); }
To see how this works, step into the
GetCheckedFiles( )
method. This method is pretty simple:
it examines the node it was handed. If that node has no children
(node.Nodes.Count
==
0
), it is a leaf. If that leaf is checked, get the
full path (by calling
GetParentString( )
on the node) and add it to the
ArrayList
passed in as a parameter:
private void GetCheckedFiles(TreeNode node, List<string> fileNames ) { // if this is a leaf... if ( node.Nodes.Count == 0 ) { // if the node was checked... if ( node.Checked ) { // get the full path and add it to the arrayList string fullPath = GetParentString( node ); fileNames.Add( fullPath ); } }
If the node is not a leaf, recurse down the tree, finding the child nodes:
else { foreach (TreeNode n in node.Nodes) { GetCheckedFiles(n,fileNames); } } }
This returns the List
filled with all the
filenames. Back in GetFileList()
, use this
List
of filenames to create a second
List
, this time to hold the actual
FileInfo
objects:
List<FileInfo> fileList = new List<FileInfo>();
Notice the use of type-safe List
objects to ensure
that the compiler flags any objects added to the collection that
aren’t of type FileInfo
.
You can now iterate through the filenames in
fileList
, picking out each name and instantiating
a FileInfo
object with it. You can detect if it is
a file or a directory by calling the Exists
property, which will return false
if the
File
object you created is actually a directory.
If it is a File
, you can add it to the new
ArrayList
:
foreach (string fileName in fileNames) { FileInfo file = new FileInfo(fileName); if (file.Exists) { fileList.Add(file); } }
You
want
to work your way through the list of
selected files in large to small order so that you can pack the
target disk as tightly as possible. You must therefore sort the
ArrayList
. You can call its
Sort()
method, but how will it know how to sort
FileInfo
objects?
To solve this, you must pass in an
IComparer<T>
interface. We’ll create a class called
FileComparer
that will implement this generic
interface for FileInfo
objects:
public class FileComparer : IComparer<FileInfo> {
This class has only one method, Compare( )
, which
takes two FileInfo
objects as arguments:
public int Compare(FileInfo file1, FileInfo file2){
The normal approach is to return 1
if the first
object (file1
) is larger than the second
(file2
), to return -1
if the
opposite is true, and to return 0
if they are
equal. In this case, however, you want the list sorted from big to
small, so you should reverse the return values.
Because this is the only use of the compare
method, it is reasonable to put this special knowledge that the sort
is from big to small right into the compare
method
itself. The alternative is to sort small to big, and have the
calling method reverse the results, as you saw
in Example 12-1.
To test the length of the FileInfo
object, you
must cast the Object
parameters to
FileInfo
objects (which is safe because you know
this method will never receive anything else):
if (file1.Length > file2.Length) { return -1; } if (file1.Length < file2.Length) { return 1; } return 0; } }
Returning to GetFileList( )
, you were about to
instantiate the IComparer
reference and pass it to
the Sort( )
method of fileList
:
IComparer<FileInfo> comparer = ( IComparer<FileInfo> ) new FileComparer(); fileList.Sort(comparer);
That done, you can return fileList
to the calling
method:
return fileList;
The calling method was btnCopy_Click
. Remember,
you went off to GetFileList( )
in the first line of
the event handler!
protected void btnCopy_Click (object sender, System.EventArgs e) {List<FileInfo> fileList = GetFileList();
At this point, you’ve returned with a sorted list of
File
objects, each representing a file selected in
the source TreeView
.
You can now iterate through the list, copying the files and updating the UI:
foreach (FileInfo file in fileList) { try { lblStatus.Text = "Copying " + txtTargetDir.Text + "\" + file.Name + "..."; Application.DoEvents( ); file.CopyTo(txtTargetDir.Text + "\" + file.Name,chkOverwrite.Checked); } catch (Exception ex) { MessageBox.Show(ex.Message); } } lblStatus.Text = "Done.";
As you go, write the progress to the lblStatus
label and call
Application.DoEvents( )
to give the UI an opportunity to
redraw. Then call CopyTo( )
on the file, passing in
the target directory obtained from the text field, and a Boolean flag
indicating whether the file should be overwritten if it already
exists.
You’ll notice that the flag you pass in is the value
of the chkOverWrite
checkbox. The
Checked
property evaluates true
if the checkbox is checked and false
if not.
The copy is wrapped in a try
block because you can
anticipate any number of things going wrong when copying files. For
now, handle all exceptions by popping up a dialog box with the error;
you might want to take corrective action in a commercial application.
That’s it; you’ve implemented file copying!
The code to
handle the Delete
event is even simpler. The very
first thing you do is ask the user if she is sure she wants to delete
the files:
protected void btnDelete_Click (object sender, System.EventArgs e) { System.Windows.Forms.DialogResult result = MessageBox.Show( "Are you quite sure?", // msg "Delete Files", // caption MessageBoxButtons.OKCancel, // buttons MessageBoxIcon.Exclamation, // icons MessageBoxDefaultButton.Button2); // default button
You can use the
MessageBox
static
Show()
method, passing in the message you want to
display, the title "Delete
Files
" as a string, and flags, as follows:
MessageBox.OKCancel
asks for two buttons:
OK
and Cancel
.
MessageBox.IconExclamation
indicates that you want
to display an exclamation mark icon.
MessageBox.DefaultButton.Button2
sets the second
button (Cancel
) as the default choice.
When the user chooses OK or Cancel, the result is passed back as a
System.Windows.Forms.DialogResult
enumerated
value. You can test this value to see if the user selected OK:
if (result == System.Windows.Forms.DialogResult.OK) {
If so, you can get the list of fileNames
and
iterate through it, deleting each as you go:
ArrayList fileNames = GetFileList(); foreach (FileInfo file in fileNames) { try { lblStatus.Text = "Deleting " + txtTargetDir.Text + "\" + file.Name + "..."; Application.DoEvents( ); file.Delete( ); } catch (Exception ex) { MessageBox.Show(ex.Message); } } lblStatus.Text = "Done."; Application.DoEvents( );
This code is identical to the copy code, except that the method that
is called on the file is Delete( )
.
Example 13-1 provides the commented source code for this example.
Example 13-1. FileCopier source code
#region Using directives using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Windows.Forms; #endregion /// <remarks> /// File Copier - Windows Forms demonstration program /// (c) Copyright 2005 Liberty Associates, Inc. /// </remarks> namespace FileCopier { /// <summary> /// Form demonstrating Windows Forms implementation /// </summary> partial classfrmFileCopier : Form { private const int MaxLevel = 2; public frmFileCopier( ) { InitializeComponent( ); FillDirectoryTree( tvwSource, true ); FillDirectoryTree( tvwTarget, false ); } /// <summary> /// nested class which knows how to compare /// two files we want to sort large to small, /// so reverse the normal return values. /// </summary> public class FileComparer : IComparer<FileInfo> { public int Compare(FileInfo file1, FileInfo file2) { if ( file1.Length > file2.Length ) { return -1; } if ( file1.Length < file2.Length ) { return 1; } return 0; } public bool Equals(FileInfo x, FileInfo y) { throw new NotImplementedException( ); } public int GetHashCode(FileInfo x) { throw new NotImplementedException( ); } } private void FillDirectoryTree( TreeView tvw, bool isSource ) { // Populate tvwSource, the Source TreeView, // with the contents of // the local hard drive. // First clear all the nodes. tvw.Nodes.Clear( ); // Get the logical drives and put them into the // root nodes. Fill an array with all the // logical drives on the machine. string[] strDrives = Environment.GetLogicalDrives( ); // Iterate through the drives, adding them to the tree. // Use a try/catch block, so if a drive is not ready, // e.g. an empty floppy or CD, // it will not be added to the tree. foreach ( string rootDirectoryName in strDrives ) { try { // Fill an array with all the first level // subdirectories. If the drive is // not ready, this will throw an exception. DirectoryInfo dir = new DirectoryInfo( rootDirectoryName ); dir.GetDirectories( ); // force exception if drive not ready TreeNode ndRoot = new TreeNode( rootDirectoryName ); // Add a node for each root directory. tvw.Nodes.Add( ndRoot ); // Add subdirectory nodes. // If Treeview is the source, // then also get the filenames. if ( isSource ) { GetSubDirectoryNodes( ndRoot, ndRoot.Text, true,1 ); } else { GetSubDirectoryNodes( ndRoot, ndRoot.Text, false,1 ); } } // Catch any errors such as // Drive not ready. catch { } Application.DoEvents( ); } } // close for FillSourceDirectoryTree /// <summary> /// Gets all the subdirectories below the /// passed in directory node. /// Adds to the directory tree. /// The parameters passed in are the parent node /// for this subdirectory, /// the full path name of this subdirectory, /// and a Boolean to indicate /// whether or not to get the files in the subdirectory. /// </summary> private void GetSubDirectoryNodes( TreeNode parentNode, string fullName, bool getFileNames, int level ) { DirectoryInfo dir = new DirectoryInfo( fullName ); DirectoryInfo[] dirSubs = dir.GetDirectories( ); // Add a child node for each subdirectory. foreach ( DirectoryInfo dirSub in dirSubs ) { // do not show hidden folders if ( ( dirSub.Attributes & FileAttributes.Hidden ) != 0 ) { continue; } /// <summary> /// Each directory contains the full path. /// We need to split it on the backslashes, /// and only use /// the last node in the tree. /// Need to double the backslash since it /// is normally /// an escape character /// </summary> TreeNode subNode = new TreeNode( dirSub.Name ); parentNode.Nodes.Add( subNode ); // Call GetSubDirectoryNodes recursively. if ( level < MaxLevel ) { GetSubDirectoryNodes( subNode, dirSub.FullName, getFileNames, level+1 ); } } if ( getFileNames ) { // Get any files for this node. FileInfo[] files = dir.GetFiles( ); // After placing the nodes, // now place the files in that subdirectory. foreach ( FileInfo file in files ) { TreeNode fileNode = new TreeNode( file.Name ); parentNode.Nodes.Add( fileNode ); } } } /// <summary> /// Create an ordered list of all /// the selected files, copy to the /// target directory /// </summary> private void btnCopy_Click( object sender, System.EventArgs e ) { // get the list List<FileInfo> fileList = GetFileList( ); // copy the files foreach ( FileInfo file in fileList ) { try { // update the label to show progress lblStatus.Text = "Copying " + txtTargetDir.Text + "\" + file.Name + "..."; Application.DoEvents( ); // copy the file to its destination location file.CopyTo( txtTargetDir.Text + "\" + file.Name, chkOverwrite.Checked ); } catch ( Exception ex ) { // you may want to do more than // just show the message MessageBox.Show( ex.Message ); } } lblStatus.Text = "Done."; Application.DoEvents( ); } /// <summary> /// Tell the root of each tree to uncheck /// all the nodes below /// </summary> private void btnClear_Click( object sender, System.EventArgs e ) { // get the top most node for each drive // and tell it to clear recursively foreach ( TreeNode node in tvwSource.Nodes ) { SetCheck( node, false ); } } /// <summary> /// on cancel, exit /// </summary> private void btnCancel_Click(object sender, EventArgs e) { Application.Exit( ); } /// <summary> /// Given a node and an array list /// fill the list with the names of /// all the checked files /// </summary> // Fill the ArrayList with the full paths of // all the files checked private void GetCheckedFiles( TreeNode node, List<string> fileNames ) { // if this is a leaf... if ( node.Nodes.Count == 0 ) { // if the node was checked... if ( node.Checked ) { // get the full path and add it to the arrayList string fullPath = GetParentString( node ); fileNames.Add( fullPath ); } } else // if this node is not a leaf { // if this node is not a leaf foreach ( TreeNode n in node.Nodes ) { GetCheckedFiles( n, fileNames ); } } } /// <summary> /// Given a node, return the /// full path name /// </summary> private string GetParentString( TreeNode node ) { // if this is the root node (c:) return the text if ( node.Parent == null ) { return node.Text; } else { // recurse up and get the path then // add this node and a slash // if this node is the leaf, don't add the slash return GetParentString( node.Parent ) + node.Text + ( node.Nodes.Count == 0 ? "" : "\" ); } } /// <summary> /// shared by delete and copy /// creates an ordered list of all /// the selected files /// </summary> private List<FileInfo> GetFileList( ) { // create an unsorted array list of the full file names List<string> fileNames = new List<string>( ); // ArrayList fileNames = new ArrayList( ); // fill the fileNames ArrayList with the // full path of each file to copy foreach ( TreeNode theNode in tvwSource.Nodes ) { GetCheckedFiles( theNode, fileNames ); } // Create a list to hold the FileInfo objects List<FileInfo> fileList = new List<FileInfo>( ); // ArrayList fileList = new ArrayList( ); // for each of the file names we have in our unsorted list // if the name corresponds to a file (and not a directory) // add it to the file list foreach ( string fileName in fileNames ) { // create a file with the name FileInfo file = new FileInfo( fileName ); // see if it exists on the disk // this fails if it was a directory if ( file.Exists ) { // both the key and the value are the file // would it be easier to have an empty value? fileList.Add( file ); } } // Create an instance of the IComparer interface IComparer<FileInfo> comparer = ( IComparer<FileInfo> ) new FileComparer( ); // pass the comparer to the sort method so that the list // is sorted by the compare method of comparer. fileList.Sort( comparer ); return fileList; } /// <summary> /// check that the user does want to delete /// Make a list and delete each in turn /// </summary> private void btnDelete_Click( object sender, System.EventArgs e ) { // ask them if they are sure System.Windows.Forms.DialogResult result = MessageBox.Show( "Are you quite sure?", // msg "Delete Files", // caption MessageBoxButtons.OKCancel, // buttons MessageBoxIcon.Exclamation, // icons MessageBoxDefaultButton.Button2 ); // default button // if they are sure... if ( result == System.Windows.Forms.DialogResult.OK ) { // iterate through the list and delete them. // get the list of selected files List<FileInfo> fileNames = GetFileList( ); foreach ( FileInfo file in fileNames ) { try { // update the label to show progress lblStatus.Text = "Deleting " + file.Name + "..."; Application.DoEvents( ); // Danger Will Robinson! file.Delete( ); } catch ( Exception ex ) { // you may want to do more than // just show the message MessageBox.Show( ex.Message ); } } lblStatus.Text = "Done."; Application.DoEvents( ); } } /// <summary> /// Get the full path of the chosen directory /// copy it to txtTargetDir /// </summary> private void tvwTargetDir_AfterSelect( object sender, System.Windows.Forms.TreeViewEventArgs e ) { // get the full path for the selected directory string theFullPath = GetParentString( e.Node ); // if it is not a leaf, it will end with a back slash // remove the backslash if ( theFullPath.EndsWith( "\" ) ) { theFullPath = theFullPath.Substring( 0, theFullPath.Length - 1 ); } // insert the path in the text box txtTargetDir.Text = theFullPath; } /// <summary> /// Mark each node below the current /// one with the current value of checked /// </summary> private void tvwSource_AfterCheck( object sender, System.Windows.Forms.TreeViewEventArgs e ) { // Call a recursible method. // e.node is the node which was checked by the user. // The state of the check mark is already // changed by the time you get here. // Therefore, we want to pass along // the state of e.node.Checked. if(e.Action != TreeViewAction.Unknown) { SetCheck(e.Node, e.Node.Checked ); } /// <summary> /// recursively set or clear check marks /// </summary> private void SetCheck( TreeNode node, bool check ) { // find all the child nodes from this node foreach ( TreeNode n in node.Nodes ) { n.Checked = check; // check the node // if this is a node in the tree, recurse if ( n.Nodes.Count != 0 ) { SetCheck( n, check ); } } } private void tvwExpand(object sender, TreeViewCancelEventArgs e) { TreeView tvw = ( TreeView ) sender; bool getFiles = tvw == tvwSource; TreeNode currentNode = e.Node; string fullName = currentNode.FullPath; currentNode.Nodes.Clear( ); GetSubDirectoryNodes( currentNode, fullName, getFiles, 1 ); } } }
C# supports a new
documentation
comment
style, with three slash marks
(///
). You can see these comments sprinkled
throughout Example 13-1. The Visual Studio editor
recognizes these comments and helps format them properly.
The C# compiler processes these comments into an XML file. You can create this file by using the /doc command-line switch. For example, you might compile the program in Example 13-1 with this command line:
csc Form1.cs /doc:XMLDoc.XML
You can accomplish this same operation in Visual Studio by clicking the FileCopier project icon in the Solution Explorer window, selecting View Property Pages on the Visual Studio menu, and then clicking Build property page. Click the XMLDocumentation File checkbox and type in a name for the XML file you want to produce, e.g., Filecopier.XML .
An excerpt of the file that’s produced for the
FileCopier
application of the previous section is
shown in Example 13-2.
Example 13-2. The XML output (excerpt) for file copy
<doc> <assembly> <name>FileCopier</name> </assembly> <members> <member name="T:FileCopier.frmFileCopier"> <summary> Form demonstrating Windows Forms implementation </summary> </member> <member name="F:FileCopier.frmFileCopier.components"> <summary> Required designer variable. </summary> </member> <member name="M:FileCopier.frmFileCopier.Dispose(System. Boolean)"> <summary> Clean up any resources being used. </summary> </member> <member name="M:FileCopier.frmFileCopier.InitializeComponent"> <summary> Required method for Designer support - do not modify the contents of this method with the code editor. </summary> </member> <member name="M:FileCopier.frmFileCopier.GetSubDirectoryNodes (System.Windows.Forms.TreeNode,System.String,System.Boolean, System.Int32)"> <summary> Gets all the subdirectories below the passed in directory node. Adds to the directory tree. The parameters passed in are the parent node for this subdirectory, the full path name of this subdirectory, and a Boolean to indicate whether or not to get the files in the subdirectory. </summary> </member> <member name="M:FileCopier.frmFileCopier.btnCopy_Click (System.Object,System.EventArgs)"> <summary> Create an ordered list of all the selected files, copy to the target directory </summary> </member>
The file is quite long, and although it can be read by humans, it isn’t especially useful in that format. You could, however, write an XSLT file to translate the XML into HTML, or you could read the XML document into a database of documentation. You can also drag the file from File Explorer into Windows Explorer, which provides a nice interface for reading the XML, as shown in Figure 13-9.
18.220.152.139