Introduction
Almost four years after originally posting this article, I am finally updating it. I've decided to update this control to Visual Studio 2005 since that is the IDE I have been using for over a year now (including beta versions). I've added some performance improvements and a few enhancements (as well as a few bug fixes) to this new version. Although I'm posting the update now, it is still a work in progress, and I will be making further enhancements to it in the near future (e.g., fully implementing the context menu).
The original catalyst for this control came from an application I was working on that had a need to view web sites within the application itself. I wanted to make it easy for the users of the application to browse to their favorite web site, so I started looking for some type of "favorites" control, something that looked and worked similar to the Favorites explorer bar in Internet Explorer. After a bit of looking (including this site), I decided I would have to create my own control for displaying a user's favorites list.
Here are some of the features I wanted in my initial control:
- Similar look and feel of the Internet Explorer Favorites explorer bar.
- Ability to automatically load the favorites of the current user.
- Ability to refresh itself automatically if any of the favorites are modified via an outside source (e.g., Internet Explorer, Windows Explorer, etc.).
On with the code
For the most part, this is a very simple user control. Rather than detail every step, I will touch on the more interesting aspects of the control. You can download and view the source for more details.
To start with, we create a UserControl
and add a TreeView
control. We set the Dock
property of the TreeView
to Fill
so it will stretch to fill the entire size of the user control. Next, we set the HotTracking
property to true
so the TreeView
nodes will be underlined as the mouse moves over them, giving them the appearance of hyperlinks.
We add an image list to the control that contains at least three icons. The icons are used as follows:
- The first icon (index 0) is used as the shortcut icon.
- The second icon is used to denote an open folder.
- The third icon is used to denote a closed folder.
The source code for this project includes these three icons.
That takes care of the initial setup tasks. Now, let's see some code.
One of the first things we have to do is get the favorites path for the current user. The original version of this control retrieved the favorites path from the Windows registry. This has been updated to use the built-in Environment
class. The following snippet shows how this is done:
private void _GetFavoritesPath()
{
_favoritesPath =
Environment.GetFolderPath(Environment.SpecialFolder.Favorites);
}
Loading the Favorites
Next, we have to load the favorites into the TreeView
. This next code example is kind of long, but I'll explain what's going on, after you've had a chance to look it over.
private void LoadFavorites()
{
tvFavorites.BeginUpdate();
tvFavorites.Nodes.Clear();
LoadFavoritesFromFolder(new System.IO.DirectoryInfo(_strFavoritesPath), null);
LoadFavoritesFromPath(_strFavoritesPath, null);
tvFavorites.EndUpdate();
tvFavorites.SelectedNode = null;
if (tvFavorites.Nodes.Count > 0)
tvFavorites.SelectedNode = tvFavorites.Nodes[0];
}
private void LoadFavoritesFromFolder(
System.IO.DirectoryInfo aobjDirInfo, TreeNode aobjNode)
{
System.Windows.Forms.TreeNode objNode;
foreach (System.IO.DirectoryInfo objDir in dirInfo.GetDirectories())
{
if (currentNode == null)
objNode = tvFavorites.Nodes.Add(objDir.Name, objDir.Name, 2, 1);
else objNode = currentNode.Nodes.Add(objDir.Name, objDir.Name, 2, 1);
objNode.Tag = objDir.FullName;
if (objDir.GetDirectories().Length == 0)
LoadFavoritesFromPath(objDir.FullName, objNode);
else
{
LoadFavoritesFromFolder(objDir, objNode);
LoadFavoritesFromPath(objDir.FullName, objNode);
}
}
}
private void LoadFavoritesFromPath(string astrPath, TreeNode aobjNode)
{
IniFile objINI = new IniFile();
string name;
System.IO.DirectoryInfo objDir = new System.IO.DirectoryInfo(astrPath);
foreach (System.IO.FileInfo objFile in objDir.GetFiles("*.url"))
{
name = Path.GetFileNameWithoutExtension(objFile.Name);
if (currentNode == null)
tvFavorites.Nodes.Add(name, name, 0, 0).Tag =
objINI.IniReadValue("InternetShortcut", "URL",
objFile.FullName);
else
currentNode.Nodes.Add(name, name, 0, 0).Tag =
objINI.IniReadValue("InternetShortcut", "URL",
objFile.FullName);
}
}
The first method, RefreshFavorites
, is the main driver for loading the favorites. We call the TreeView
's BeginUpdate
method to suspend any painting until we have completed populating the TreeView
control. This will prevent any flickering during population, and makes for faster loading.
We then clear the TreeView
control, and call the LoadFavoritesFromFolder
method, which is used to recursively add all subdirectories in the favorites root directory to the TreeView
control. This method will be recursively called for each subdirectory found. Once in the LoadFavoritesFromFolder
method, if no subdirectories are found, the LoadFavoritesFromPath
method is called, which will load all shortcuts for the current subdirectory.
Note how the shortcuts are stored as text files with a .url extension. The contents of the file are in standard Windows INI format. To read the URL for the shortcut, you will need to use the Windows API calls for reading INI files. I have chosen to use an INI class that I found at The Code Project for this purpose. Once the URL is extracted from the INI file, it is stored in the TreeView
's current node's Tag
property. The Text
property is set to the filename of the shortcut sans the .url extension.
That takes care of loading the shortcuts into the TreeView
control, now let's customize some of the features of the control.
Let's customize
There are a couple of minor features that we need to implement to give our control a similar look and feel to that of Internet Explorer's Favorites explorer bar.
- The cursor should be an arrow when over folders, and should change to a hand when over a shortcut.
- Only one folder should be allowed to be open at any given level (i.e., if you click on a folder in the favorites list to open it and then click on another folder at the same level (or any level above it), then any other open folders should be collapsed). This will make more sense once you have a chance to see it in action.
private void tvFavorites_MouseMove(object sender,
System.Windows.Forms.MouseEventArgs e)
{
TreeView objTreeView = (TreeView)sender;
TreeNode objNode = objTreeView.GetNodeAt(e.X, e.Y);
if (objNode != null)
{
if (objNode.Tag != null)
objTreeView.Cursor = Cursors.Hand;
else
objTreeView.Cursor = Cursors.Default;
}
else
objTreeView.Cursor = Cursors.Default;
}
The code above is the event handler for the TreeView
's MouseMove
event. The code first gets a reference to the TreeView
control. It then attempts to get a reference to the node under the mouse pointer. If there is no node under the mouse pointer, the reference is set to null
and the method is exited.
If the current node is a folder, the cursor is changed to an arrow (the default setting). If it is a shortcut, the cursor is changed to a hand.
private void tvFavorites_Click(object sender, System.EventArgs e)
{
if ((_intX != -1) && (_intY != -1))
{
TreeView objTreeView = (TreeView)sender;
TreeNode objNode = objTreeView.GetNodeAt(_intX, _intY);
if (objNode != null)
{
if (objNode.ImageIndex == 0)
{
_currentURL = (string)objNode.Tag;
_currentURLName = (string)objNode.Text;
if (objNode.Parent == null)
{
_currentFolderName = "";
_currentFolder = _favoritesPath;
}
else
{
_currentFolderName = (string)objNode.Parent.Text;
_currentFolder = (string)objNode.Parent.Tag;
}
UrlClick(this, new UrlClickEventArgs(_currentURL));
}
else
{
tvFavorites.BeginUpdate();
_currentFolderName = (string)objNode.Text;
_currentFolder = (string)objNode.Tag;
_currentURL = "";
_currentURLName = "";
CollapseSiblings(objNode);
if (!objNode.IsExpanded)
objNode.Expand();
else objNode.Collapse();
tvFavorites.EndUpdate();
}
}
}
}
The code above is the event handler for the TreeView
's Click
event. I used the Click
event instead of the AfterSelect
event because, once a node has been selected, the AfterSelect
event won't be called again until you click on a different node. Since I want the node to expand/collapse each time I click on it (without necessarily having to leave the node), I placed the code here.
First, we get a reference to the TreeView
control, similar to the tvFavorites_MouseMove
method. We then get a reference to the clicked node. However, notice the use of the instance variables _intX
and _intY
. These variables are set in the tvFavorites_MouseDown
method, since the Click
event does not pass any mouse-related arguments.
We then check the Tag
property of the clicked node to determine if a folder or a shortcut was clicked. Folders do not have anything in their Tag
property, so if it is equal to null
, then we know it's a folder. If a shortcut was clicked, we store the shortcut's URL and the name, and then raise an event notifying any subscribers that a shortcut has been clicked.
If a folder was clicked, we store the current folder name, and then collapse any sibling nodes so that only one folder per level is open. We collapse the sibling folders using the following code:
private void CollapseSiblings(TreeNode aobjNode)
{
TreeNode objNode = currentNode.PrevNode;
while (objNode != null)
{
if (objNode.IsExpanded)
{
objNode.Collapse();
.. at a time, we can go ahead and exit the loop as
break;
}
objNode = objNode.PrevNode;
}
if (objNode == null)
{
objNode = currentNode.NextNode;
while (objNode != null)
{
if (objNode.IsExpanded)
{
objNode.Collapse();
break;
}
objNode = objNode.NextNode;
}
}
}
After collapsing the sibling nodes, the folder is either expanded or collapsed depending on its current state.
Who's messing with My Favorites!
The last piece we need to implement is the ability to refresh the TreeView
if any of the favorites are modified (e.g., renamed, deleted, added, etc.). Fortunately, this has been made very simple for us to accomplish with C# in .NET, by using the FileSystemWatcher
class. This class can be used to monitor various actions on a file system and raise events accordingly.
Here is the code we're going to use for our purposes:
private void InitializeFileSystemWatcher()
{
_objFSW = new FileSystemWatcher();
_objFSW.Path = _strFavoritesPath;
_objFSW.NotifyFilter = NotifyFilters.LastAccess |
NotifyFilters.LastWrite |
NotifyFilters.DirectoryName | NotifyFilters.FileName;
_objFSW.Filter = "*.*";
_objFSW.IncludeSubdirectories = true;
_objFSW.Changed += new FileSystemEventHandler(FSW_OnChanged);
_objFSW.Created += new FileSystemEventHandler(FSW_OnChanged);
_objFSW.Deleted += new FileSystemEventHandler(FSW_OnChanged);
_objFSW.Renamed += new RenamedEventHandler(FSW_OnRenamed);
_objFSW.EnableRaisingEvents = true;
}
private void fsw_OnChanged(object source, FileSystemEventArgs e)
{
this.Invoke(new EventHandler(fsw_Reload));
}
private void fsw_OnRenamed(object source, RenamedEventArgs e)
{
this.Invoke(new EventHandler(fsw_Reload));
}
private void fsw_Reload(object source, EventArgs args)
{
LoadFavorites();
}
First, we create a FileSystemWatcher
class and set its various properties. For example, the Path
property tells the FileSystemWatcher
class which folder to monitor. The IncludeSubdirectories
property tells the FileSystemWatcher
to include any subdirectories while it's monitoring for events. The other properties are mostly self-explanatory. If you have questions about any of them, they're readily available in the MSDN help.
Next, we set the event handlers for each of the four events - Changed
, Created
, Deleted
, and Renamed
. The first three events are handled by the same event handler, while the Renamed
event is handled by a separate handler - only because of a different method signature.
Each of the event handler's only purpose is to refresh the favorites list since something has changed in the file system. However, since the FileSystemWatcher
executes on a separate thread from that of the TreeView
control, we have to call the LoadFavorites
method using the Invoke
method of the control. This method takes a reference to a delegate that will be executed on the same thread of execution as the control.
Conclusion
Well, that pretty much covers the important parts of the control. There have been performance and usability improvements added to this release that weren't specifically covered in the article, but suffice it to say that this version is easier and more responsive to use than the previous version. Give the demo project a try, play around, and let me know what you think. Maybe, you will find a good use for this control as well.
To Do
There are several enhancements that could be made to this control to make it more complete. I may add some of these features in a future release, and then again, I may not :)
- Complete the context menu for URL/folder properties (e.g., Open, Rename, Delete, etc.). This release has the context menu, but it is not yet functional.
- Implement the ability to display a "favicon" for a URL, if one exists.
- Modify the
LoadFavorites
method to refresh only those nodes affected by a change in the favorites (e.g., if a URL is deleted from the favorites list, then delete that node from the TreeView
instead of refreshing the entire TreeView
).
- Add exception handling.
I'm sure there are other features that I have not yet thought of. If you think of anything cool you would like to see added to the control, let me know, and I'll see what I can do. Likewise, if you see any way of improving what I have done so far (remember, I'm relatively new to C#), then please let me know as I would like to learn from everyone else as well.