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

Writing Your First Visual Studio Language Service

By , 11 Dec 2009
 

Introduction

This is part 1 in a multi-article series on building your own IDE. The series will show how to extend the Visual Studio Isolated Shell to create an IDE. Each article in the series will build on the previous article to add new features to the environment. This article will create the My C language service in C# using the Irony Compiler Construction Kit. The language was initially created in the ManagedMyC example that is included with the Visual Studio SDK. The SDK example uses the Managed Babel System.

Using the Code in the Download

The first download supplied with this article contains the completed My C language service sample discussed in this article. It requires Visual Studio 2008 Standard edition or higher, and the Microsoft Visual Studio 2008 SDK.

Visual Studio - My C Programming Language

In order to run the solution:

  1. Download and unzip the source files.
  2. Open MyCLanguageService.sln in Visual Studio 2008.
  3. Build and run the project (F5).
  4. When the Visual Studio experimental hive opens, open a My C source file (.myc) in the code editor. Two test files are included in the download: short.myc and thing.myc.

Background

A language service in Visual Studio provides language-specific support (syntax coloring and highlighting, statement completion, brace matching, parameter information tooltips, etc.). A language service is implemented by creating a Visual Studio Integration Package. A project template is provided when the Visual Studio SDK is installed.

Managed language services are created using the Managed Package Framework. In addition, the Managed Babel System uses MPF, and provides a scanner and parser, MPLex, and MPPG. MPLex and MPPG are a variation of the Garden Point Scanner Generator (GPLex) and the Garden Point Parser Generator (GPPG) created by the Queensland University of Technology. These tools take a LEX-like and YACC-like specification and generate a scanner and parser in C#.

The Irony Compiler Construction Kit combines the lexical and parser specification in one C# file. Also, since the file is already in C#, no code generation takes place. This creates a solution that is easier to debug and maintain.

Creating the Language Service Package

The second download is a compressed file containing a template to generate a Visual Studio Integration Package for an Irony Language Service. In order to use the template, download and unzip the contents to a temporary location. The two DLL files, Wizard.dll and IronyLanguageServiceWizard.dll, should be placed in the Global Assembly Cache (C:\Windows\assembly). The third file, IronyLanguageServicePackage.zip, should be placed in the Visual Studio user templates directory, typically "My Documents\Visual Studio 2008\Templates\ProjectTemplates". This will install a Visual Studio template that can be accessed in the New Project dialog. It will appear in the My Templates section when C# is selected.

Add New Project Dialog

When creating a new Irony Language Service Package, a wizard is run to gather information specific to the language that is being created.

Irony Language Service Wizard

The first page requests basic information about the language. The company name will be used as the namespace for all the classes. Also, if your language supports multiple file extensions, separate each extension with a semicolon.

Irony Language Service Wizard - Language Information

Next, the wizard requests the features your language will support in Visual Studio. Leaving the options checked does not create any additional work, because the necessary integration is already included in the package.

Irony Language Service Wizard - Language Options

Package Load Keys (PLK) are required for VSPackages to load successfully. The Visual Studio SDK provides a Developer Load Key (DLK) that makes it possible for VSPackages to load without a PLK during development. If the language being developed will be used on machines without the Visual Studio SDK, a PLK is a must. A PLK can be generated here.

Irony Language Service Wizard - PLK

Defining Grammar in Irony

After running the Irony Language Service Wizard, there should be a Grammar.cs file open in the editor. This is where the Irony Grammar will be defined. The LEX and YACC files included with the ManagedMyC sample are being used for the language specification.

First, declare the terminals. Terminals include keywords, operators, comments, variables, etc.

CommentTerminal blockComment = new CommentTerminal("block-comment", "/*", "*/");
CommentTerminal lineComment = new CommentTerminal("line-comment", "//",
    "\r", "\n", "\u2085", "\u2028", "\u2029");
NonGrammarTerminals.Add(blockComment);
NonGrammarTerminals.Add(lineComment);

NumberLiteral number = new NumberLiteral("number");
IdentifierTerminal identifier = new IdentifierTerminal("identifier");

Next, declare the non-terminals:

NonTerminal program = new NonTerminal("program");
NonTerminal declarations = new NonTerminal("declaration");
NonTerminal declaration = new NonTerminal("declaration");
NonTerminal simpleDeclarations = new NonTerminal("simple-declarations");
NonTerminal simpleDeclaration = new NonTerminal("simple-declaration");
NonTerminal semiDeclaration = new NonTerminal("semi-declaration");
NonTerminal parenParameters = new NonTerminal("paren-parameters");
NonTerminal parameters = new NonTerminal("parameters");
NonTerminal classOption = new NonTerminal("class-option");
NonTerminal variableType = new NonTerminal("variable-type");
NonTerminal block = new NonTerminal("block");
NonTerminal blockContent = new NonTerminal("block-content");
NonTerminal statements = new NonTerminal("statements");
NonTerminal statement = new NonTerminal("statement");
NonTerminal parenExpressionAlways = new NonTerminal("paren-expression-always");
NonTerminal parenExpression = new NonTerminal("paren-expression");
NonTerminal forHeader = new NonTerminal("for-header");
NonTerminal forBlock = new NonTerminal("for-block");
NonTerminal semiStatement = new NonTerminal("semi-statement");
NonTerminal arguments = new NonTerminal("arguments");
NonTerminal parenArguments = new NonTerminal("paren-arguments");
NonTerminal assignExpression = new NonTerminal("assign-expression");
NonTerminal expression = new NonTerminal("expression");
NonTerminal booleanOperator = new NonTerminal("boolean-operator");
NonTerminal relationalExpression = new NonTerminal("relational-expression");
NonTerminal relationalOperator = new NonTerminal("relational-operator");
NonTerminal bitExpression = new NonTerminal("bit-expression");
NonTerminal bitOperator = new NonTerminal("bit-operator");
NonTerminal addExpression = new NonTerminal("add-expression");
NonTerminal addOperator = new NonTerminal("add-operator");
NonTerminal multiplyExpression = new NonTerminal("multiply");
NonTerminal multiplyOperator = new NonTerminal("multiply-operator");
NonTerminal prefixExpression = new NonTerminal("prefix-expression");
NonTerminal prefixOperator = new NonTerminal("prefix-operator");
NonTerminal factor = new NonTerminal("factor");
NonTerminal identifierExpression = new NonTerminal("identifier-expression");

Then, define the rules for the non-terminals. Also, in Irony, the root non-terminal must be specified.

this.Root = program;

program.Rule = declarations;

declarations.Rule = MakeStarRule(declarations, declaration);

declaration.Rule
    = classOption + variableType + identifier + parameters + block
    | classOption + identifier + parenParameters + block
    | variableType + identifier + parenParameters + block
    | identifier + parenParameters + block
    | simpleDeclaration;

simpleDeclarations.Rule = MakePlusRule(simpleDeclarations, simpleDeclaration);

simpleDeclaration.Rule = semiDeclaration + ";";

semiDeclaration.Rule
    = semiDeclaration + "," + identifier
    | classOption + variableType + identifier
    | variableType + identifier;

parameters.Rule
    = parameters + "," + variableType + identifier
    | variableType + identifier;

parenParameters.Rule
    = ToTerm("(") + ")"
    | "(" + parameters + ")";

classOption.Rule
    = ToTerm("static")
    | "auto"
    | "extern";

variableType.Rule
    = ToTerm("int")
    | "void";

block.Rule
    = ToTerm("{") + "}"
    | "{" + blockContent + "}";

blockContent.Rule
    = simpleDeclarations + statements
    | simpleDeclarations
    | statements;

statements.Rule = MakePlusRule(statements, statement);

statement.Rule
    = semiStatement + ";"
    | "while" + parenExpressionAlways + statement
    | "for" + forHeader + statement
    | "if" + parenExpressionAlways + statement
    | "if" + parenExpressionAlways + statement + "else" + statement;

parenExpressionAlways.Rule = parenExpression;

parenExpression.Rule = ToTerm("(") + expression + ")";

forHeader.Rule = "(" + forBlock + ")";

forBlock.Rule = assignExpression + ";" + expression + ";" + assignExpression;

semiStatement.Rule
    = assignExpression
    | "return" + expression
    | "break"
    | "continue";

arguments.Rule
    = expression + "," + arguments
    | expression;

parenArguments.Rule
    = ToTerm("(") + ")"
    | "(" + arguments + ")";

assignExpression.Rule
    = identifier + "=" + expression
    | expression;

expression.Rule
    = relationalExpression + booleanOperator + expression
    | relationalExpression;

booleanOperator.Rule
    = ToTerm("&&")
    | "||";

relationalExpression.Rule
    = bitExpression + relationalOperator + bitExpression
    | bitExpression;

relationalOperator.Rule
    = ToTerm(">")
    | ">="
    | "<"
    | "<="
    | "=="
    | "!=";

bitExpression.Rule
    = addExpression + bitOperator + bitExpression
    | addExpression;

bitOperator.Rule
    = ToTerm("|")
    | "&"
    | "^";

addExpression.Rule
    = multiplyExpression + addOperator + addExpression
    | prefixExpression;

addOperator.Rule
    = ToTerm("+") | "-";

multiplyExpression.Rule
    = prefixExpression + multiplyOperator + multiplyExpression
    | prefixExpression;

multiplyOperator.Rule
    = ToTerm("*")
    | "/";

prefixExpression.Rule
    = prefixOperator + factor
    | factor;

prefixOperator.Rule = ToTerm("!");

factor.Rule
    = identifierExpression + parenArguments
    | identifierExpression
    | number
    | parenExpression;

identifierExpression.Rule
    = identifier
    | identifierExpression + "." + identifier;

Last, the keywords are defined for the My C language. This is necessary for the syntax coloring. All terminals in the keyword list will be displayed as keywords in the code editor for Visual Studio.

this.MarkReservedWords("break", "continue", "else", "extern", "for", 
                       "if", "int", "return", "static", "void", "while");

Testing the My C Grammar

There are two options for testing the grammar: Grammar Explorer and Visual Studio Experimental Hive. The next two sections are a walkthrough in setting up the environment for either option.

Grammar Explorer

One of the many nice things about Irony is that it includes a tool for testing Irony grammars, called Grammar Explorer.

To setup the environment, right-click the language service project and select Properties. Select the Debug tab and choose Start External Program, and point it to the Irony.GrammarExplorer executable in the Resources directory.

Visual Studio - Project Properties

The first time Grammar Explorer is run, the grammar will need to be added. To add a grammar into Grammar Explorer, click the button next to the drop down and click Add Grammar from the menu.

Grammar Explorer - Add Grammar

Find the language service DLL and click Open.

Grammar Explorer - Open Grammar

The grammar should appear in the list and be selected, so the only thing to do here is click OK.

Grammar Explorer - Select Grammar

Once the grammar is added, it can be selected from the drop down. Now, it will display the grammar errors.

Grammar Explorer - Grammar Errors

The shift-reduce conflict on the else is in the original YACC grammar, and is caught by MPPG when the ManagedMyC example is compiled. This is a common problem with C grammars. Here is an example demonstrating the conflict:

if (x>y) if (x<z) foo(); else bar();

The compiler faces the problem of deciding whether to reduce "if (x<z) foo();" to an if_statement and then shift the "else", or if the "else" should be shifted first: both would be equally valid under the grammar as stated, but reduction would result in this:

if (x>y)
{
  if (x<z)
  {
    foo();
  }
}
else
{
  bar();
}

... associating the "else" with the first "if", whereas shifting the "else" produces...

if (x>y)
{
  if (x<z)
  {
    foo();
  }
  else
  {
    bar();
  }
}

... with the "else" associated with the second "if".

Irony provides a way to resolve this issue, which is presented below. However, if left alone, the grammar will still perform correctly. This is because the default behaviour of Irony is to shift, which produces the correct result in C and similar languages.

Visual Studio Experimental Hive

To setup the environment, right-click the language service project and select Properties. Select the Debug tab and choose Start External Program, and point it to the Visual Studio 2008 executable (default: C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe). Also, add /ranu /rootsuffix Exp to the command line arguments.

Visual Studio - Project Properties

Build and run the project in Visual Studio (F5). This will launch the experimental hive. To test the language service, open a file with the .myc extension, or create a text file and save it with the .myc extension. Two files were included in the demo project: short.myc and thing.myc.

Visual Studio - My C Programming Language

Grammar Additions\Changes

This section covers additions or changes made to the My C sample grammar to demonstrate additional features of Irony.

Possible Non-terminal Duplication Warning

This warning is displayed anytime a non-terminal's rule contains a single non-terminal. There are several ways to resolve this issue. First, consider whether the non-terminal is really necessary. If the non-terminal should be there, the issue can be resolved by merging the two non-terminals.

The My C grammar has a program non-terminal that is causing this warning. This non-terminal was included from the original YACC file, and was probably placed there for expansion. To resolve the warning, the program and the declarations non-terminals are merged. Note that the program non-terminal must follow declarations so that the assignment is made after its rule is initialized.

this.Root = program;

declarations.Rule = MakeStarRule(declarations, declaration);

//Must follow declarations so that assignment 
//is made after rule is initialized.
program.Rule = declarations.Rule; 

Shift-reduce Conflicts

Irony includes a method for resolving shift-reduce conflicts, PreferShiftHere(). It allows you to tell the compiler where to shift. So, the error from earlier can be resolved by the following:

statement.Rule
    = semiStatement + ";"
    | "while" + parenExpressionAlways + statement
    | "for" + forHeader + statement
    | "if" + parenExpressionAlways + statement
    | "if" + parenExpressionAlways + statement + 
      PreferShiftHere() + "else" + statement;

Number Prefixes and Suffixes

Irony also supports number prefixes and suffixes. For example, the C language uses the prefix 0x to specify hexadecimal numbers. It also uses the f suffix to designate a value as a float. This can be defined in Irony with the following:

NumberLiteral number = new NumberLiteral("number");
number.AddPrefix("0x", NumberFlags.Hex);
number.AddSuffixCodes("f", TypeCode.Single);

Registering Brace Pairs

In YACC, brace pairs are matched for each rule.

Block
    : OpenBlock CloseBlock              {Match(@1, @2); }
    | OpenBlock BlockContent CloseBlock {Match(@1, @3); }
    ;

In Irony, it is a little different. Any terminal that should be matched in Visual Studio is registered in the grammar with the function RegisterBracePair. The My C grammar registers parenthesis and curly braces.

this.RegisterBracePair("{", "}");
this.RegisterBracePair("(", ")");

Operator Precedence

Another strength of Irony is the ability to define operator precedence. The My C language gives a higher precedence to the multiply and divide operators.

this.RegisterOperators(1, "+", "-");
this.RegisterOperators(2, "*", "/");

Points of Interest

When I first discovered Visual Studio language services, I used the Managed Babel System. After working with a YACC grammar, I quickly found that it was difficult to debug and would be hard to maintain in the future. Irony seems to solve both of these issues, and performs just as well. Irony also uses BNF expressions, so it was a very easy transition from YACC.

Articles to Come

  • Part 2 - Building your First Isolated Shell
  • Part 3 - Building your First Custom Project System
  • Part 4 - Building your First Advanced Language Service
  • Part 5 - Building your First Microsoft Build Task
  • Part 6 - Building your First Custom Debug Engine

History

  • December 10, 2009
    • Updated article and download content to work with the alpha release of Irony (October 13, 2009). Changes to the source code were made by Thaddeus Ryker.
  • February 19, 2009
    • Rewrote article to take more of a step-by-step approach.
    • Included the Irony Language Service Wizard.
    • Used the My C language as the sample, instead of My C#.
    • Included Irony.GrammarExplorer.exe.
  • February 10, 2009
    • Initial version.

License

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

About the Author

Ben Morrison
Software Developer
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   
GeneralRun AuthoringScope.GetMethodsmembermykhayloZober22 Sep '09 - 7:52 
Hi,
I have created simple Package. I would like to create a simple IntelliSense.
I create and implement class derived from AuthoringScope using some examples.
Now, only method GetDeclarations running when I test my project.
What can I do to run GetMethods method with GetDeclarations?
 
Best Regards,
Mykhaylo
 
Mykhaylo

GeneralRe: Run AuthoringScope.GetMethodsmemberBen Morrison23 Sep '09 - 4:30 
Hi Mykhaylo,
 
There is actually a long discussion about this here[^].
GeneralRe: Run AuthoringScope.GetMethodsmembersmbika21 Jan '10 - 9:47 
And I have checked it out and they do not explain how either...if they DO it is cleverly hidden in a lot of cut and pasted code - none of which is useful to me...
 
Firefly, Stargate, Farscape: in that order

GeneralRe: Run AuthoringScope.GetMethodsmemberBen Morrison21 Jan '10 - 10:03 
Its been awhile since I have read that post, but I am pretty sure the answer is there. Unfortunately, I don't have time to dig through the post at the moment to point you to a particular location. I do remember Dimitry explaining the issue and offering a fix. Sorry I cannot offer any more assistance.
GeneralRe: Run AuthoringScope.GetMethodsmembersmbika22 Jan '10 - 3:56 
Thanks for the reply! I will pore over it some more...I have set all the triggers correctly (although I may not be using a comma as a delimiter for parameters, i have set it to the default to keep things simple for now) but nothing happens...
 
Firefly - no more need be said

GeneralRe: Run AuthoringScope.GetMethodsmembersmbika22 Jan '10 - 4:58 
The page in question mentions the solution is to call the 4 functions StartName, StartParameter, NextParameter and EndParameter in the ParseSource function of the LanguageService Class...but does not say where, in what sequence and with what... Cry | :((
 
Firefly, Stargate, Farscape: in that order

QuestionUpdates?memberDaveX863 May '09 - 6:58 
Very cool project...Irony is very cool too. I've been struggling with Managed Babel for a while and just came across this the other day...wish I'd found all this sooner.
 
The Irony codebase has changed since you published this, are you planning on keeping your wizard updated? Does it live someplace on CodePlex or elsewhere? Do I just have to overwrite the irony.dll file?
 
Good stuff!
AnswerRe: Updates?memberBen Morrison12 May '09 - 2:41 
Sorry I have been really busy lately but I have been watching the progress of Irony. At some point I will need to update my work to include the latest changes, but that may be a couple of months from now. Until then it should work if you just replace the dll file. If it doesn't just let me know and I can try to help figure out the problem.
GeneralRe: Updates?memberDaveX8612 May '09 - 7:01 
No problem about the lag...nice to be busy Smile | :)
 
I had to set my project aside for a while as well as other things have come up so I should be going at it again this summer at some point...I'll check back then.
 
Thanks for getting back!
 
Dave
QuestionRe: Updates?memberThaddeus Ryker9 Dec '09 - 8:43 
I've been recently working with Irony and your article has been a great help. I did notice though that Irony has changed a lot since your article and replacing the reference to the Irony.dll no longer works. Do you think you might update this project soon for the latest Irony build? In any case thanks for a great article.
 
-[I traded my sanity for a railgun]-

AnswerRe: Updates?memberBen Morrison9 Dec '09 - 10:50 
Currently, I am still using the older dll. Now that a new alpha version has been released, I may look into it sometime in the near future. The changes required to update this article to the latest version of Irony should be minimal.
GeneralRe: Updates?memberThaddeus Ryker9 Dec '09 - 10:48 
I have attempted to update the code that your wizard generates so that I could link it to a current assembly of Irony and so far I think it's working, I'd be happy to shoot you the changes I made if you're interested. They may not be 100% correct, but perhaps it could save you a little bit of time updating it. Big Grin | :-D
If anyone else is interested I'd be happy to send you copies of my modified files as well. You should be able to overwrite the generated ones, change your reference and you'll be cooking with gas! I should state though that I'm rather new to parser building so you might want to do a diff btwn the old files and my new ones, for your own piece of mind Wink | ;)
 
-[I traded my sanity for a railgun]-

GeneralRe: Updates?memberBen Morrison12 Dec '09 - 8:39 
Thanks for the code. I have incorporated it and updated the article. Your changes were very helpful.
Generaloriginal MyC errorsmemberAlex.Herz24 Mar '09 - 23:30 
Hi,
 
I'm trying to add my own language to VS2008. Compiling the managed MyC LangService I get the following:
 
C:\Programme\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Tools\bin\MPLex.exe "/out:obj\Debug\lexer.cs" "Generated\lexer.lex"
"C:\Programme\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Tools\bin\MPPG.exe" -mplex Generated\parser.y > obj\Debug\parser.cs
Reduce/Reduce conflict, state 21: 35 vs 73 on error
Shift/Reduce conflict, state 22 on error
Shift/Reduce conflict, state 24 on error
Shift/Reduce conflict, state 31 on '('
Shift/Reduce conflict, state 51 on '('
Shift/Reduce conflict, state 106 on KWELSE
 
Is that ok or is the version of yacc/lexx I have installed wrong?
 
Also I'm having problems setting breakpoints. With the langage service installed the breakpoints are always forced to be on the first line of the .myc file. Without the service I can set them anywhere (I'm using the managed myc service together with the textinterpreter and myc compiler/package sample ported from vs2005 sdk because my language needs a custom debug engine as it is running on a custom VM).
 
I just installed the latest yacc/lexx I could find, as MPlex wouldn't work without them at all. Any help would be very much appreciated.
 
Thx,
Alex
GeneralRe: original MyC errorsmemberBen Morrison6 Apr '09 - 10:50 
The reduce/reduce conflict is because the program and declarations non-terminals need to be combined.
 
The shift/reduce can be ignored and happens because of the ambiguity in if/else statements.
 
Additional detail can be found in the article.
 
I am using Irony instead of MPLEX and MPPG but I remember having to do a custom build action to compile the lex and yacc files. There is a how-to video on the VSX site that explains this.
 
To fix the breakpoint issue, place the following code in your LanguageService class.
 
public override int ValidateBreakpointLocation(IVsTextBuffer buffer, int line, int col, TextSpan[] pCodeSpan)
        {
            if (pCodeSpan != null)
            {
                pCodeSpan[0].iStartLine = line;
                pCodeSpan[0].iStartIndex = col;
                pCodeSpan[0].iEndLine = line;
                pCodeSpan[0].iEndIndex = col;
                if (buffer != null)
                {
                    int length;
                    buffer.GetLengthOfLine(line, out length);
                    pCodeSpan[0].iStartIndex = 0;
                    pCodeSpan[0].iEndIndex = length;
                }
                return VSConstants.S_OK;
            }
            else
            {
                return VSConstants.S_FALSE;
            }
        }
It is also in the demo solution that is available for download.
 
Ben
GeneralRe: original MyC errorsmemberAlex.Herz6 Apr '09 - 21:17 
Thx,
 
this helps me a lot and saves a lot of time having to guess what's going on! Smile | :)
GeneralExcellentmemberDaniel Flower27 Feb '09 - 3:52 
I've used and written articles on Irony, and always wondered about Visual Studio integration, so I thought this article was excellent. Nice one!
GeneralRe: ExcellentmemberBen Morrison27 Feb '09 - 9:14 
Thanks for the feedback!
 
I have read your articles on DSLs using Irony and thought they were great. In fact, that is part of the reason why I decided to write this article. I wanted to share what I was able to accomplish using Irony. Irony is a great tool that simplifies the whole process in defining a language.
GeneralIncredible....memberJosemaproject25 Feb '09 - 5:01 
My vote of 5.
 
Thanks for the article.
Greetings.
Josema.
press releases archive
online training
GeneralGood workmemberNorm .net24 Feb '09 - 20:46 
5!
 


Software Kinetics
- Moving software


GeneralVery nice work man!memberdave.dolan15 Feb '09 - 18:40 
This is a priceless example. And look, you just made all the Queensland University cats jealous!
GeneralRe: Very nice work man!memberBen Morrison16 Feb '09 - 17:19 
Thanks! I have to say Roman Ivantsov, the creator of Irony, deserves some credit as well. He made several code changes to Irony to make the Visual Studio integration straightforward.
 
Also, keep watching the article. I am almost finished with an update that will make the process even easier!
GeneralRe: Very nice work man!memberdave.dolan16 Feb '09 - 17:23 
I'm also a fan of Roman's as well. Truly the combination of his new no-code-gen paradigm and its implied end-run around the YACCisms normally required for similar products will be a huge step forward in language creation/translation efforts for a long time. If nothing else, it proves that it can be done. I wonder what Terrance Parr would think of this.

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

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 11 Dec 2009
Article Copyright 2009 by Ben Morrison
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid