![]() |
Desktop Development »
Menus »
General
Intermediate
License: The Code Project Open License (CPOL)
A Magical Edit Menu ManagerBy Tom ClementA magical edit menu that works with no connections to the rest of your project. |
C#, Windows, .NET 1.0, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||

The code in this article creates a fully functional Edit menu that you can add to a WinForms application by calling one function. You can add support for non-text editing features by implementing a simple interface on controls that display the non-text editing objects.
So you've almost finished writing your new application and all that is left are a few finishing touches. You know that you need an edit menu, but what's the best way to hook it into your project? If you have a single edit surface, it's probably a no-brainer, but if your project is like mine, you have many distinct controls that are dynamically loaded as the user context changes and any of them may contain editable constituent controls. You may also have editable task panes and controls in navigation bars that are constantly changing.
If you try to track the state of all these controls, update the menus enabled and visible properties and respond appropriately to menu click events, your code can become a jumble. This article presents a UI design pattern that takes this complex situation and makes it surprisingly (well, surprising to me anyway) simple by deferring menu manipulation until the moment the menu is shown. You end up with a completely independent component that knows nothing about the application hosting the edit menu. The steps are:
Control.FromHandle() .NET Framework method to get a
reference to the .NET control associated with the focused window.
Of course, as always, the devil is in the details. In this article, we'll go step by step through the process of creating the magical edit menu manager and solving the various little problems encountered along the way. When we're done, you'll have a component you can easily reuse in any WinForms project.
As it was with my previous article, I'm sure that readers will have plenty of criticisms, comments and suggestions for improvement. Please feel free to share them, and I'll do my best to update the article accordingly.
Our goal is to create a separate module that can easily be reused in any C# application. To this end, we'll create and manage the edit menu inside of our module. All the calling application needs to do is pass in the top level edit menu. The Edit Menu Manager populates it.
Start by creating a new Windows Application Project called
MenuManagers. Delete Form1.cs and add a new Class called
EditMenuManager. We'll use this to define and manage the edit menu
items. Add using directives for some WinForms
namespaces.
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
In the class, declare the menu items.
// Declare the MenuItems to insert under the Edit menu
private MenuItem m_miEdit;
private MenuItem m_miUndo;
private MenuItem m_miRedo;
private MenuItem m_miCut;
private MenuItem m_miCopy;
private MenuItem m_miPaste;
private MenuItem m_miDelete;
private MenuItem m_miDividerRedoCut;
private MenuItem m_miDividerPasteDelete;
private MenuItem m_miSelectAll;
private MenuItem m_miDividerPropertiesSelectAll;
private MenuItem m_miProperties;
Create an enumeration corresponding to the positions of the menu items in the menu. This makes it a little bit easier to respond to menu commands.
[Flags]
private enum MenuIndex
{
Undo = 0,
Redo = 1,
Divider1 = 2,
Cut = 3,
Copy = 4,
Paste = 5,
Divider2 = 6,
Delete = 7,
SelectAll = 8,
Divider3 = 9,
Properties = 10
}
Create a function to create and initialize the menu items and call it from the constructor.
private void CreateMenus()
{
// Create
m_miUndo = new MenuItem();
m_miRedo = new MenuItem();
m_miCut = new MenuItem();
m_miCopy = new MenuItem();
m_miPaste = new MenuItem();
m_miDelete = new MenuItem();
m_miDividerRedoCut = new MenuItem();
m_miDividerPasteDelete = new MenuItem();
m_miSelectAll = new MenuItem();
m_miDividerPropertiesSelectAll = new MenuItem();
m_miProperties = new MenuItem();
// Initialize
m_miUndo.Index = (int)MenuIndex.Undo;
m_miUndo.Text = "&Undo";
m_miUndo.Shortcut = Shortcut.CtrlZ;
m_miRedo.Index = (int)MenuIndex.Redo;
m_miRedo.Text = "&Redo";
m_miRedo.Shortcut = Shortcut.CtrlY;
m_miDividerRedoCut.Index = (int)MenuIndex.Divider1;
m_miDividerRedoCut.Text = "-";
m_miCut.Index = (int)MenuIndex.Cut;
m_miCut.Text = "Cu&t";
m_miCut.Shortcut = Shortcut.CtrlX;
m_miCopy.Index = (int)MenuIndex.Copy;
m_miCopy.Text = "&Copy";
m_miCopy.Shortcut = Shortcut.CtrlC;
m_miPaste.Index = (int)MenuIndex.Paste;
m_miPaste.Text = "&Paste";
m_miPaste.Shortcut = Shortcut.CtrlV;
m_miDividerPasteDelete.Index = (int)MenuIndex.Divider2;
m_miDividerPasteDelete.Text = "-";
m_miDelete.Index = (int)MenuIndex.Delete;
m_miDelete.Text = "&Delete";
m_miDelete.Shortcut = Shortcut.Del;
m_miSelectAll.Index = (int)MenuIndex.SelectAll;
m_miSelectAll.Text = "Select A&ll";
m_miSelectAll.Shortcut = Shortcut.CtrlA;
m_miDividerPropertiesSelectAll.Index = (int)MenuIndex.Divider3;
m_miDividerPropertiesSelectAll.Text = "-";
m_miProperties.Index = (int)MenuIndex.Properties;
m_miProperties.Text = "Pr&operties";
m_miProperties.Shortcut = Shortcut.F4;
}
Now add a public method to let the calling application add these menus to its top level Edit menu. While we're at it, hook the Popup event on the Edit menu so we can tell when the menu is being clicked, and hook the Click events on the other menu items.
// The main entry point for this module. Used for initializing
// the Edit menu of the Windows Forms application.
public void ConnectMenus(MenuItem miEdit)
{
// Subsequent calls are ignored.
if( m_miEdit == null )
{
CreateMenus();
m_miEdit = miEdit;
m_miEdit.MenuItems.AddRange(
new MenuItem[] {
m_miUndo,
m_miRedo,
m_miDividerRedoCut,
m_miCut,
m_miCopy,
m_miPaste,
m_miDividerPasteDelete,
m_miDelete,
m_miSelectAll,
m_miDividerPropertiesSelectAll,
m_miProperties});
m_miEdit.Popup += new System.EventHandler(Edit_Popup);
m_miUndo.Click += new System.EventHandler(Menu_Click);
m_miRedo.Click += new System.EventHandler(Menu_Click);
m_miCut.Click += new System.EventHandler(Menu_Click);
m_miCopy.Click += new System.EventHandler(Menu_Click);
m_miPaste.Click += new System.EventHandler(Menu_Click);
m_miDelete.Click += new System.EventHandler(Menu_Click);
m_miSelectAll.Click += new System.EventHandler(Menu_Click);
m_miProperties.Click += new System.EventHandler(Menu_Click);
}
}
This is the only method you'll need to call from your application.
When the user clicks on the Edit menu, the first thing we need to do is find
out what control currently has the focus. To do this, we'll need to call a Win32
API method. Since we will need a number of Win32 methods, let's take a minute to
create a utility class for them. Create a new class called Win32API
in a separate file. We'll put all API calls and related utility methods in
it.
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
Now add some declarations and imports to the class.
public class Win32API
{
[StructLayout(LayoutKind.Sequential)]
public struct GETTEXTLENGTHEX
{
public Int32 uiFlags;
public Int32 uiCodePage;
}
public const int WM_USER = 0x400;
public const int EM_CUT = 0x300;
public const int EM_COPY = 0x301;
public const int EM_PASTE = 0x302;
public const int EM_CLEAR = 0x303;
public const int EM_UNDO = 0x304;
public const int EM_CANUNDO = 0xC6;
public const int EM_CANPASTE = WM_USER + 50;
public const int EM_GETTEXTLENGTHEX = WM_USER + 95;
/// Windows API SendMessage functions
[DllImport("user32.dll", CharSet = CharSet.Auto,
SetLastError = true)]
public static extern int SendMessage(IntPtr hWnd,
int msg, int wParam, int lParam);
[DllImport("user32.dll", EntryPoint="SendMessage",
CharSet=CharSet.Auto )]
public static extern int SendMessage( IntPtr hWnd, int Msg,
ref GETTEXTLENGTHEX wParam, IntPtr lParam);
// Return the handle of the window that has the focus.
[DllImport("user32.dll")]
public static extern IntPtr GetFocus();
/// Windows API GetParent function
[DllImport("user32", SetLastError = true)]
public extern static IntPtr GetParent(IntPtr hwnd);
// The constructor. Not used since all methods are static.
public Win32API(){}
Add a method to obtain the framework control that is associated with the currently focused control.
// Return the Framework control associated with the specified handle.
public static Control GetFrameworkControl(IntPtr hControl)
{
Control rv = null;
if( hControl.ToInt32() != 0 )
{
rv = Control.FromHandle(hControl);
// Try the parent, since with a ComboBox, we get a
// handle to an inner control.
if( rv == null )
rv = Control.FromHandle(GetParent(hControl));
}
return rv;
}
Finally, we add a few methods to fill in the gaps in the WinForms API. These
will be used to provide Cut/Copy/Paste/Undo functionality to the ComboBox
control, and to work around a bug in the RichTextBox control that
causes the Undo/Redo buffer to be cleared when the Text or
TextLength properties are read(!).
// Edit commands for the inner textbox in the ComboBox control.
public static void Undo(IntPtr hEdit)
{
SendMessage(hEdit, EM_UNDO, 0, 0);
}
public static void Cut(IntPtr hEdit)
{
SendMessage(hEdit, EM_CUT, 0, 0);
}
public static void Copy(IntPtr hEdit)
{
SendMessage(hEdit, EM_COPY, 0, 0);
}
public static void Paste(IntPtr hEdit)
{
SendMessage(hEdit, EM_PASTE, 0, 0);
}
public static bool CanUndo(IntPtr hEdit)
{
return SendMessage(hEdit, EM_CANUNDO, 0, 0) != 0;
}
// Determine whether the clipboard contains any format
// that can be pasted into a rich text box.
public static bool CanPasteAnyFormat(IntPtr hRichText)
{
return SendMessage(hRichText, EM_CANPASTE, 0,0) != 0;
}
// Determine the length of a control's Text. Required since using the
// RichTextBox .Length property wipes out the Undo/Redo buffer.
public static int GetTextLength(IntPtr hControl)
{
GETTEXTLENGTHEX lpGTL = new GETTEXTLENGTHEX();
lpGTL.uiFlags = 0;
lpGTL.uiCodePage = 1200; // Unicode
return SendMessage(hControl, EM_GETTEXTLENGTHEX,
ref lpGTL, IntPtr.Zero);
}
We now have everything we need to setup the menu when the user clicks on the
edit menu item. We use the GetFocus() API call to obtain the handle
of the currently focused control, then call the utility function
GetFrameworkControl() to find the first parent of that control that
corresponds to a framework object. Usually, this is the control with the handle
returned by GetFocus(), but sometimes, like with the
ComboBox, it's the parent.
Now that we've obtained a reference to the focused control, we can enable or
disable the edit menu items based on its state. But before handling the Popup
event, let's define a flags enum to represent the edit state of
the control. This way, a single bit field variable can easily describe the
condition of the menu. We use the [Flags] attribute since the
values will be combined in a bit flag.
// Declare an enumeration to represent the
// visible/invisible enabled/disabled
// state of the menu.
[Flags]
private enum EditState
{
None = 0x0,
UndoVisible = 0x1,
RedoVisible = 0x2,
UndoEnabled = 0x4,
RedoEnabled = 0x8,
CutEnabled = 0x10,
CopyEnabled = 0x20,
PasteEnabled = 0x40,
SelectAllEnabled = 0x80,
DeleteEnabled = 0x100,
RenameEnabled = 0x200,
PropertiesEnabled = 0x400
}
Now, write the Popup event handler. We'll start with TextBoxBase
to handle both TextBox and RichTextBox controls and
call the function GetTextBoxEditState() to return the state of
those classes of controls. Later, we'll add support for other types of controls.
After obtaining the bit flag determining the editable state of the
TextBoxBase, we set the corresponding properties on the menu
items.
// When the user clicks on the Edit menu, determine the focused control,
// call a method to get state flags, then setup the edit menu based
// on the flags.
private void Edit_Popup(object sender, System.EventArgs e)
{
IntPtr hFocus = Win32API.GetFocus();
Control ctlFocus = Win32API.GetFrameworkControl(hFocus);
EditState eEditState = EditState.None;
if( ctlFocus is TextBoxBase )
eEditState = GetTextBoxEditState((TextBoxBase)ctlFocus);
// Show or hide and enable or disable menu
// controls according to eEditState.
m_miUndo.Visible = (eEditState & EditState.UndoVisible) != 0;
m_miRedo.Visible = (eEditState & EditState.RedoVisible) != 0;
m_miDividerRedoCut.Visible = (m_miUndo.Visible == true
|| m_miRedo.Visible == true);
m_miUndo.Enabled = (eEditState & EditState.UndoEnabled) != 0;
m_miRedo.Enabled = (eEditState & EditState.RedoEnabled) != 0;
m_miCut.Enabled = (eEditState & EditState.CutEnabled) != 0;
m_miCopy.Enabled = (eEditState & EditState.CopyEnabled) != 0;
m_miPaste.Enabled = (eEditState & EditState.PasteEnabled) != 0;
m_miDelete.Enabled = (eEditState & EditState.DeleteEnabled) != 0;
m_miSelectAll.Enabled = (eEditState & EditState.SelectAllEnabled) != 0;
m_miProperties.Enabled = (eEditState & EditState.PropertiesEnabled) != 0;
}
The GetTextBoxEditState() method does the work:
// Obtain EditState flags for TextBox and RichTextBox controls.
private EditState GetTextBoxEditState(TextBoxBase textbox)
{
// Set Booleans defining the textbox state.
bool bWritable = (textbox.ReadOnly == false && textbox.Enabled == true);
bool bTextSelected = (textbox.SelectionLength > 0);
// Cannot use textbox.TextLength, because that
// wipes out Undo/Redo buffer in RichTextBox
bool bHasText = Win32API.GetTextLength(textbox.Handle) > 0;
bool bIsRichText = (textbox is RichTextBox);
// Use the Booleans to set the EditState flags.
EditState eState = EditState.UndoVisible;
if( bIsRichText )
{
eState |= EditState.RedoVisible;
if( ((RichTextBox)textbox).CanRedo )
eState |= EditState.RedoEnabled;
}
if( textbox.CanUndo )
eState |= EditState.UndoEnabled;
if( textbox.CanSelect )
eState |= EditState.SelectAllEnabled;
if( bTextSelected )
eState |= EditState.CopyEnabled;
if( bWritable )
{
if( bTextSelected )
{
eState |= EditState.CutEnabled;
eState |= EditState.DeleteEnabled;
}
if( bIsRichText )
{
if( Win32API.CanPasteAnyFormat(textbox.Handle) )
eState |= EditState.PasteEnabled;
}
else // TextBox
{
if( Clipboard.GetDataObject().GetDataPresent(DataFormats.Text) )
eState |= EditState.PasteEnabled;
}
}
return eState;
}
The method looks at various aspects of the TextBox (or
RichTextBox) state, like whether it is enabled, how much text is
selected and so forth. And based on this, determines whether various Edit
operations can be done on the contained text. Most of it is pretty
straightforward. The one trick that's not obvious, is that you cannot use the
TextBoxBase.TextLength method to determine whether there is any
text to select. The problem is that a bug in the RichTextBox causes
the reading of this property (or the RichTextBox.Text property for
that matter) to wipe out the entire Undo/Redo buffer in the
RichTextBox. In production code, you might consider deriving a
control from RichTextBox and overriding the Text and
TextLength methods with ones that obtain the data using the
API.
Another common control that we'd like the edit menu to operate against is the
ComboBox when its style is DropDown. We start by
adding a new function call to the Edit_Popup event handler.
...
else if( ctlFocus is ComboBox &&
((ComboBox)ctlFocus).DropDownStyle == ComboBoxStyle.DropDown )
eEditState = GetComboBoxEditState(hFocus, (ComboBox)ctlFocus);
...
And write the code to get the ComboBox state.
// Obtain EditState flags for ComboBox controls.
private EditState GetComboBoxEditState(IntPtr hEdit, ComboBox combobox)
{
// Set Booleans defining the ComboBox state.
bool bWritable = combobox.Enabled;
bool bClipboardText =
Clipboard.GetDataObject().GetDataPresent(DataFormats.Text);
bool bTextSelected = combobox.SelectionLength > 0;
bool bHasText = combobox.Text.Length > 0;
// Use the Booleans to set the EditState flags.
EditState eState = EditState.UndoVisible;
if( Win32API.CanUndo(hEdit) )
eState |= EditState.UndoEnabled;
if( bWritable )
{
if( bTextSelected )
{
eState |= EditState.CutEnabled;
eState |= EditState.DeleteEnabled;
}
if( bClipboardText )
eState |= EditState.PasteEnabled;
}
if( bTextSelected )
eState |= EditState.CopyEnabled;
if( bHasText )
eState |= EditState.SelectAllEnabled;
return eState;
}
As with the TextBox, we again look at various aspects of the
ComboBox to determine which menu actions are possible, and set the
flags accordingly. Since there is no CanUndo property on the
ComboBox, we pass the handle of the underlying TextBox
control to the Win32API.CanUndo() method to determine if the
previous action can be undone.
While it's nice to have automatic support for standard text editing controls,
many interesting edit behaviors are against non-text objects in controls you
have defined. For example, you might be implementing a graphical editor that
supports cut/copy and paste. Our menu can easily work with these controls so
long as they've implemented an interface that lets us both obtain state
information, and issue the corresponding commands to the control. We'll define a
public interface called ISupportsEdit and define it at the end of
the file.
// Define the public interface for user defined editable controls.
public interface ISupportsEdit
{
bool UndoVisible { get;}
bool CanUndo { get; }
void Undo();
bool RedoVisible {get;}
bool CanRedo { get; }
void Redo();
bool CanCut { get;}
void Cut();
bool CanCopy { get; }
void Copy();
bool CanPaste { get; }
void Paste();
bool CanSelectAll { get; }
void SelectAll();
bool CanDelete { get; }
void Delete();
bool CanShowProperties { get; }
void ShowProperties();
}
Now, let's modify the Edit_Popup event handler to check for
controls that support this interface. After the checks for
TextBoxBase and ComboBox, we add the following
code:
else
{
// If this is not a simple control, search up the parent chain for
// a custom editable control.
ISupportsEdit ctlEdit = GetISupportsEditControl(ctlFocus);
if( ctlEdit != null )
eEditState = GetISupportsEditState(ctlEdit);
}
The GetISupportsEditControl() method checks the control passed
in to see if it implements the ISupportsEdit interface. If it
doesn't, it travels up the parent chain until it finds a control that does, or
fails, returning null. If we find one, we call the
GetISupportsEditState() method to query it for menu state.
// Takes a control and traverses the parent chain until it finds a control
// that supports the ISupportsEdit interface. Returns that control, or null
// if none is found.
private ISupportsEdit GetISupportsEditControl(Control ctlFocus)
{
while( !(ctlFocus is ISupportsEdit) && ctlFocus != null )
ctlFocus = ctlFocus.Parent;
return (ISupportsEdit)ctlFocus;
}
// Set EditState flags for ISupportsEdit controls.
private EditState GetISupportsEditState(ISupportsEdit control)
{
EditState eState = EditState.None;
if( control.UndoVisible ) eState |= EditState.UndoVisible;
if( control.CanUndo ) eState |= EditState.UndoEnabled;
if( control.RedoVisible ) eState |= EditState.RedoVisible;
if( control.CanRedo ) eState |= EditState.RedoEnabled;
if( control.CanCut ) eState |= EditState.CutEnabled;
if( control.CanCopy ) eState |= EditState.CopyEnabled;
if( control.CanPaste ) eState |= EditState.PasteEnabled;
if( control.CanSelectAll ) eState |= EditState.SelectAllEnabled;
if( control.CanDelete ) eState |= EditState.DeleteEnabled;
if( control.CanShowProperties ) eState |= EditState.PropertiesEnabled;
return eState;
}
We're almost done. All that's left is to write an event handler for the menu click events and call methods for the various types of controls to do the requested edit operation. We start by writing the event handler for the menu click events.
// Click handler for all edit menus. Determine the focused window
// and framework control, then call a method to take the appropriate
// action, based on the type of the control.
private void Menu_Click(object sender, System.EventArgs e)
{
MenuItem miClicked = sender as MenuItem;
IntPtr hFocus = Win32API.GetFocus();
Control ctlFocus = Win32API.GetFrameworkControl(hFocus);
MenuIndex menuIndex = (MenuIndex)miClicked.Index;
if( ctlFocus is TextBoxBase )
DoTextBoxCommand((TextBoxBase)ctlFocus, menuIndex);
else if( ctlFocus is ComboBox &&
((ComboBox)ctlFocus).DropDownStyle == ComboBoxStyle.DropDown )
DoComboBoxCommand(hFocus, (ComboBox)ctlFocus, menuIndex);
else
{
ISupportsEdit ctlEdit = GetISupportsEditControl(ctlFocus);
if (ctlEdit != null )
DoISupportsEditCommand(ctlEdit, menuIndex);
}
}
This code gets the focused control, determines its type and passes the
control and the menu command on to a method for performing the command. The
method for Textbox and RichTextBox looks like
this:
// Perform the command associated with MenuIndex on the specified TextBoxBase.
private void DoTextBoxCommand(TextBoxBase textbox, MenuIndex menuIndex)
{
switch(menuIndex)
{
case MenuIndex.Undo: textbox.Undo(); break;
case MenuIndex.Redo:
if( textbox is RichTextBox )
{
RichTextBox rt = (RichTextBox)textbox;
rt.Redo();
}
break;
case MenuIndex.Cut: textbox.Cut(); break;
case MenuIndex.Copy: textbox.Copy(); break;
case MenuIndex.Paste: textbox.Paste(); break;
case MenuIndex.Delete: textbox.SelectedText = ""; break;
case MenuIndex.SelectAll: textbox.SelectAll(); break;
case MenuIndex.Properties: break;
}
}
The code is fairly simple, with the only difference between the
TextBox and RichTextBox is that
RichTextBox supports Redo(), so that is called out
separately.
The methods for ComboBox and ISupportsEdit are
equally straightforward:
// Perform the command associated with MenuIndex
// on the specified ComboBox.
private void DoComboBoxCommand(IntPtr hEdit,
ComboBox combobox, MenuIndex menuIndex)
{
switch(menuIndex)
{
case MenuIndex.Undo: Win32API.Undo(hEdit); break;
case MenuIndex.Cut: Win32API.Cut(hEdit); break;
case MenuIndex.Copy: Win32API.Copy(hEdit); break;
case MenuIndex.Paste: Win32API.Paste(hEdit); break;
case MenuIndex.SelectAll: combobox.SelectAll(); break;
case MenuIndex.Delete: combobox.SelectedText = ""; break;
}
}
// Perform the command associated with MenuIndex on
// the specified ISupportsEdit control.
private void DoISupportsEditCommand(
ISupportsEdit control, MenuIndex menuIndex)
{
switch(menuIndex)
{
case MenuIndex.Undo: control.Undo(); break;
case MenuIndex.Redo: control.Redo(); break;
case MenuIndex.Cut: control.Cut(); break;
case MenuIndex.Copy: control.Copy(); break;
case MenuIndex.Paste: control.Paste(); break;
case MenuIndex.SelectAll: control.SelectAll(); break;
case MenuIndex.Delete: control.Delete(); break;
case MenuIndex.Properties: control.ShowProperties(); break;
}
}
I've included a test project in the download that illustrates how to use this code. It has a form with TextBox, RichTextBox, ComboBox and custom TreeView based controls (TestEditableUserControl) on it. The TestEditableUserControl supports the MenuManagers.ISupportsEdit interface which lets you cut, copy and paste nodes between the trees. It also implements a trivial Property dialog (a MessageBox) that displays the node Text as an illustration of the Properties menu.
Well, believe it or not, we're done. If you're using WinForms menus, all you
have to do is include this project in your solution, define a single Edit top
level menu and pass it into the ConnectMenus() method. If you have
any custom controls that support editing capability, you'll want to implement
the ISupportsEdit interface on them. In my case, it was pretty
easy, since the controls already had context menus with all the edit commands on
them.
If you're using another menu system, you'll need to modify the code accordingly, but all the basic ideas of the article still apply. On this front, there is one caveat. If the menu system you're using takes the focus (it shouldn't) then the code in this article will not work, since the focused control will always appear to be the menu itself.
Anyway, good luck, have fun and let me know how it turns out if you do use this code. To be clear, you may use this code for any purpose whatsoever, personal or commercial.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 22 Feb 2004 Editor: Nishant Sivakumar |
Copyright 2004 by Tom Clement Everything else Copyright © CodeProject, 1999-2009 Web17 | Advertise on the Code Project |