Click here to Skip to main content
Click here to Skip to main content

ForestPad - a method for storing and retrieving textual information

By , 6 Jun 2004
 

ForestPad - a method for storing and retrieving textual information

Introduction

ForestPad is a method for storing and retrieving textual information and consists of three applications:

Also included are two Setup projects. One for the PocketPC application that calls custom code which launches the ActiveSync application and one for a Windows Desktop version.

Before I go any further, I would like to pay homage to a wonderful program that I used for many years before deciding to write a replacement. The program is TreePad and it was written by Henk Hagedoorn . I would also like to thank CodeProject and all of the contributing developers for their hard work in creating one of the best resources on the web for information about .NET and C#.

Either ForestPadDesktop or ForestPadCE can be used independently. They become more powerful when used in combination with the ForestPadService which enables the PocketPC and Desktop applications to communicate over the web and syncronize data.

Currently, the Syncronize feature only checks to see which ForestPad document is newer. This means that data can be lost if you edit the same ForestPad document on two clients and then syncronize from each of them. Instead, you have to work in a "disconnected" mode. For instance, if you are editing data on the PocketPC in the field, you must syncronize the data to the ForestPadService, then syncronize before editing the same document in ForestPadDesktop.

This simple method works fine for me but could be extended in a variety of ways. You could add a check-in / check-out system similar to source control, you could syncronize data at the node level, or any other method that suites your needs. If you have a situation where you are always connected to the internet, you could even make the syncoronization automatic. If you only use one of the clients, you could still use the ForestPadService for the purpose of backup. The ForestPadService does not rely on a database so you can install it on any hosting account that supports the .NET Framework.

Background

I decided to write ForestPad as a way of storing all of the textual information that I need to recall. Here is the way that I use it:

I use it to store lyrics. When practicing guitar, I can quickly flip through the tabulature for the songs. (Ctrl-K) puts the cursor in the search box just like FireFox allowing me to quickly search for an item in the ForestPad document simply by paging using multiple presses of the enter key.

I store C# code fragments in it. This is allowed because the underlying file format is XML with CDATA sections. The only thing that currently can't be stored is an XML fragment that contains CDATA tags.

At work, I store IP addresses, urls, contacts, interesting programming articles, emails addresses, todo lists, the text of emails, you name it. If it is text and worth storing and retrieving, it goes into ForestPad. I also use it as a portable internet favorites application. To import a url, you just drag it from your browser onto the ForestPad Desktop application. Currently, links are not clickable as I implemented the TextBox control rather than the RichTextBox control. This decision was made because the .NET CF does not provide the RichTextBox yet (although I understand it is possible to access something like it through un-managed code.) You can also drag text from other programs to the TreeView, the main TextBox and the graphical buttons. In ForestPadDesktop, you can also select a section of text or a whole node and email the text if you have entered an SMTP server and a From address in the Settings section.

ForestPad design decisions

One of the things that always bothered me about TreePad was not a fault of TreePad but rather one of my own disorganization. I would constantly bury information so deep in a hierarchy that I would "lose" it. Also, there is a root node and for some inexplicable reason, it bothered me from a graphical perspective. I decided to follow a strict paradigm that both removed the root node and limited the depth of the hierarchy. In each ForestPad document, there are multiple forests which can contain multiple trees, branches, and leaves. This limited hierarchy, in my opinion, also makes the PocketPC version easier to use. Also, I decided to auto-name the TreeView node's Text property by displaying the text in the node up to the first carriage return. This allowed for quicker entry on the PocketPC as it is not necessary to name the node and made it easy to build an outline without minimizing the InputPanel.

The file format of TreePad seemed strange to me but one must take into account that it was designed around 1995. Each node had a number representing its level in the tree and was terminated using the following string:

<end node> 5P9i0s8y19Z

I would assume that Mr. Hagedoorn thought the sequence 5P9i0s8y19Z would be unlikely to appear in the text of a node and I think that was probably right, as I never had a problem with it during the time that I used TreePad.

A TreePad file with a root node and one sub-node looked like this:

<Treepad version 2.7>
dt=Text
<node>
name
0
<end node> 5P9i0s8y19Z
dt=Text
<node>
name
1
text
<end node> 5P9i0s8y19Z

For some interesting code that deals with parsing another file format, check out the TreePadConverter class. I have included a TreePad example file so that you can see how it works. The only limitation that I know of is that the TreePad file must only be 5 levels deep (a root node and four levels) so that it can be mapped into the forest, tree, branch, leaf format. The root node from the TreePad file will not be retained. If you have not used a Stack before and are curious about one of its many uses, it will be of special interest. (Note: The TreePadConverter has only been tested with "TreePad version 2.7" files)

I wanted to store ForestPad files using XML so that I could import the data into other progams and so that the file would retain its visual heirarchy when opened in UltraEdit .

So, in contrast to TreePad, the file format of ForestPad looks like this:

<?xml version="1.0" encoding="utf-8"?>
<forestPad
    guid="6c9325de-dfbe-4878-9d91-1a9f1a7696b0"
    created="5/14/2004 1:05:10 AM"
    updated="5/14/2004 1:07:41 AM">
<forest 
    name="A forest node"
    guid="b441a196-7468-47c8-a010-7ff83429a37b"
    created="01/01/2003 1:00:00 AM"
    updated="5/14/2004 1:06:15 AM">
    <data>
    <![CDATA[A forest node
        This is the text of the forest node.]]>
    </data>
    <tree
        name="A tree node"
        guid="768eae66-e9df-4999-b950-01fa9be1a5cf"
        created="5/14/2004 1:05:38 AM"
        updated="5/14/2004 1:06:11 AM">
        <data>
        <![CDATA[A tree node
            This is the text of the tree node.]]>
        </data>
        <branch
            name="A branch node"
            guid="be4b0993-d4e4-4249-8aa5-fa9c940ae2be"
            created="5/14/2004 1:06:00 AM"
            updated="5/14/2004 1:06:24 AM">
            <data>
            <![CDATA[A branch node
                This is the text of the branch node.]]></data>
                <leaf
                name="A leaf node"
                guid="9c76ff4e-3ae2-450e-b1d2-232b687214aa"
                created="5/14/2004 1:06:26 AM"
                updated="5/14/2004 1:06:38 AM">
                <data>
                <![CDATA[A leaf node
                    This is the text of the leaf node.]]>
                </data>
            </leaf>
        </branch>
    </tree>
</forest>
</forestPad>

Each item in the document contains a Guid , a Created date, and an Updated date. Text is contained within a CDATA section in the "data" node. Additional elements or attributes could be added to the ForestPad file format to allow futher functionality.

You can add nodes to the TreeView by clicking on one of the iconic buttons. ForestPad will add the node relative to the currently selected item. Nodes can be rearranged in ForestPadDesktop but currently can't be promoted or demoted. To delete a node, right-click on it and select Delete, or in ForestPadCE, hold the stylus on the node until the context menu appears.

Documents are stored in \My Documents\ForestPad on both platforms.

Summary of design goals

Overall, my design goals involved creating similar but even simpler program than TreePad with some fundamental differences.

  • XML-based file format
  • Limited node heirarchy
  • Auto-naming of nodes
  • Drag and drop of textual information
  • PocketPC and Desktop versions
  • Email text snippets from within ForestPadDesktop
  • Syncronization (and backup) of the data using a web service
  • Experimentation with a different style of user interface

About the code

ForestPadDesktop and ForestPadCE share some code. It is located in the ForestPadUtilities project. You will notice that there are several classes named xxxxxDesktop and xxxxxCE. The changes between these class are minimal and the code could have been combined using compiler switches. The disadvantage of this method is that there is duplicate code. The advantage is that it makes the build process smoother. Hopefully, Microsoft provides some obvious ommisions from the Compact Framework in the next release, such as the Guid class. Having to compile using the /unsafe directive is at odds with the concept of managed code.

If you haven't used the TreeView before, check out the ForestTreeNode class and the PopulateTreeView method in either ForestDesktop.cs or ForestCE.cs where you can see a ForestTreeNode object placed in a Tag attached to each TreeView node. This Tag contains a pointer to the corresponding node in the XmlDocument and provides a mechanism for keeping the TreeView and XmlDocument in sync.

public enum ForestType
{
    Forest = 0,
    Tree = 1,
    Branch = 2,
    Leaf = 3
}

namespace ForestPadUtilities
{
    /// <summary>
    /// Summary description for ForestTreeNode.
    /// </summary>
    public class ForestTreeNode
    {
        public ForestType NodeType;
        public string NodeName;
        public XmlNode NodePointer;
        public string NodeGuid;
        public string NodeCreated;
        public string NodeUpdated;

        public ForestTreeNode()
        {
        }
    }
}
        

Currently, you must download the source code to install the ForestPadService. At some point, I may build an installer.

In order to perform a full build of the code, you will have to modify the BuildCab.bat file in \ForestPadCE\BuildCabs\ to reflect the location of the files on your drive. The simplified command line is below:

cabwiz.exe ForestPadCE_PPC.inf /dest \ForestPad\ForestPadCE_Setup /err
    Logfile.log /cpu ARMV4 ARM SH3 MIPS X86 WCE420X86

You will also need to modify ForestPadCE_PPC.inf and update the paths. Read the CodeProject article Developing and Deploying Pocket PC Setup Applications for more information.

Build first in Debug mode and then in Release mode. I replaced the CAB files in the ForestPadCE_Setup project with empty text files to save space. They will be replaced with the actual CAB files during the first Release build.

There is a virtual directory to setup for this solution. Point a virtual directory to the ForestPadService directory. Give the ASPNET process full permissions to the ForestPadService\DATA folder. Then visit http://localhost/ForestPadService/Login.aspx and provide the Username: admin and Password: admin

Edit the admin user and create a new password. Then, in either ForestPadCE or ForestPadDesktop, in the Settings section, put http://localhost/ForestPadService/ForestPadService.asmx as the web service url, admin as your username and your new password. Save the changes, create a document, enter some text, and choose Syncronize.

Your data will transfer from the client to the ForestPadService.

Security

Security in ForestPad is minimal at the moment. It was not a major concern of mine when designing the program. Passwords for the ForestPad web service are encrypted within each application but currently the data stored on the ForestPad web service is not encrypted and the password is transferred to the web service in plain text. Adding encryption to the transaction would be a fairly trivial task. Also, it would be easy to store the ForestPad documents in a database, or to encrypt the XML.

You could also encrypt the information on the ForestPadCE and ForestPadDesktop clients. I chose not to do this as I wanted to be able to open the documents in a text editor. If you keep sensitive information, I would suggest making sure that both your PocketPC and your Desktop machine are password protected. For now, unless you modify the ForestPadService to include encryption, you could be putting yourself at risk by storing documents with it.

In the future, I will be releasing a version of the source at ForestPad.com that could be used to store secure information remotely.

Known open issues

ForestPadCE
  • Settings form displays instead of the main form sometimes. It seems to happen after you have opened and closed the settings form, then minimized the application and maximized it using the icon in the Start menu.
  • Problem with icon not showing up in 'Recent Programs" menu
ForestPadDesktop
  • Wierd resizing issue in the Desktop version -- on my system it grows vertically by 19 pixels on each launch. On other systems, acts unpredictably.
ForestPadService
  • Currently, ForestPad syncronizes using LastUpdated date. This means that data can be lost if the data is edited in two locations (e.g. ForestPadCE / ForestPadDesktop) and then synced from both locations. Data from the file that was edited longest ago would be lost.

Possibilities

This code could be used as the basis for many different types of projects.

For instance:

You could add a read-only attribute to the ForestPad User, and setup an identical Username and Password for everyone in an office to allow them all to receive ForestPad documents that you create. You could take the idea even further and allow the ForestPad User to store both their user specific documents and still receive the read-only syndicated documents. Or, modify the ForestPadService to redirect all the documents that a group of users create directly to you as a sort of data collection mechanism.

Add an option to ForestPadCE to store ForestPad documents on a removable memory card.

A search option could be added that would search through all of the ForestPad documents rather than just the one that is currently open. You could also extend the existing search to support regular expressions.

You could build a web-based viewer or editor for ForestPad documents.

Or, you could implement an option to transfer a ForestPad document between two PocketPC's using IR.

Special Thanks

The images for the four node levels were designed by my friend, Sean Kabanuk.

References

These resources have proven useful while building ForestPad:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

timothy_russell
Software Developer Snoffleware Studios LLC
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralRe: No icons with ppc2003 se - solutionmembertimothy_russell4 May '05 - 20:56 
I have posted a solution that works with Windows Mobile 2003 Second Edition.
 
It is located at http://www.forestpad.com under Downloads.
 
The scripts located in ForestPadCE\BuildCabs\ are keyed to paths on my local machine. You will need to change them to match your configuration. They get triggered from the PreBuildEvent.bat file in the CustomInstaller project. After you change them, in Debug compile, it shouldn't create the CAB files, while in Release mode, it should.
 
Also, before the solution is opened, create two IIS virtual directories, pointing to ForestPadWeb and ForestPadService.
 
Timothy Lee Russell
http://www.anatone.net
GeneralCoding question - cloning TreeView nodememberBaxslash18 Nov '04 - 22:14 
Question: how would one clone a TreeView Node?
 
Here is an example I've been attempting. So far only for the Forest node.
 
public void MoveNodeDown(TreeNode SelectedNode, TreeView Tv)
{
XmlNode nodePointer = ((ForestTreeNode)Tv.SelectedNode.Tag).NodePointer;
 
XmlNode newClonedNode = (XmlNode) nodePointer.Clone();
this.document.DocumentElement.InsertAfter(newClonedNode, nodePointer.NextSibling);
 
// but here is where I run into an error message
TreeNode clonedTreeNode = (TreeNode) Tv.SelectedNode.Clone();
Tv.Nodes.Insert(Tv.SelectedNode.Index + 2, Tv.SelectedNode);
}

 
However I get a MissingMethodException because of the clonedTreeNode attempt. I'm sure I'm forgetting something else important too. Any suggestions?
GeneralRe: Coding question - cloning TreeView nodemembertimothy_russell20 Nov '04 - 6:28 
Baxslash,
 
Try this:
 
Add a "clone" option to the context menu in ForestPadCE.cs --
 
Then add:
private void menuItemContextMenuForestViewClone_Click(object sender, System.EventArgs e)
		{
			currentForest.CloneSelectedNode(forestPadViewer.SelectedNode, forestPadViewer);
			currentForest.UpdateForestPadUpdatedDate();
			forestPadViewer.Update();
			forestPadHasChanged = true;
			searchTermHasChanged = true;		
		}
To ForestPad.cs, add:
 
public void CloneSelectedNode(TreeNode SelectedNode, TreeView Tv)
		{
			switch(((ForestTreeNode)Tv.SelectedNode.Tag).NodeType)
			{
				case ForestType.Forest :
					ForestTreeNode ftn = (ForestTreeNode)Tv.SelectedNode.Tag;
 
					ForestTreeNode newForestTreeNode = new ForestTreeNode();
					newForestTreeNode = ftn;
 
					XmlNode newForestNode = this.document.CreateElement("forest");
					XmlAttribute newForestNodeName = this.document.CreateAttribute("name");
					newForestNodeName.Value = newForestTreeNode.NodeName;
					newForestNode.Attributes.Append(newForestNodeName);
						
					//create the guid
					XmlAttribute newForestNodeGuid = this.document.CreateAttribute("guid");						
					newForestNodeGuid.Value = PocketGuid.NewGuid().ToString();
					newForestNode.Attributes.Append(newForestNodeGuid);
 
					//created - updated
					XmlAttribute created = this.document.CreateAttribute("created");
					created.Value = DateTime.Now.ToUniversalTime().ToString();
					XmlAttribute updated = this.document.CreateAttribute("updated");
					updated.Value = DateTime.Now.ToUniversalTime().ToString();
					newForestNode.Attributes.Append(created);
					newForestNode.Attributes.Append(updated);
 
					//add the data node						
					XmlNode dataNode = this.document.CreateElement("data");
					
					//get data
					string data = "";
 
					foreach(XmlNode childNode in ((XmlNode)ftn.NodePointer).ChildNodes)
					{
						if(childNode.Name == "data")
						{
							foreach(XmlCDataSection cData in childNode.ChildNodes)
							{
								data = cData.InnerText;
							}
						}
					}
 
					XmlCDataSection dataNodeCData = this.document.CreateCDataSection(data);
 
					dataNode.AppendChild(dataNodeCData);
					newForestNode.PrependChild(dataNode);
 
					this.document.DocumentElement.InsertAfter(newForestNode, (((ForestTreeNode)SelectedNode.Tag).NodePointer));
                    
					TreeNode forestTreeNode = new TreeNode(newForestNodeName.Value);
					forestTreeNode.ImageIndex = 0;
					forestTreeNode.SelectedImageIndex = 1;
 
					setTreeViewTag(forestTreeNode, newForestNode, ForestType.Forest);
					Tv.Nodes.Insert(Tv.Nodes.IndexOf(SelectedNode)+1, forestTreeNode);
 
					Tv.SelectedNode = forestTreeNode;
					break;
				case ForestType.Tree :
					break;
				case ForestType.Branch :
					break;
				case ForestType.Leaf :
					break;
			}
		}
 
Sorry, it's a bit unnecessarily complicated -- and, if you want the Forest node to bring its child nodes along with it, a whole world of hurt ensues.
 
I'm planning on a rewrite of ForestPad, now that I have been using the proof of concept for awhile and have received enough feedback to know where I am on the right track and where I'm not.
 
In the new version, cloning / moving nodes etc... will be much simpler. I am planning on hiding the complexity of keeping the TreeView and XmlDocument in sync.
 
Timothy
timothy@anatone.net
QuestionTypeLoadException error?sussBaxslash6 Nov '04 - 13:30 
Hello,
 
I recently stumbled on this article and I must say, this is such a great program! Especially for the PocketPC! It's funny, I even thought of making something like this just a week ago, after getting fed up with the various Notepad clones out there for the PocketPC.
 
The ForestPadCE program runs fine. However I'm trying to compile it -- to try out some changes, but I can't get even the original code to compile. Or rather, it compiles just fine, and I copy it to my iPaq (running Windows Mobile 2003, with the .NET CF SP2 installed of course). The program opens, but when I push New or Open it crashes.
 
The error:
TypeLoadException
Could not load type System.Windows.Forms.TreeView from assembly System.Windows.Forms
 
So I'm probably forgetting something in the compiler settings, or something. I'm using Visual Studio .NET 2003 with the Pocket PC 2003 SDK installed.
 
Do you have any tips or suggestions? Even a pointer to where to go to find out more about this problem would be helpful; I'm not the most experienced programmer. Thanks in advanced!
AnswerRe: TypeLoadException error?membertimothy_russell8 Nov '04 - 17:03 
Baxslash,
 
Thanks for the kind words about ForestPad.
 
I had the same problem when I upgraded to SP2 of the framework. Apparently, they changed some code having to do with the TreeView.
 
I have made other updates to the application but have not yet had time to post the updated solution.
 
In the meantime, try these changes:
 
In the ForestPadCE project, modify the ForestPadCE.cs file with the following lines:
 
line 494 should be:
 
currentForest = null;
 
add a line after it:
 
forestPadViewer.SelectedNode = null;
 
then, repeat at line 531 and line 660.
 
If that doesn't work, let me know.
 
Timothy
timothy@anatone.net
 
P.S. Diff information courtesy of the SourceGear implicit one user license.
GeneralRe: TypeLoadException error?memberBaxslash9 Nov '04 - 9:31 
Thanks for your response. Didn't work on my iPaq. It did work on the emulator though (which however is not SP2). So I got to try out some of my little modifications. A couple things I did: add a Select All option to the Edit menu, and add a Word Wrap toggle to the Tools menu (instead of having to go to Settings).
 
Looking forward to trying your changes. One suggestion I have: could you get keyboard combinations like Ctrl+C (copy), Ctrl+X (cut), and Ctrl+V (paste) to work? I find them really useful in other programs when I'm using an external keyboard.
 
Your code is nice and easy to follow. I'm surprised how few examples there are out there for just a PocketPC text editor program.
GeneralRe: TypeLoadException error?membertimothy_russell13 Nov '04 - 20:15 
Baxslash,
 
Say, I wasn't able to replicate the issue that you are having. I installed SP2 on my emulator, clicked New in ForestPad and it seemed to work fine.
 
Would you mind doing me a favor?
 
Could you add to the end of the MainWindow_Load method in ForestPadCE.cs:
 
after:
 
settings.ApplySettings();
 
add the following line:
 
MessageBox.Show(System.Environment.Version.ToString());
 
and let me know what version number it gives you on your emulator and on your iPaq. On my emulator, I got the version number: 1.0.3316.0 which is supposedly SP2 Final according to the list of version numbers I found on microsoft's site...
 
RTM = 1.0.2268.0
SP1 = 1.0.3111.0
SP2 Recall = 1.0.3226.0
SP2 Beta = 1.0.3227.0
SP2 Final = 1.0.3316.0
 
Thanks,
Timothy
timothy@anatone.net
 

GeneralRe: TypeLoadException error?memberBaxslash14 Nov '04 - 11:10 
On my iPaq I got 1.0.3316.0 as well. On the emulator I got 1.0.2268.0 until I installed SP2.
 
Well, guess there's something wrong with my iPaq install. I'll do some troubleshooting.
 
By the way, I discovered a very interesting resource which I thought you might be interested in checking out. It's at http://www.opennetcf.org/[^] , and basically is a bunch of libraries that "add what Microsoft left out" of the .NET CF. I quite like their TextBoxEx class; it has all sorts of cool methods built in like Undo, Copy and Paste -- stuff that is in .NET but not in the Compact Framework for some reason.
 
Also, I think you should submit your ForestPad program at http://www.pocketpcfreewares.com/en/[^] . It would be a great place to get some exposure; I'm sure plenty of other people would find your program useful.
GeneralRe: TypeLoadException error?membertimothy_russell15 Nov '04 - 14:53 
Baxslash,
 
Good luck with figuring out the problem. I am going to look into it more and see if I can replicate it on my iPaq. I don't think that I have upgraded to SP2 on it yet. The fact that it works in the emulator on SP2 is a good sign but I have found other things that work on the emulator but not on a real device.
 
I have seen the libraries that are available from opennetcf.org but I decided not to use them as I wanted to build the application using only what was in the compact framework libraries. (I was hoping that Microsoft would enhance certain areas, such as the TextBox class, in the next version of the compact framework.)
 
Thanks for the tip on the pocketpcfreewares.com site. I released the source for ForestPad to CodeProject mostly to say thanks to other developers whose articles I have learned so much from...but with a bit more polish, I might consider releasing ForestPadCE to the wild.
 
I'm curious to hear more of your ideas for improvements!
 
Timothy
timothy@anatone.net
GeneralSuggestionsmemberBaxslash16 Nov '04 - 12:18 
Don't spend too much time on that problem any more. I suspect it's my compiler -- the dlls in the CompactFrameworkSDK folder all say 2268 and I can't figure out how to upgrade it; I reinstalled the PocketPC2003 SDK and still no change. I downgraded my iPaq to .NET CF SP1 and it works fine now.
 
I enjoy suggesting things for programs I use. So here comes a few Wink | ;-) Some of these I've already done (*), so if you end up having questions let me know and I can send you my code.
 
*Keyboard combinations like Ctrl+C, Ctrl+X, and Ctrl+V (paste), etc.
 
*a Select All option to the Edit menu, and a Word Wrap toggle to the Tools menu.
 
*a "Fixed Font" (maybe you can think of a better name) option toggle in the Tools menu, which toggles between the default font and Courier New. [merely cosmetic, but useful in certain cases like coding where a fixed-width font is helpful.]
 
*Search selects the text it finds.
 
Now some probably harder/more complicated suggestions, in order of perceived difficulty. I'm trying to program them myself as well; it's a great learning experience!
 
-A way to store in memory the current selected text of the opened node. Thus, when you switch to a different node and then switch back, it 'remembers' where you were and you don't have to scroll down to find it.
 
-Inserting new nodes should (or at least there should be an option to) insert the node right 'after' the selected node in the TreeView. Thus one has control over what comes after what. [Implementing this for the actual nodes is easy, but reflecting it in the TreeView is complicated; I'm still working on this.]
 
-Ability to Undo. (Just in the textBox.)
 
That's it for now. Hope you find these useful, and I look forward to helping test out any new versions!

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 7 Jun 2004
Article Copyright 2004 by timothy_russell
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid