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

WWhizInterface: Enhancements to the Visual C++ Automation Interface

, 28 Jul 2001
Rate this:
Please Sign up or sign in to vote.
A C++ interface with a number of Visual C++ automation enhancements, allowing for more robust add-in programming.

Example of WWhizInterface's use in Workspace Whiz!

Introduction

Microsoft's Visual C++, like many Microsoft applications, exposes part of its functionality through a COM automation interface. Although Microsoft's intent was undoubtedly to put the power of Visual Studio in users' hands, those who have ever tried to do anything complex with the automation interface just become frustrated. Unlike the Microsoft Office products, which expose a rich automation interface, Visual C++'s looks like it was hacked in at the last moment.

Some time ago, Microsoft announced the Visual Studio Integration Program. Through the VSIP, developers would get full access to the headers and libraries to communicate with the various Visual Studio applications. The VSIP supposedly costs tens of thousands of dollars a year with a 5-year minimum commitment. For anyone who isn't a big company, this is a Bad Thing (TM).

Despite Microsoft's reluctance to provide average developers with the detailed Visual Studio specifications they need to create tightly integrated add-in applications, that has not stopped the ingenuity of several authors who have created some of the most phenomenal add-ins out there. Oz Solomonovich, for instance, writes an add-in called WndTabs that subclasses the main Visual C++ window to seamlessly insert a bar with window tabs into the user interface. Jerzy Kaczorowski's CvsIn integrates CVS with Visual C++, providing a powerful, free, alternative to source control systems such as Visual SourceSafe.

The purpose of this article is to describe the WWhizInterface SDK that complements Microsoft's Visual Studio automation interface. WWhizInterface was born of years of work developing the Visual C++ add-in Workspace Whiz! and its predecessor, the Workspace Utilities. WWhizInterface is a C++ interface providing access to certain Visual C++ capabilities the automation interface left out. The current iteration of WWhizInterface has been used in Jerzy Kaczorowski's CVS integration add-in, CvsIn, and Oz Solomonovich's Project Line Counter add-in. A previous form of WWhizInterface powers Mirec Miskufovic's Replace All Across Project Files add-in.

As mentioned previously, WWhizInterface and the Visual C++ automation interface work hand-in-hand. WWhizInterface exposes functionality the automation interface left out. WWhizInterface has an added benefit; most of its functionality can work without Visual C++ being active. WWhizInterface works seamlessly with Visual C++ 5.0, Visual C++ 6.0, and eMbedded Visual C++ 3.0.

WWhizInterface provides the following capabilities over the Visual C++ automation interface:

  • Retrieval of the full path to the workspace (.dsw or .vcw file).
  • Retrieval of the current project.
  • Retrieval of the current filename.
  • External parsing of the workspace .dsw or .vcw file to retrieve projects in the workspace.
  • External parsing of the project's .dsp, .vcp, or .vcproj (Visual C++ 7.0) file to retrieve files within the project.
  • Resolution of filenames containing environment variables and relative paths.
  • Quick scans of workspace and project files for changes.
  • Addition of projects not part of the workspace for all facilities provided by WWhizInterface.
  • Retrieval of file lists for the global workspace, and individual projects
  • Fast creation of tag lists for any files in the workspace.
  • Determination of tag scope for a given file and line number.
  • XML queries of project information.
  • ... and more.

The latest WWhizInterface can be found online at http://workspacewhiz.com/WWhizInterface.html. The Workspace Whiz! source distribution, which contains the sample code described below, and the source documentation (viewable online and in an archive), is available from there, in addition to far more information about WWhizInterface. 

Note: If any sample crashes in a Debug build, it is likely that WWhizInterface2D.mod could not be found (the error checking in the samples is only so-so). Either add the HKLM\Software\WWhizInterface\DebugPath value to the registry or copy WWhizInterface2D.mod to the Working Directory. If any sample crashes in a Release build, be sure to have run WWhizInterfaceInstallerWithCtags212.exe first.

Working With Workspaces, Projects, and Files

Creating a WWhizInterface object

To use WWhizInterface, add WWhizInterface2Loader.cpp, WWhizInterface2Loader.h, and WWhizInterface2.h to your project.

First, we need an instance of the WWhizInterface object. Retrieve this instance by calling the function WWhizInterface2Create(), declared in WWhizInterface2Loader.h:

WWhizInterface* __cdecl WWhizInterface2Create(HINSTANCE hInstance, IApplication* pApplication); 

hInstance is AfxGetInstanceHandle() in an MFC application. A console application may just pass in NULL.

pApplication is the Visual Studio automation interface IApplication pointer. If the application is not a Visual Studio add-in, then NULL may be passed instead.

In a console application, initialization would be performed like:

WWhizInterface* g_wwhizInterface;
g_wwhizInterface = WWhizInterface2Create(NULL, NULL);

It is possible for WWhizInterface2Create() to fail. The function first checks the working directory for the appropriate WWhizInterface2.mod or WWhizInterface2D.mod. If it is not there, it relies on a path stored in the registry at HKLM\Software\WWhizInterface\Path (or HKLM\Software\WWhizInterface\DebugPath if using a Debug build). The WWhizInterface\Path key is created by the WWhizInterfaceInstaller. The WWhizInterface\DebugPath key must be created through REGEDIT.

Workspace Name

A significant function of WWhizInterface is the retrieval of the active workspace's filename. This takes advantage of a property of Visual C++ described by Nick Hodapp in his article Undocumented Visual C++ which appeared on the Code Project. A helper .pkg file installed in the Common\MSDev98\Bin\IDE directory by the installer used to distribute WWhizInterface. For a user of WWhizInterface, the retrieval of the name is merely a function call.

CString workspaceName = g_wwhizInterface->GetWorkspaceName();

The name returned is the full path to the workspace's .dsw file, not the actual name of the workspace.

Obtaining the current file

If the application is a Visual Studio add-in, WWhizInterface::GetCurrentFilename() may be used to obtain the active file's filename. Visual Studio's automation functionality can do the same thing but not without a lot of COM setup pain.

CString currentFilename;
if (g_wwhizInterface->GetCurrentFilename(currentFilename))
{
    // Retrieve the WWhizFile object for the currentFilename:
    WWhizFile* curFile = g_wwhizInterface->GetFileList().Find(currentFilename);
    // Do something with curFile...
}

Filename resolution

Many filenames come in relative path form. Some filenames include an environment variable embedded in them formatted as $(ENV)\Filename.ext. WWhizInterface::ResolveFilename() will resolve any given filename to its absolute path.

CString relativeFilename = "test.cpp";
CString environmentFilename = "$(HOMEDRIVE)\\test.cpp";
CString rootDirectory;   // Empty = current directory

g_wwhizInterface->ResolveFilename(rootDirectory, relativeFilename);
// fullPath now contains the absolute path to test.cpp.
g_wwhizInterface->ResolveFilename(rootDirectory, environmentFilename);
// environmentFilename now contains the absolute path to $(HOMEDRIVE)\test.cpp.

Project Retrieval

Another of WWhizInterface's capabilities is the retrieval of every file in every project of a workspace. Unlike Visual Studio, WWhizInterface can work with multiple workspaces at a time. This powerful function is the basis behind Workspace Whiz!'s Extra Files feature. Extra Files makes information about extra workspaces and projects available for all supported Workspace Whiz! functions.

Whether the application is a Visual Studio add-in or not, workspaces and projects may be added to WWhizInterface. This is done by passing a Visual Studio-compatible filename to WWhizInterface::AddProject().

g_wwhizInterface->AddProject("d:\mfc.dsp");
g_wwhizInterface->AddProject("$(HOMEDRIVE)\WorkspaceWhiz\Src\WorkspaceWhiz60.dsw");

If the WWhizInterface-enabled application is a Visual Studio add-in, then the active workspace and active projects are automatically added by refreshing the file list.

When the caller is ready to access the information from added projects and workspaces, call WWhizInterface::RefreshFileList().

g_wwhizInterface->RefreshFileList();

Retrieving the Project List

The project list may be retrieved via a call to WWhizInterface::GetProjectList().

WWhizProjectList& projectList = g_wwhizInterface->GetProjectList();

The project list is made up of all workspaces and projects registered with WWhizInterface. To print the names of all projects in the active workspace, each project must be queried through WWhizProject::IsWorkspaceProject().

for (int i = 0; i < projectList.GetProjectCount(); ++i)
{
    WWhizProject* project = projectList.GetProjectByIndex(i);
    if (project->IsWorkspaceProject())
    {
        AfxMessageBox(project->GetName());
    }
}

If WWhizInterface is used within an add-in, it is possible to retrieve the current project as a WWhizProject.

WWhizProject* project = g_wwhizInterface->GetCurrentProject();

Retrieving filenames

WWhizInterface maintains several file lists:

  • A complete file list, comprised of every file from every project in every workspace registered with WWhizInterface. Accessed through WWhizInterface::GetFileList().
  • A per project file list containing every file within a given project. Accessed through WWhizProject::GetFileList().
  • A list of files recursively found within the Global Include and Source paths. Obtained through WWhizInterface::GetGlobalFileList().

Upon obtaining a file list, iterating its members is performed via the WWhizFileList::GetCount() and WWhizFileList::Get() functions:

WWhizFileList& fileList = g_wwhizInterface->GetFileList();
for (int i = 0; i < fileList.GetCount(); ++i)
{
    WWhizFile* file = fileList.Get(i);
    const CString& fullName = file->GetFullName());
    printf("%s\n", fullName);
}

Clearing out the file list

To completely clear the file list, use the function WWhizInterface::RemoveAllFiles(). This invalidates all registered workspaces and projects.

g_wwhizInterface->RemoveAllFiles();

Querying the Global Include and Source directories

Some applications require the files from the global Include and Source directories. WWhizInterface makes it easy to obtain those files.

g_wwhizInterface->RefreshGlobalFileList();
WWhizFileList& globalFileList = g_wwhizInterface->GetGlobalFileList();

Example: Backup Projects Add-in

Using the information above, a small add-in will be built that makes a backup copy of every file in the workspace. This is done by simply making a copy of the file to a file ending in the extension .bak.

The code below is from CCommands::BackupProjectsAddinCommandMethod() in the Src/Samples/BackupProjectsAddin directory of the Workspace Whiz! source distribution.

// Create the WWhizInterface.
WWhizInterface* g_wwhizInterface = WWhizInterface2Create(AfxGetInstanceHandle(), m_pApplication);

// Refresh all the files.
g_wwhizInterface->RefreshFileList();

// Get the project list.
WWhizProjectList& projectList = g_wwhizInterface->GetProjectList();

// Iterate the projects in the list.
for (int i = 0; i < projectList.GetProjectCount(); ++i)
{
    // Get the project at the specified index.
    WWhizProject* project = projectList.GetProjectByIndex(i);

    // Is it a member of the workspace? If not, then skip it.
    if (!project->IsWorkspaceProject())
        continue<;

    // Ask if we should backup the project.
    if (AfxMessageBox("Backup the project " + project->GetName() + "?", MB_YESNO) == IDNO)
        continue<;

    // Get the project's file list.
    WWhizFileList& fileList = project->GetFileList();

    // Iterate all members of the file list.
    for (int j = 0; j < fileList.GetCount(); ++j)
    {
        // Get the file at the specified index.
        WWhizFile* file = fileList.Get(j);

        // Make a copy of it.
        CString existingFilename = file->GetCaseFullName();
        CString newFilename = existingFilename + ".bak";
        ::CopyFile(existingFilename, newFilename, FALSE);
    }
}

// Destroy the WWhizInterface.
WWhizInterface2Destroy();

Using Tags

Overview

WWhizInterface builds a variety of tag lists for every file in the workspace. Tag lists contain information about almost every type of identifier known within the C++ language.

WWhizInterface maintains several tag lists:

  • A complete sorted tag list, comprised of every tag from every file in every project in every workspace registered with WWhizInterface (phew!). Accessed through WWhizInterface::GetTagList().
  • A per project sorted tag list containing every tag in every file within a given project. Accessed through WWhizProject::GetTagList().
  • A per file sorted tag list containing every tag in the file. Accessed through WWhizFile::GetTagList().
  • A per file ordered tag list containing every tag in the order it appears in the file. Accessed through WWhizFile::GetOrderedTagList().

Tags

A single tag contains a myriad of information about the identifier.

  • The access type (public, protected, private, or friend).
  • The implementation type (abstract, virtual, pure virtual).
  • The tag type (class, declaration, define, enum, enum member, file, function, class member, namespace, structure, typedef, union, variable).
  • The filename where the tag resides.
  • The regular expression search string to find the tag.
  • The parent identifier name (class name if the tag is a class member, etc).
  • The line number the tag resides.
  • The namespace.
  • The "buddy" tag (declaration if the tag is a definition and vice versa).

Example: Show Functions Add-in

To show off a few of the capabilities of WWhizInterface's tag interface, we'll build an add-in to show all the functions in the current file (similar to Workspace Whiz!'s Find Tag Special command). Our add-in, though, will show the body of the function in another window as the user clicks on a function.

When the user presses the 'ShowFunctionsAddin' button in Visual C++, the method CCommands::ShowFunctionsAddinCommandMethod() is called. This function first checks to see if there is an active document. If there is, the CFunctionsDialog is displayed.

// Get the current file. CString currentFilename; g_wwhizInterface->GetCurrentFilename(currentFilename); // If there isn't a file currently open, then don't show the dialog. if (!currentFilename.IsEmpty()) { CFunctionsDialog dlg; dlg.DoModal(); }

CFunctionsDialog::OnInitDialog() does the work of filling in the functions in the list box. It first refreshes the file list and tag list.

// Refresh all the files.
g_wwhizInterface->RefreshFileList();

// Refresh the tags.
g_wwhizInterface->RefreshTagList();

The next step is to retrieve the ordered tag list from the currently open file.

// Get the current filename.
CString currentFilename;
g_wwhizInterface->GetCurrentFilename(currentFilename);

// Get the WWhizFile pointer based on the current filename.
WWhizFile* file = g_wwhizInterface->GetFileList().Find(currentFilename);

// Retrieve the ordered tag list.
m_orderedTagList = &file->GetOrderedTagList();

Lastly, we need to iterate the ordered tag list looking for functions. When a function is found, the fully qualified name should be inserted into the function list box.

// Iterate the ordered tag list looking for functions. UINT tagListCount = m_orderedTagList->GetCount(); for (UINT i = 0; i < tagListCount; ++i) { // Get a pointer to the tag. WWhizTag* tag = m_orderedTagList->Get(i); // Is it a function? if (tag->GetType() == WWhizTag::FUNCTION) { // Yes, build the qualified name. CString fullName = tag->GetParentIdent() + CString("::") + tag->GetIdent(); // Add it to the list box. int index = m_functionList.AddString(fullName); // Set the list box item's data to be the index of the tag within // the ordered tag list. m_functionList.SetItemData(index, i); } }

In the list box's OnSelChange(), we need to retrieve the block of text representing the function source code (an approximation, anyway) and display it in the rich edit control. To simplify the COM automation and potentially add portability to Visual C++ 5.0 and eMbedded Visual C++ 3.0, the helper class ObjModelHelper from the Workspace Whiz! source distribution is used.

// Get the current selection.
int curSel = m_functionList.GetCurSel();
if (curSel == LB_ERR)
    return;

// Set up the object model for the active document.
ObjModelHelper objModel;
objModel.GetActiveDocument();
	
// Retrieve the ordered tag list index from the current selection's item data.
int tagNumber = m_functionList.GetItemData(curSel);

// Get the selected tag.
WWhizTag* tag = m_orderedTagList->Get(tagNumber);

// Move to the line number the selected tag starts at.
objModel.MoveTo(tag->GetLineNumber(), 1, dsMove);

// Now extend the selection to the top of the next tag.
WWhizTag* nextTag = NULL;
if (tagNumber + 1 < m_orderedTagList->GetCount())
{
    // There was another tag to go off.  Use it.
    nextTag = m_orderedTagList->Get(tagNumber + 1);
    objModel.MoveTo(nextTag->GetLineNumber(), 1, dsExtend);
}
else
{
    / The selected tag was the last one in the file.
    objModel.EndOfDocument(dsExtend);
}

// Retrieve the text.
CString text = objModel.GetText();

// Set it into the rich edit control.
m_functionCode.SetWindowText(text);

Using the XML Interface

In version 2.12, Workspace Whiz! introduced Visual C++ 7.0 project support through WWhizInterface. Visual C++ 7.0 project files are written in XML form and have the file extension .vcproj. Some helper classes, XmlData and XmlNode (both of which reside in the Src/Shared/ directory of the source distribution) allow simple parsing and manipulation of XML files.

The XML project files are so convenient to iterate through that a large part of Visual C++ 5.0 and 6.0 .dsp files are internally converted to the XML form. Every WWhizInterface project provides access to the internal representation of the XML format.

The XML Project File Format

At this time, only the groups and files between the # Begin Target and # End Target of a .dsp file are converted into XML form. Configuration data may be converted at a later time, based on demand.

<VisualStudioProject ProjectType="Visual C++" Version="6.00" Name="Project Name">
  <Files>
    <Filter Name="Source Files">
      <File RelativePath=".\StdAfx.cpp"&gt
      </File>
      <Filter Name="Subgroup">
      </Filter>
    &lt/Filter>
  </Files>
</VisualStudioProject>

Full XML project information is available for .vcproj files. Please refer to the .vcproj file for more information on other XML sections available.

Adding XML Support to your Project

In order to access WWhizInterface's XML functionality, four files need to be added to your project: XmlData.cpp, XmlData.h, Node.cpp, and Node.h. These files exist in the Src/Shared/ directory of the Workspace Whiz! source distribution.

Be sure to #include "XmlData.h" in the appropriate source files.

Retrieving the XML Project Data

WWhizProject::GetXmlData() returns a reference to the internal XmlData object containing the project information.

WWhizProject& project = g_wwhizInterface->GetCurrentProject();
XmlData& xmlData = project.GetXmlData();
.

Iterating the XML Project Data

Since the only guaranteed section available is <Files>, we need to ask the XmlData object for the XmlNode pointing to <Files>.

XmlNode* filesNode = (XmlData*)xmlData.Find("Files");

If filesNode is NULL, then there was no section in the XML file called <Files>.

An XmlNode is derived from a base class called Node. Node provides basic tree traversal through the functions GetParent(), GetNextSiblingNode(), GetPrevSiblingNode(), GetFirstChildNode(), and GetLastChildNode(). Iteration of XmlNodes is done through the Node traversal functions.

Retrieving Attributes

Various attributes are stored within the WWhizInterface XML node representation. The retrieval of these attributes is performed by either calling a function that finds an attribute by name or by iterating the attribute list.

Finding an attribute by name occurs through the function XmlNode::FindAttribute(). It returns an XmlNode::Attribute pointer. If the pointer is NULL, the attribute was not found.

XmlNode::Attribute* attr = fileNode->FindAttribute("RelativePath");

Iterating through the attribute list is done via the XmlNode::GetAttributeHead() and XmlNode::GetAttributeNext() functions. They are very similar in design to CList::GetHeadPosition() and CList::GetNext().

Example: Show Groups Add-in

To iterate the supported WWhizInterface hierarchy, a recursive function must be used. The sample code, Src/Samples/ShowGroupsAddin, shows this process.

static void RecurseFileNodes(XmlNode* parentNode, CTreeCursor cursor)
{
    // If the parent is NULL, then abort.
    if> (!parentNode)
        return;

    // Start with the first child.
    XmlNode* node = (XmlNode*)parentNode->GetFirstChildNode();

    // Iterate the children until there are no more.
    while< (node)
    {
        // Is it a <File> node?
        if (node->GetName() == "File")
        {
            // Get the relative path of the filename.
            XmlNode::Attribute* attr = node->FindAttribute("RelativePath");
            if (attr)
            {
                // Add it to the appropriate place in the tree control.
                cursor.AddTail(attr->GetValue());
            }
        }

        // Else is it a <Filter> node?
        else if (node->GetName() == "Filter")
        {
            // Get the name of the filter (the group name).
            XmlNode::Attribute* attr = node->FindAttribute("Name");

            // Give it a default, in case something is wrong.
            CString name = "Unknown";
            if (attr)
            {
                name = attr->GetValue();
            }

            // Add it to the appropriate place in the tree control.
            CTreeCursor childCursor = cursor.AddTail(name);

            // Recurse deeper into the XML.
            RecurseFileNodes(node, childCursor);
        }

        // Go to the next XML node.
        node = (XmlNode*)node->GetNextSiblingNode();
    }
}

Redistribution

A custom installer made just for WWhizInterface, called the WWhizInterfaceInstaller, is available to ease end-user installations. Although not publicly available at this time, the WWhizInterfaceInstaller's MiniInstaller source code will be available soon. Please continue checking http://workspacewhiz.com/ for it. Special thanks to Jeremy Collake for his excellent PECompact software which compresses one version of the WWhizInterfaceInstaller executable to only 51k.

WWhizInterface is freely redistributable subject to the following restrictions:

  • To distribute the WWhizInterface module in its entirety, the WWhizInterfaceInstaller must be used.
  • Individual source modules from the WWhizInterface source code made be inserted into an application. In this case, distribution of the WWhizInterfaceInstaller is not required.

License

The license from the Workspace Whiz! source distribution (which includes WWhizInterface) reads:

Workspace Whiz! - A Visual Studio Add-in Source Code
(http://workspacewhiz.com/) is Copyright 1999-2001 by Joshua C. Jensen
(jjensen@workspacewhiz.com).

The code presented in this source distribution may be freely used and
modified for all non-commercial and commercial purposes so long as due credit
is given and the source file header is left intact.

If the source module is from another author, that module may be used
subject to the restrictions of the author.

Workspace Whiz! and its accompanying files are provided "as is."
The author can not be held liable for any damages caused through the use of
this software, except for refund of the purchase price.

Conclusion

WWhizInterface has been invaluable for a number of add-in projects myself and others have created. It continues to be enhanced as Workspace Whiz! evolves and as users request new features. It is my hope that the functionality WWhizInterface provides can be of benefit to other add-ins and tools authors.

Thanks,
Joshua Jensen

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Joshua Jensen
Web Developer
United States United States
Joshua Jensen is a gamer at heart and as such, creates games for a living. He has the distinct pleasure of creating titles exclusively for the Xbox.
 
In his spare time, he maintains a Visual C++ add-in called Workspace Whiz! Find it at http://workspacewhiz.com/.

Comments and Discussions

 
GeneralHi, something is wrong with the WWhizInterfaceInstaller.zip I also had problems when trying to download it from the main site. Pinmemberrubimazaki17-May-08 4:43 
GeneralAdding projects to a workspace PinmemberMic19-Dec-03 4:21 
GeneralMemory buffered filelists Pinmemberjsgaarde16-Nov-03 11:25 
GeneralRe: Memory buffered filelists PinmemberJoshua Jensen17-Nov-03 18:15 
GeneralRe: Memory buffered filelists PinmemberJakob Simon-Gaarde18-Nov-03 9:49 
Generaladding menu items to (right click) context menues Pinmemberrobinraul12-Mar-03 11:52 
QuestionHow to put Arguments in Add-in command ??? PinsussAnonymous25-Nov-02 0:41 
AnswerRe: How to put Arguments in Add-in command ??? PinmemberJoshua Jensen26-Nov-02 20:19 
GeneralHooking Visual Studio to enhance IntelliSense Pinmemberroberts4-Sep-02 6:44 
GeneralVSS and C++ automation PinmemberRastislav22-May-01 2:40 
GeneralRe: VSS and C++ automation PinmemberAnonymous3-Apr-02 22:09 
If you do find an answer please post it here too, thanks.
GeneralOne question PinmemberVitaly Belman6-Apr-01 16:20 
GeneralRe: One question PinmemberJoshua Jensen11-Apr-01 13:58 
GeneralA weird error just when I add the files PinmemberVitaly Belman4-Apr-01 18:22 
GeneralRe: A weird error just when I add the files PinmemberJoshua Jensen4-Apr-01 18:28 
GeneralRelevance of Virtual* file codes PinmemberAnonymous9-Jan-01 8:08 
GeneralRe: Relevance of Virtual* file codes PinmemberJoshua Jensen9-Jan-01 9:39 
GeneralRe: Relevance of Virtual* file codes PinmemberAnonymous9-Jan-01 13:05 
GeneralRe: Relevance of Virtual* file codes PinmemberJoshua Jensen9-Jan-01 14:22 
GeneralRe: Relevance of Virtual* file codes PinmemberAnonymous10-Jan-01 6:14 
GeneralReasons for doing the tag system the way it is PinmemberJoshua Jensen10-Jan-01 7:07 
GeneralRe: Reasons for doing the tag system the way it is PinmemberAnonymous10-Jan-01 12:02 
GeneralRe: Reasons for doing the tag system the way it is PinmemberJoshua Jensen10-Jan-01 12:36 
GeneralRe: Reasons for doing the tag system the way it is PinmemberPaul Selormey8-Feb-01 9:18 
GeneralRe: Reasons for doing the tag system the way it is Pinmemberrepcsi23-Jan-02 21:43 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.140709.1 | Last Updated 29 Jul 2001
Article Copyright 2001 by Joshua Jensen
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid