Click here to Skip to main content
15,867,488 members
Articles / Programming Languages / C#
Article

Building the CVS Root File Changing Utility

Rate me:
Please Sign up or sign in to vote.
4.60/5 (8 votes)
13 Nov 2005BSD16 min read 64.7K   1.2K   40   8
The process of building a tool to temporarily change the CVS/Root files for remote CVS access from gathering requirements through implementation and refinement.

Image 1

Introduction

Users of the Concurrent Versions System (CVS) who travel may find themselves battling with the content of the CVS/Root files that exist in their source tree. Because the host name used to resolve a computer can change from one LAN to another, the value stored in the CVS/Root file must reflect that. This article describes the process of building a visual tool to change the CVS/Roots' file contents and, when finished, revert them to their original state.

This essay adopts a more conversational than pedantic tone. However, I hope that some readers still find this narrative instructive. It ranges from abstract topics like problem analysis to detailed solutions like embedding images into an assembly and using them in a TreeView. The code samples in the article, as well as in the source code, have <string>a lot of comments. If you reside in the camp that we, as developers, should write self-commenting code, then I hope that you can tolerate the excessive //s.

Once upon a time...

On a warmer-than-usual winter morning, I sit on a rather comfortable bench just beyond the doors of the building in which I had a small, borrowed office. My espresso steams quietly in the thin air as I contemplate my problem: I have to figure out a way to temporarily change how the CVS client on my laptop connects to my CVS server. Normally, I connect to my CVS server using SSH tunnels. However, outside my firewall, I have to use my SSH tunnel for piping my IMAP connection.

This worry really doesn't constitute a huge problem. My CVS client, WinCVS, has a macro built into it that will do exactly what I need. However, if I run it, the settings become permanent. Until I change them back again. And, since I'm lazy and prone to forget things like that, I wanted a different solution. The subject of this article consists of the process of developing that solution.

A brief description of CVS and its files

If you know CVS and about the Root file, then you can probably skip to the next section. Otherwise, please let me explain.

From the Wikipedia: Concurrent Versions System, "[CVS] keeps track of all work and all changes in a set of files, typically the implementation of a software project, and allows several (potentially widely separated) developers to collaborate". As a contractor, I use CVS to track all of the changes for my clients' code, as well as allowing other developers on my projects to develop on the same code base.

Image 2

When you get the code from CVS using the normal check-out process, your CVS client will most likely make subdirectories in all of the directories in the code's source tree. In those subdirectories, aptly named "CVS," the client will create files that describe the files retrieved, the name of the module from which it retrieved them, and (most importantly for this article's existence), a file named "Root" that contains the connection information for the CVS client to automatically reconnect to the CVS server to get file updates. In the image to the left of this section, you can see a cropped view of my Window Explorer for some directories that have code that I checked out from my CVS server. Those subdirectories named "CVS" under the "Test Area" and "Test Area/deader" directories got created by my CVS client when I checked out the source files. Though you can't see it in the tree view, those "CVS" subdirectories have the "hidden" attribute set.

Inside those "CVS" subdirectories exists the Root file. As stated in the previous paragraph, the Root file contains the connection information for the CVS client to communicate with the CVS server. Normally, the Root files in my "CVS" subdirectories contain the string :ssh:curtis@cvs.grayiris.com:/var/cvs, which means "Use SSH to connect to the CVS server at cvs.grayiris.com with the user name 'curtis' and look in the path /var/cvs for the source code tree."

Requirements gathering

As stated in the last paragraph, the Root file in my CVS-controlled directories contains the string :ssh:curtis@cvs.grayiris.com:/var/cvs. However, because I have already used the SSH connection to open tunnels for my IMAP connection, I have to tunnel the CVS client's SSH connection through the existing SSH connection. I do that by forwarding requests on my localhost at port 22 to cvs.grayiris.com:22. So, I have to change the content of all my Root files to :ssh:curtis@localhost:/var/cvs. Then, when I finish my work, I'd like them to revert to their normal state without my interference. I'd like this to happen without creating backup files. I'm just not a big fan of them. I don't like my file system to get cluttered.

Furthermore, I don't want to spend a lot of time writing this tool. I mean, if it takes a huge investment of my time, then I can just live with running macros or writing a Perl script to take care of it and ignoring the temporary files that got created.

I get out my notebook and quickly write the requirements in a list:

  1. Short development time: 1 or 2 hours at most.
  2. Automatic reversion of CVS/Root files' content to old connection string.
  3. Capability to optionally, recursively descend through directories to change all CVS/Root files where they exist.
  4. An efficient tree view of the file system that contains at the top level:
    • The "My Documents" folder.
    • All local, logical, non-removable drives.
  5. Tree view must distinguish CVS-controlled directories in its presentation.

I also sketch how I want the user interface to look. The next image contains a reproduction of that sketch using computer drawing tools:

Image 3

Analysis and design

I really want to get something done on this tool before I have to work. I turn the page of my notebook and sit back on the bench. I glance at my watch and note that I have a couple of more minutes before I need to go inside.

Flow chart describing CVS/Root file modification

I decide to draw a quick flow chart to model the code's process of changing the CVS/Root files' contents. I come up with the flow chart in the following image:

Image 4

An efficient TreeView of the file system

When looking at the tree view, I decide that a load-on-demand scheme would work the best for me. I don't want to have to walk the entire directory structure to populate the TreeView that will represent the paths available to me. Starting with the root nodes of the "My Documents" directory and the local drives, the TreeView would only contain the nodes that the user has actively expanded.

After that, I walk into the building for work.

Lunch time

What does a software developer do during lunch? Develop software! In the morning I thought about this little project. It interests me. While others go off to their restaurants and microwaves, I take my laptop out to that bench where all this had started. I have some good ideas in writing, and a flow chart to help me code. I fire up Visual StudioTM and get to work.

I create a "Windows Application" project and build the user interface first. I really hate applications that don't resize well, so I take the time to ensure that this one will. The following image shows the layout of the items on my main form. With those attributes set, if I maximize the window on my widescreen laptop, everything will resize appropriately.

Image 5

Now, I have three real pieces of code to complete: the routine that manages the TreeView, the code that will modify the CVS/Root files, and the code that will revert the files.

Loading the TreeView

I decide to initially address issue four from my list above. Initially populating the TreeView must happen when the application begins. Since most of my work resides in the "My Documents" directory, I decide to add that first.

Since I decided earlier that I would create the view of the directory structure with a load-on-demand scheme, I need a way to store the path that a TreeNode represents, as well as if the application has already loaded the child nodes, if they exist. At the bottom of my form's class declaration, I create the following structure to hold that information:

C#
/// <summary>
/// A structure that contains information for a <see cref="TreeNode"/>.
/// </summary>
private struct NodeInfo
{
  /// <summary>
  /// Initializes the structure with the specified <see cref="bool"/>
  /// and <see cref="string"/> values.
  /// </summary>
  /// <param name="init">Denotes if the node has been initialized.</param>
  /// <param name="path">
  /// The path to the CVS-controlled directory represented by the node.
  /// </param>
  public NodeInfo( bool init, string path )
  {
    Initialized = init;
    Path = path;
  }
  
  /// <summary>
  /// A <see cref="bool"/> that contains the flag representing if the node
  /// has been initialized.
  /// </summary>
  public bool Initialized;
  
  /// <summary>
  /// A <see cref="string"/> containing the path represented by the node.
  /// </summary>
  public string Path;
}

Adding the "My Documents" TreeNode

Then, after the InitializeComponent method call in the constructor of the form, I add the following lines. Note that the TreeView control has the name dirTree:

C#
// Change the font of the tree to a fixed-width 
// font that I can read from a distance
// without using my glasses.
dirTree.Font = new Font( "Courier New", 10.0f );

// Create a bold font to mark CVS-controlled subdirectories.
bold = new Font( dirTree.Font.FontFamily.Name, 
                        dirTree.Font.Size, FontStyle.Bold );

// Get the path for My Documents.
string myDocDir = 
  Environment.GetEnvironmentVariable( "USERPROFILE" ) + "\\My Documents";

// Add the node representing the "My Documents" directory, if it exists.
if( Directory.Exists( myDocDir ) )
{
  // Create the My Documents tree node.
  <A name=addMyDocNode>TreeNode</A> myDocNode = new TreeNode( "My Documents" );
  
  // Check to see if the My Documents directory has subdirectories.
  if( Directory.GetDirectories( myDocDir ).Length > 0 )
  {
    // The My Documents has subdirectories. Let's add the loading node.
    myDocNode.Nodes.Add( new TreeNode( "Loading..." ) );
    
    // Also, we tag the node with the pertinent information that we'll need later
    // to populate the tree on demand.
    myDocNode.Tag = new NodeInfo( false, myDocDir );
  }
  
  // Add the My Documents node to the tree.
  dirTree.Nodes.Add( myDocNode );
}

Adding the logical drives' TreeNodes

I want to only add logical, non-removable drives to my TreeView. The method Environment.GetLogicalDrives() returns all of the logical drives without any drive type information. For a moment, I think that I have no alternative but to add all of them. Then, I remember the Windows Management Instrumentation interface that I used in some scripts that I wrote last year. I look in the .NET documentation and find that the System.Management assembly has what I need. I add a reference to it in my project, add a using System.Management directive to the file, and add the following code after the code that I added in the last section:

C#
// Create a management class to get the logical disks 
// that we can add to the directory tree.
ManagementClass c = new ManagementClass( "Win32_LogicalDisk" );

// Get the instances of the logical drives.
ManagementObjectCollection moc = c.GetInstances();

// Iterate over the logical drives.
foreach( ManagementObject mo in moc )
{
  try
  {
    // If the drive is a hard drive, then add 
    // it to the tree as a root item.
    if( mo[ "DriveType" ] != null && 
           Int32.Parse( mo[ "DriveType" ].ToString() ) == 3 )
    {
      // Get the name of the drive 
      // ("C:", for example) and append the path
      // separator character to it.
      string title = 
          mo[ "Name" ].ToString() + Path.DirectorySeparatorChar;
      
      // Create a new node that contains that 
      // represents that drive letter.
      <A name=addDriveNode></A>TreeNode tn = new TreeNode( title );
      
      // If subdirectories exist for that drive letter, then
      // add a "Loading..." node.
      if( Directory.GetDirectories( title ).Length > 0 )
      {
        // Create and add the "Loading..." node.
        tn.Nodes.Add( new TreeNode( "Loading..." ) );
        
        // Tag the node with the pertinent information.
        tn.Tag = new NodeInfo( false, title );
      }
      
      // Add the node that represents that drive letter.
      dirTree.Nodes.Add( tn );
    }
  }
  catch( Exception ) {}
}

I build the project and check out what I've done. Sure enough, the TreeView has the appropriate nodes in it with the appropriate expansion capabilities.

Adding the Load-On-Demand TreeView expansion

Of course, when I expand the root nodes in my TreeView, I only see a node that reads "Loading...". Alas, I have to do some more work. I look at the documentation for the TreeView control and note that the AfterExpand event meets my requirements. After the last code that I added to the constructor, I add the following line and allow Visual Studio to create the appropriate method for me:

C#
// Create an event handler that will load
// subdirectories into the tree when
// it expands.
dirTree.AfterExpand +=
       new TreeViewEventHandler( dirTree_AfterExpand );

Down in dirTree_AfterExpand, I populate it with the following code:

C#
/// <summary>
/// The event handler that will populate the 
/// tree's node with subdirectories, if
/// they exist.
/// </summary>
/// <param name="sender">The <see cref="object"/> 
/// that invoked the event.</param>
/// <param name="e">Some <see cref="TreeViewEventArgs"/>.</param>
private void dirTree_AfterExpand( object sender, TreeViewEventArgs e )
{
  // The information for the node.
  NodeInfo ni = ( NodeInfo ) e.Node.Tag;
  
  // If the code has not initialized the node, 
  // yet, with subdirectories, then
  // do that.
  if( !ni.Initialized )
  {
    // Get rid of the "Loading..." node.
    e.Node.Nodes.Clear();
    
    string[] dirs = null;
    try
    {
      // Get the subdirectories assigned to the path of the node.
      dirs = Directory.GetDirectories( ni.Path );
    }
    catch( DirectoryNotFoundException )
    {
      // Somebody's gone and removed the directory 
      // that once existed. We need to
      // inform the user and remove it from the tree.
      MessageBox.Show( 
         "That directory no longer exists and I will remove it from the tree.",
         "Invalid Directory",
         MessageBoxButtons.OK,
         MessageBoxIcon.Exclamation );
      e.Node.Remove();
      return;
    }
    
    // For each subdirectory, check its attributes and, 
    // if they meet the specified criteria,
    // add it to the tree.
    for( int i = 0; i < dirs.Length; i++ )
    {
      try
      {
        // Get the directory's information.
        DirectoryInfo di = new DirectoryInfo( dirs[ i ] );
        
        // If the directory does not have the 
        // System nor Hidden attributes set,
        // then add it to the tree.
        if( ( di.Attributes & FileAttributes.System ) == 0 && 
                   ( di.Attributes & FileAttributes.Hidden ) == 0 )
        {
          // Create a tree node that represents the subdirectory.
          <A name=addSubDirNode></A>int lastDirSepChar = 
                  dirs[ i ].LastIndexOf( Path.DirectorySeparatorChar );
          string nodeTitle = dirs[ i ].Substring( lastDirSepChar + 1 );
          TreeNode n = new TreeNode( nodeTitle );
          
          // Count the subdirectories and, if more than one exists, add a
          // "Loading..." node.
          if( CountSubDirs( Directory.GetDirectories( dirs[ i ] ) ) > 0 )
          {
            n.Nodes.Add( new TreeNode( "Loading..." ) );
          }
          
          // If the CVS subdirectory exists and 
          // the Root file exists in it, then
          // make the font for the node bold.
          string p = String.Format( "{0}{1}CVS{1}Root", 
                          di.FullName, Path.DirectorySeparatorChar );
          if( File.Exists( p ) )
          {
            <A name=specifyCvsNode></A>n.NodeFont = bold;
          }
          
          // Tag the subdirectory's node with the pertinent information.
          n.Tag = new NodeInfo( false, dirs[ i ] );
          
          // Add the new subdirectory node to the recently expanded node.
          e.Node.Nodes.Add( n );
        }
      }
      catch( Exception ) {}
    }
    
    // Mark the node as initialized.
    ni.Initialized = true;
    
    // Reset the node's tag.
    e.Node.Tag = ni;
  }
}

Now the TreeView works exactly how I want it to work. I look at my watch and note I only have about 25 minutes left for my lunch break. If I'm going to get this done, I need to step up the pace a little.

Modifying the CVS/Root files

When the application modifies a file, it needs to remember to which file it did what. Then, it needs to put an item in the ListBox to show what it did. I look at the documentation for the ListBox.Add method and see that it accepts any old System.Object. The ListBox then uses the Object's ToString method to display the entry. I scroll down to the bottom of my class' declaration and type in the following structure definition:

C#
/// <summary>
/// A structure that contains the path and content of modified
/// CVS/Root files.
/// </summary>
private struct PathSelectInfo
{
  /// <summary>
  /// Initializes the structure with the given <see cref="string"/>
  /// values.
  /// </summary>
  /// <param name="path">The path to the modified directory.</param>
  /// <param name="root">The value of the Root file.</param>
  public PathSelectInfo( string path, string root )
  {
    Root = root.Trim();
    Path = path;
  }
  
  /// <summary>
  /// A <see cref="string"/>-based representation of the struct
  /// used by the ListBox.
  /// </summary>
  /// <returns>A <see cref="string"/>-based 
  /// representation of the struct.</returns>
  public override string ToString()
  {
    return Path + " (" + Root + ")";
  }
  
  /// <summary>
  /// A <see cref="string"/> that contains 
  /// the value for the Root file.
  /// </summary>
  public string Root;
  
  /// <summary>
  /// A <see cref="string"/> that contains 
  /// the path to the CVS-controlled
  /// directory.
  /// </summary>
  public string Path;
}

Now, that I have a way to store information regarding the modified file, something needs to happen when I click the "Apply" button. Luckily, I spent the time earlier in the day defining the process in my flow chart. I double click on the button in my "Form View," Visual Studio adds the btnApply_Click event handler, and I add the following code to it:

C#
/// <summary>
/// The event handler for the "Apply" <see cref="Button"/>.
/// </summary>
/// <param name="sender">The <see cref="object"/> 
/// that invoked the event.</param>
/// <param name="e">Some <see cref="EventArgs"/>.</param>
private void btnApply_Click( object sender, System.EventArgs e )
{
  // Disable the controls.
  cbRecurse.Enabled = false;
  btnApply.Enabled = false;
  btnRevertAll.Enabled = false;
  
  // Get the current node's information.
  NodeInfo ni = ( NodeInfo ) dirTree.SelectedNode.Tag;
  
  // Change the CVS/Root files.
  ChangeCvsRoot( ni.Path );
  
  // Enable the controls.
  cbRecurse.Enabled = true;
  btnApply.Enabled = true;
  btnRevertAll.Enabled = true;
}

I don't do any file modification in the method because, if the user has chosen to descend into the subdirectories, then I will need a function that can get recursively called to perform those actions. Hence, I define the ChangeCvsRoot method:

C#
/// <summary>
/// Changes the Root file in the CVS subdirectory and adds the file's
/// information to the <see cref="ListBox"/>.
/// </summary>
/// <param name="path">The current path to investigate.</param>
private void ChangeCvsRoot( string path )
{
  try
  {
    // Create a string that will contain the CVS path.
    string cvsPath = String.Format( "{0}{1}CVS{1}Root", path, 
                                       Path.DirectorySeparatorChar );
    // Open a StreamReader to read the file's contents.
    StreamReader sr = File.OpenText( cvsPath );
    
    // Read the file's contents.
    string root = sr.ReadToEnd();
    
    // Close the StreamReader.
    sr.Close();
    
    // Open a StreamWriter to overwrite the file just read.
    StreamWriter sw = new StreamWriter( cvsPath );
    
    // Write the new root to the file.
    sw.Write( txtRoot.Text + Environment.NewLine );
    
    // Close the StreamWriter
    sw.Close();
    
    // If all that went well, add the modified file's path and old
    // value to the ListBox.
    lbFilePaths.Items.Add( new PathSelectInfo( path, root ) );
    
    // If the user has specified that she would like to descend
    // recursively into subdirectories, then do that.
    if( cbRecurse.Checked )
    {
      // Get the subdirectories of the current directory.
      string[] paths = Directory.GetDirectories( path );
      
      // For each subdirectory, change its CVS/Root file.
      foreach( string p in paths )
      {
        ChangeCvsRoot( p );
      }
    }
  }
  catch( Exception ) {}
}

Now, when I type in a new Root value, select a CVS-controlled directory from the TreeView, and click the "Apply" button, everything "Just Works"TM.

Reverting the files

Of course, I want to change the files back to their original form. Luckily, I have all that information stored in the ListBox! So, I double-click the "Revert All" button in the form view, Visual Studio creates the event handler btnRevertAll_Click and I fill it:

C#
/// <summary>
/// The "Revert All" <see cref="Button"/> that sets the modified CVS/Root
/// files to their original state.
/// </summary>
/// <param name="sender"></param>
/// <param name="sender">The <see cref="object"/> 
/// that invoked the event.</param>
/// <param name="e">Some <see cref="EventArgs"/>.</param>
private void btnRevertAll_Click( object sender, System.EventArgs e )
{
  // For each item in the ListBox starting with the bottom, revert the
  // file to its original state.
  for( int i = lbFilePaths.Items.Count - 1; i >= 0; i-- )
  {
    try
    {
      // Get the item out of the ListBox.
      PathSelectInfo psi = ( PathSelectInfo ) lbFilePaths.Items[ i ];
      
      // Open a StreamWriter to write the original value.
      string p = String.Format( "{0}{1}CVS{1}Root", psi.Path, 
                                       Path.DirectorySeparatorChar );
      StreamWriter sw = new StreamWriter( p );
      
      // Write the original value plus a new line character.
      sw.Write( psi.Root + Environment.NewLine );
      
      // Close the StreamWriter.
      sw.Close();
      
      // If no error has occurred, remove the item from the ListBox.
      lbFilePaths.Items.RemoveAt( i );
    }
    catch( Exception ) {}
  }
}

And, that's it. My utility works! I walk back inside the building with a satisfied smile on my face.

Making it pretty

Later that night...

After getting the kids off to bed, my wife and I relax out on the back porch. I have my laptop out there and show her my new program. She does not write software, nor does she really have an interest in CVS, C#, or my firewall woes. She does, however, like that I write little utilities for myself. I really think the world of her. So, her review means something to me. She says, "That's neat. But, it's kind of ugly. And it loads slowly."

Well, I have to agree with her. So, I decided to attack the immediate aesthetic deficiency. I had not put images in the tree and, admittedly, it looks a little peaked. So, I dig into the Tango Icon Library and find four images in the 16x16 size range that work well for me:

  • devices/drive-harddisk.png for the logical drives' nodes.
  • apps/system-file-manager.png for the "My Documents" node.
  • mimetypes/x-directory-normal.png for normal subdirectory nodes.
  • mimetypes/x-directory-remote.png for subdirectory nodes under CVS control.

I add them all to my project.

Embedding the images into the assembly and using them

For little utilities such as this one, I like to embed static resources such as those images. It makes the executable larger, but I don't have to worry about the paths to those files anymore. This requires the use of the .NET Reflection functionality. So, I follow these three easy steps:

  1. Change the image files' "Build Action" property value from "Content" to "Embedded Resource";
  2. Add the using System.Reflection statement to the file; and,
  3. Write the code to add the images to the tree.

Image 6

Step 1: Change the image files' build action

As you can see from the screenshot to the right, the system-file-manager.png file has a "Build Action" property in the Properties pane beneath the Solution Explorer pane. Note that I changed the value to "Embedded Resource". This instructs Visual Studio to instruct the compiler to embed this resource into the resulting assembly, an executable in this case. This way, in step 3, I can write the code to extract the image from the executable and use it in the TreeView.

Step 2: Add a using directive

Pretty self-explanatory: I type using System.Reflection at the top of my class' file.

Step 3: Using the Embedded Resources

To use images in a tree, the TreeView exposes the TreeView.ImageList property to which a System.Windows.Forms.ImageList can get assigned. So, to add my newly found images to the TreeView, I need to create an ImageList, add the images to it, assign that list to the tree, and go specify which images to use for the different nodes.

To that end, I go back to the constructor of my form and, after the call to InitializeComponent, I add the following lines to create the ImageList and populate it:

C#
// Create an image list for the tree.
ImageList iList = new ImageList();

// Populate the image list by getting the 
// executing assembly, getting each image
// from the manifest resources, then 
// adding that image to the image list.
Assembly a = Assembly.GetExecutingAssembly();
Stream imageStream = 
    a.GetManifestResourceStream( "CvsRootChanger.drive-harddisk.png" );
iList.Images.Add( Image.FromStream( imageStream ) );
imageStream = 
    a.GetManifestResourceStream( "CvsRootChanger.system-file-manager.png" );
iList.Images.Add( Image.FromStream( imageStream ) );
imageStream = 
    a.GetManifestResourceStream( "CvsRootChanger.x-directory-normal.png" );
iList.Images.Add( Image.FromStream( imageStream ) );
imageStream = 
    a.GetManifestResourceStream( "CvsRootChanger.x-directory-remote.png" );
iList.Images.Add( Image.FromStream( imageStream ) );

// Set the tree's image list.
dirTree.ImageList = iList;

If you use embedded resources in your assemblies, you can not forget to prepend the namespace to the file name when you want to retrieve it from its embedded nature. You'll see that in the four lines where I add the images to the list; I refer to each as "CvsRootChanger.filename" because my project has the namespace "CvsRootChanger".

Now that I have assigned the images to the TreeView, I must go specify which images to use. I search for all the places where I create a new TreeNode and specify which image to use by its zero-based index in the ImageList. I find three applicable instances where I add nodes that don't read "Loading..." and change them:

C#
// In the portion where I create the "My Documents" TreeNode
// Replaces <A href="#addMyDocNode">this line</A>.
TreeNode myDocNode = new TreeNode( "My Documents", 1, 1 );
C#
// In the portion where I create the TreeNodes for the logical drives
// Replaces <A href="#addDriveNode">this line</A>.
TreeNode tn = new TreeNode( title, 0, 0 );
C#
// In the portion where I create the TreeNodes for subdirectories
// Replaces <A href="#addSubDirNode">this line</A>.
int lastDirSepChar = 
        dirs[ i ].LastIndexOf( Path.DirectorySeparatorChar );
string nodeTitle = dirs[ i ].Substring( lastDirSepChar + 1 );
TreeNode n = new TreeNode( nodeTitle, 2, 2 );

Now, I don't create a new TreeNode for subdirectories under the control of CVS. I had just made them bold. So, I look for the bold assignment and add the instructions to use the fourth image for that node:

C#
// In the dirTree_AfterExpand method, after I specify that I want to use a bold
// font for the node.
// Inserted after <A href="#specifyCvsNode">this line</A>.
n.ImageIndex = n.SelectedImageIndex = 3;

I compile my project and start expanding the tree. Sure enough, the images exist correctly in each node.

Overcoming the slow loading

Nothing says professionalism like a splash screen.

Okay, that's not true. But, whenever I started the application, I had to wait while the logical drives got retrieved and added to the TreeView. This process could take up to five seconds. I did not like it. So, I decide to use a separate thread to load the main form while a secondary form got displayed to the user. I didn't want to invest a lot of time into this, so I kept the design simple. I get out my notebook, again, and quickly write some pseudo code to model the design.

  • Show the loading form.
  • When the loading form gets activated, start a thread to load the main form.
  • If the thread ends because the main form got loaded correctly,
    • Close the loading form.
    • Show the main form.

    Otherwise,

    • Display an error message.
    • Display a button to close the form.

I created another Windows Form in my project, set its ControlBox property to "False" set the form's Text property to "Loading Application...", added a Panel docked to the bottom with a Button in it, added a Label to the form docked with the "Fill" setting, and set the Label's Text property to "Loading the drive information for the application". The following image shows a screen shot of the form designer view of the form:

Image 7

Now, I needed a way for the loading form to create a main form that would not disappear after the loading form closed. So, at the end of my main form's class declaration, I added the following line:

C#
/// <summary>
/// A public static variable for the loading form to set.
/// </summary>
public static MainAppForm f;

With that in place, I could now store the form in that static variable and use it when the loading form closed. Since I planned on using a separate thread for loading the main form, I added the using System.Threading directive to the top of the loading form's class file. In the loading form's constructor, after the call to InitializeComponent, I added the following line and allowed Visual Studio to create the event handler for me:

C#
// An event handler that will load the application's main form.
this.Activated += new EventHandler( LoadingForm_Activated );

In the event handler, I wrote the following lines of code:

C#
/// <summary>
/// An event handler that will load the application's main form in
/// another <see cref="Thread"/>.
/// </summary>
/// <param name="sender">The <see cref="object"/> 
/// that invoked the event.</param>
/// <param name="e">Some <see cref="EventArgs"/>.</param>
private void LoadingForm_Activated( object sender, EventArgs e )
{
  // Create the object that contains the data for the thread.
  Foo f = new Foo();
  
  // Give the other thread a reference to this form so that it can
  // close it when it completes its work.
  f.f = this;
  
  // Create a thread to load the form.
  Thread t = new Thread( new ThreadStart( f.LoadMainAppForm ) );
  
  // Start the thread.
  t.Start();
}

And, last of all, at the end of the class declaration, I needed to create my Foo class that actually handled loading the application's main form:

C#
/// <summary>
/// A utility class that loads the application's main form.
/// </summary>
private class Foo
{
  /// <summary>
  /// A reference to the form that shows that we're loading the
  /// application's main form.
  /// </summary>
  public LoadingForm f;
  
  /// <summary>
  /// The method used to load the application's main form.
  /// </summary>
  public void LoadMainAppForm()
  {
    try
    {
      // Load the main form.
      MainAppForm.f = new MainAppForm();
      
      // Close the loading form.
      f.Close();
    }
    catch( Exception e )
    {
      // Change the alignment of the label's content.
      f.lblMessage.TextAlign = ContentAlignment.TopLeft;
      
      // Catch the exception and informa the user.
      f.lblMessage.Text = 
             "An exception occurred while loading the application"
             + Environment.NewLine
             + Environment.NewLine
             + "Message: "
             + e.Message;
      
      // Show the "OK" button.
      f.cancelPanel.Height = 40;
    }
  }
}

Finally, I want the loading form to show first when the application starts. I return to the code for my main form and find the Main method. I change it according to the following snippet:

C#
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main() 
{
  // First, show the loading form.
  Application.Run( new LoadingForm() );
  
  // If the loading form did its job, then run the application with
  // the application's main form.
  if( MainAppForm.f != null )
  {
    Application.Run( MainAppForm.f );
  }
}

I sat back, took a sip of my tea and showed my wife the effect of the changes that I had made. When she gave me the thumbs up, I knew that I had completed my application.

Request for feedback

Please, at your leisure, if you have an opinion about this article and its associated source files, please feel free to express your pleasure or dissent.

History

  • 2005-11-13
    • Wrote and submitted the article.
  • 2005-11-14
    • Changed the width of the <pre> tags to better fit in Firefox.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Architect curtissimo.com
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralVery helpful Pin
jsiemens4-Oct-07 14:05
jsiemens4-Oct-07 14:05 
GeneralRe: Very helpful Pin
Curtis Schlak.17-Apr-08 5:44
Curtis Schlak.17-Apr-08 5:44 
GeneralNice article Pin
commandant_.11-Jun-07 23:21
commandant_.11-Jun-07 23:21 
GeneralRe: Nice article Pin
Curtis Schlak.17-Apr-08 5:44
Curtis Schlak.17-Apr-08 5:44 
GeneralNice article. Can be used for smooth working of TortoiseCVS too, in conjunction with WinCVS Pin
VC Sekhar Parepalli20-Nov-05 20:32
VC Sekhar Parepalli20-Nov-05 20:32 
GeneralRe: Nice article. Can be used for smooth working of TortoiseCVS too, in conjunction with WinCVS Pin
Curtis Schlak.21-Nov-05 16:31
Curtis Schlak.21-Nov-05 16:31 
GeneralGreat Article Pin
jcsston14-Nov-05 7:27
jcsston14-Nov-05 7:27 
I've done this a few times manually. Changing those Root files gets very tiring in large folder trees. Wink | ;)
GeneralRe: Great Article Pin
Curtis Schlak.14-Nov-05 7:54
Curtis Schlak.14-Nov-05 7:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.