Click here to Skip to main content
Email Password   helpLost your password?

Introduction

This article presents an enhanced version of the CodeBox of my previous article. It now features line numbering and more efficient rendering. Only the visible text is now rendered. In addition to the decorations of word coloring, highlighting, strikethroughs, and underlining, there is now support for decoration schemes that serve as a base for further decorations. For instance, we can now have a C# decoration scheme and then add highlighting on top of that.

The sample application is a simple text editor that supports decoration schemes and text coloring. It also allows you to take snapshots of the displayed code as image files. I was looking to make a simple but at least somewhat useful app to show off what the improved CodeBox can do.

The Big Ideas

CodeBox is an enhanced version of the WPF textbox, allowing a greater degree of visual customization, while keeping as much of the original textbox functionality as possible. It comes from the following observations:

Background

This CodeBox is a major revision and upgrade of the one presented in my CodeBox article. Although this version is much improved and definitely the one I would advise you to use for your purposes, reading the original and examining its code should make this one easier to understand.

In creating this control, I found that a significant amount of the documentation on the TextBox that I would have liked to read was nonexistent. Hopefully, this will cover that deficit. I will be using the following format:

Member Name MSDN Definition
Disassembled code from Reflector

Commentary

GetFirstVisibleLineIndex: Returns the line index for the first line that is currently visible in the text box.

public int GetFirstVisibleLineIndex()
{
    if (base.RenderScope == null)
    {
        return -1;
    }
    double lineHeight = this.GetLineHeight();
    return (int) Math.Floor((double) ((base.VerticalOffset / lineHeight) + 0.0001));
}

The first thing that we should notice is the line testing the RenderScope property. The Renderscope is set as part of the building of the Visual tree. As far as I have been able to ascertain, the RenderScope property will always be null in design mode. A Unit Test like the following will always fail:

[Test]
public void LineCount_Test()
{
    TextBox tx = new TextBox();
    tx.Text = "1\n2";
    tx.Height = 200;
    tx.Width = 200;
    Assert.AreEqual(2, tx.LineCount);
}

This and many variations will fail because the RenderScope is null. It is also worth noting that the private private GetLineheight method depends only on the font size and the font family. There is no parameter to represent leading.

LineCount: Gets the total number of lines in the text box.

[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int LineCount
{
    get
    {
        if (base.RenderScope == null)
        {
            return -1;
        }
        return (this.GetLineIndexFromCharacterIndex
			(base.TextContainer.SymbolCount) + 1);
    }
}

The LineCount is simply the line at the end of the text. This is the number of lines which appear in the textbox, and thus changes with the textbox™s width. This method cannot be depended upon. Immediately after a change in the Text property, it will take the value 0. Be careful as this means that it cannot be called in response to the TextChanged event.

There is no built-in function for the Minimum Line Count, setting RexrWrapping to None should give the same results. The following function will work, and serves to illustrate the rules used to determine line numbers:

public int MinLineCount(TextBox tx)
{
    string str = tx.Text;
    // \r\n represents one, not two linebreaks
    str = str.Replace("\r\n", "\n"); //Make all line delimiters 1 character
    char[] chars = str.ToCharArray();
    int breakCount = 0;
    //lineBreaks are the list of special character that cause an additional line
    char[] lineBreaks = {  Convert.ToChar("\r"),  
                           Convert.ToChar("\n"),  
                           Convert.ToChar("\f"),  
                           Convert.ToChar("\v" )};
    for (int i = 0; i < str.Length; i++)
    {
        if (lineBreaks.Contains(chars[i]))
        {
            breakCount++;
        }
    }
    return breakCount + 1;
}

GetLastVisibleLineIndex: Returns the line index for the last line that is currently visible in the text box.

public int GetLastVisibleLineIndex()
{
    if (base.RenderScope == null)
    {
        return -1;
    }
    double extentHeight = 
      ((IScrollInfo) base.RenderScope).ExtentHeight;//Last line visible box
    if ((base.VerticalOffset + base.ViewportHeight) >= extentHeight)
    {
        return (this.LineCount - 1);
    }
    return (int) Math.Floor((double) (((base.VerticalOffset + 
            base.ViewportHeight) - 1.0) / this.GetLineHeight()));
}

GetLastVisibleLineIndex is rather undependable. After changes to text and especially key presses, it will take the value of -1. It is capable of failing even if LineCount > 0.

We see that there are two cases for the last visible line index. The last line of text could appear in the viewport in which the index is determined by the LineCount. Otherwise, it is computed from the Lineheight, VerticalOffset, and ViewportHeight. Note that this is the index of the last, at least partially visible, line, rather than the index of the last fully visible line.

GetRectFromCharacterIndex: Overloaded. Returns the bounding rectangle for the character at the specified character index.

public Rect GetRectFromCharacterIndex(int charIndex, bool trailingEdge)
{
    Rect rect;
    if ((charIndex < 0) || (charIndex > base.TextContainer.SymbolCount))
    {
        throw new ArgumentOutOfRangeException("charIndex");
    }
    TextPointer insertionPosition = base.TextContainer.CreatePointerAtOffset(charIndex, 
        LogicalDirection.Backward).GetInsertionPosition(LogicalDirection.Backward);
    if (trailingEdge && (charIndex < base.TextContainer.SymbolCount))
    {
        insertionPosition = 
          insertionPosition.GetNextInsertionPosition(LogicalDirection.Forward);
        Invariant.Assert(insertionPosition != null);
        insertionPosition = 
          insertionPosition.GetPositionAtOffset(0, LogicalDirection.Backward);
    }
    else
    {
        insertionPosition = 
          insertionPosition.GetPositionAtOffset(0, LogicalDirection.Forward);
    }
    this.GetRectangleFromTextPositionInternal(insertionPosition, true, out rect);
    return rect;
}

 public Rect GetRectFromCharacterIndex(int charIndex)
{
    return this.GetRectFromCharacterIndex(charIndex, false);
}

GetLineIndexFromCharacterIndex: Returns the zero-based line index for the line that contains the specified character index.

public int GetLineIndexFromCharacterIndex(int charIndex)
{
    if (base.RenderScope != null)
    {
        Rect rect;
        if ((charIndex < 0) || (charIndex > base.TextContainer.SymbolCount))
        {
            throw new ArgumentOutOfRangeException("charIndex");
        }
        TextPointer position = base.TextContainer.CreatePointerAtOffset(charIndex, 
                               LogicalDirection.Forward);
        if (this.GetRectangleFromTextPositionInternal(position, false, out rect))
        {
            rect.Y += base.VerticalOffset;
            return (int) ((rect.Top + (rect.Height / 2.0)) / this.GetLineHeight());
        }
    }
    return -1;
}

GetLineText: Returns the text that is currently displayed on the specified line.

public string GetLineText(int lineIndex)
{
    if (base.RenderScope == null)
    {
        return null;
    }
    if ((lineIndex < 0) || (lineIndex >= this.LineCount))
    {
        throw new ArgumentOutOfRangeException("lineIndex");
    }
    TextPointer startPositionOfLine = this.GetStartPositionOfLine(lineIndex);
    TextPointer endPositionOfLine = this.GetEndPositionOfLine(lineIndex);
    if ((startPositionOfLine != null) && (endPositionOfLine != null))
    {
        return TextRangeBase.GetTextInternal(startPositionOfLine, endPositionOfLine);
    }
    return this.Text;
}

The Code

Rendering

This version of the CodeBox only renders the text that would be currently visible, for greater efficiency. The textbox provides a few methods that can be used to determine the text that is visible and its position. There are two main wrinkles though. First, the methods used to get this information do not function in the designer. Secondly, the methods cannot be depended on. When they fail to give values, which usually occurs when the text is changing, the control will have to repeat the last render and then try the render again later.

In order to render the text, we need to know two things: the text to use, and how much it is scrolled up or down. The visible text is created as follows:

private  string VisibleText
{
    get
    {
        if (this.Text == "") { return ""; }
        string visibleText = "";
        try
        {
            int textLength = Text.Length;
            int firstLine = GetFirstVisibleLineIndex();
            int lastLine = GetLastVisibleLineIndex();

            int lineCount = this.LineCount;
            int firstChar = 
               (firstLine == 0) ? 0 : GetCharacterIndexFromLineIndex(firstLine);

            int lastChar = GetCharacterIndexFromLineIndex(lastLine) + 
                           GetLineLength(lastLine) - 1;
            int length = lastChar - firstChar + 1;
            int maxlenght = textLength - firstChar;
            string text =  Text.Substring(firstChar, Math.Min(maxlenght, length));
            if (text != null)
            {
                visibleText = text;
            }
        }
        catch
        {
            Debug.WriteLine("GetVisibleText failure");
        }
    return    visibleText;
    }
}

We get the first character of the first visible line, the last character of the last visible line, and the text must be everything that lies between them. It should be noted that GetFirstVisibleLineIndex and GetLastVisibleLineIndex are on the generous side. Lines can be declared visible even if some of their allotted space is visible, though they are undetectable to the human eye.

The other thing that we need to know in order to render the text is the location we will use to render from.

private Point GetRenderPoint(int firstChar)
{
    try
    {
        Rect cRect = GetRectFromCharacterIndex(firstChar);
        Point  renderPoint = new Point(cRect.Left, cRect.Top);
        if (!Double.IsInfinity(cRect.Top))
        {
            renderinfo.RenderPoint = renderPoint;
        }
        else
        {
             this.renderTimer.IsEnabled = true;
        }
        return renderinfo.RenderPoint;
    }
    catch
    {
        this.renderTimer.IsEnabled = true;
        return renderinfo.RenderPoint;
    }
}

This code seems bloated. The calculation requires only two lines.

Rect cRect = GetRectFromCharacterIndex(firstChar);
Point  renderPoint = new Point(cRect.Left, cRect.Top);

The origin for rendering purposes is the top left corner of the first visible character. The rest of the code exists to handle the fact that the textbox is not ready to give that information all the time. I suspect that things are being handled asynchronously under the hood, but the code we see in Reflector is pretty hairy.

Finally, we come to the main method of the CodeBox control: OnRenderRuntime.

protected void OnRenderRuntime(DrawingContext drawingContext)
{
   drawingContext.PushClip(new RectangleGeometry(new Rect(0, 0, this.ActualWidth, 
                           this.ActualHeight)));//restrict drawing to textbox
   drawingContext.DrawRectangle(CodeBoxBackground, null, new Rect(0, 0, 
                  this.ActualWidth, this.ActualHeight));//Draw Background
   if (this.Text == "") return;

       int firstLine = GetFirstVisibleLineIndex();// GetFirstLine();
       int firstChar = (firstLine == 0) ? 0 : 
           GetCharacterIndexFromLineIndex(firstLine);// GetFirstChar();
       string visibleText = VisibleText;
       if (visibleText == null) return;

       Double leftMargin = 4.0 + this.BorderThickness.Left;
       Double topMargin = 2.0 + this.BorderThickness.Top;

       formattedText = new FormattedText(
              this.VisibleText,
               CultureInfo.GetCultureInfo("en-us"),
               FlowDirection.LeftToRight,
               new Typeface(this.FontFamily.Source),
               this.FontSize,
               BaseForeground);  //Text that matches the textbox's
       formattedText.Trimming = TextTrimming.None;

       ApplyTextWrapping(formattedText);

               Pair visiblePair = new Pair(firstChar, visibleText.Length);
               Point renderPoint =   GetRenderPoint(firstChar);
               
                //Generates the prepared decorations for the BaseDecorations
               Dictionary<EDecorationType, Dictionary<Decoration, 
                          List<Geometry>>> basePreparedDecorations 
                   = GeneratePreparedDecorations(visiblePair, 
                     DecorationScheme.BaseDecorations);
               //Displays the prepared decorations for the BaseDecorations
               DisplayPreparedDecorations(drawingContext, 
                              basePreparedDecorations, renderPoint);

               //Generates the prepared decorations for the Decorations
               Dictionary<EDecorationType, Dictionary<Decoration, 
                          List<Geometry>>> preparedDecorations 
                   = GeneratePreparedDecorations(visiblePair, mDecorations);
               //Displays the prepared decorations for the Decorations
               DisplayPreparedDecorations(drawingContext, 
                      preparedDecorations, renderPoint);

               //Colors According to Scheme
               ColorText(firstChar, DecorationScheme.BaseDecorations);
               ColorText(firstChar, mDecorations);//Colors According to Decorations
               drawingContext.DrawText(formattedText, renderPoint);

               if (this.LineNumberMarginWidth > 0)
               //Are line numbers being used
               {
                   //Even if we gey this far it is still 
                   //possible for the line numbers to fail
                   if (this.GetLastVisibleLineIndex() != -1)
                   {
                       FormattedText lineNumbers = GenerateLineNumbers();
                       drawingContext.DrawText(lineNumbers, new Point(3, 
                                               renderPoint.Y));
                       renderinfo.LineNumbers = lineNumbers;
                   }
                   else
                   {
                       drawingContext.DrawText(renderinfo.LineNumbers, 
                                               new Point(3, renderPoint.Y));
                   }
               }

           //Cache information for possible renderer
            renderinfo.BoxText = formattedText;
            renderinfo.BasePreparedDecorations = basePreparedDecorations;
            renderinfo.PreparedDecorations = preparedDecorations;
       }

I've looked at this code so long that it seems self explanatory, but here are the bullet points of what it does:

Line Numbers

Adding line numbers first requires that we alter the ControlTemplate for the CodeBox and then generate the line numbers.

LineNumber XAML

In order to add a margin to the text box, the control part of the ControlTemplate was changed from:

<Border BorderThickness="{TemplateBinding Border.BorderThickness}" 
   BorderBrush="{TemplateBinding Border.BorderBrush}"
   Background="{TemplateBinding Panel.Background}" 
   Name="Bd" SnapsToDevicePixels="True">
     <ScrollViewer Name="PART_ContentHost" 
        SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />

to:

<Border BorderThickness="{TemplateBinding Border.BorderThickness}" 
    BorderBrush="{TemplateBinding Border.BorderBrush}"
    Background="{TemplateBinding Panel.Background}" 
    Name="Bd" SnapsToDevicePixels="True" > 
        <Grid Background="Transparent"  >
                <Grid.ColumnDefinitions>
                    <ColumnDefinition 
                       Width="{Binding Path =  LineNumberMarginWidth, 
                              RelativeSource={RelativeSource Templatedparent}, 
                              Mode=OneWay}" />
                    <ColumnDefinition  Width ="*"/>  
                </Grid.ColumnDefinitions>
           <ScrollViewer Name="PART_ContentHost" 
               SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" 
               Grid.Column="1" />
            </Grid></Border >

The most important thing to note is that the Mode for the width of the line number column is OneWay. Leaving it at the default (TwoWay) gives poor performance. We can also run into some trouble with our control templates not being recognized. Creating and then modifying a user control is an odd but effective workaround.

Generating the Line Numbers

There are two distinct line numbering scenarios: wrapping and no wrapping. In both cases, a string is created with \n characters to separate the lines. This is then used to create a formatted text object representing the line numbers. Without wrapping, creating this string is rather simple.

int firstLine = GetFirstVisibleLineIndex();  
int lastLine = GetLastVisibleLineIndex(); 
StringBuilder sb = new StringBuilder();
for (int i = firstLine; i <= lastLine; i++)
{
    sb.Append((i + StartingLineNumber) + "\n");
}
string lineNumberString = sb.ToString();

The case with wrapping is more involved. It involves three methods:

The textbox is wider than any of the lines of text. When the box is thinner than the width of the longest line, then wrapping occurs, and there are additional elements in VisibleLineStartCharcterPositions that are not in MinLineStartCharcterPositions. All of the elements in MinLineStartCharcterPositions should appear in VisibleLineStartCharcterPositions plus some additional ones. We should then be able to go down the VisibleLineStartCharcterPositions, checking for the matches in MinLineStartCharcterPositions and thus get the line numbers. If there is interest, please comment and I will add a more detailed explanation.

The LineNumbers method uses the merge algorithm, which is reasonably efficient for working with sorted lists. The method is about 70 lines long, so I will resist putting it here. It is amply documented though.

Using the Code

For all of the 900+ lines of code in the CodeBox class, it is after all just a TextBox. Once the appropriate namespaces and references are added, it can just be used. The two caveats are that you should use the CodeBoxBackground for the Background, and the BaseForeground for the ForeGround. If you do not want line numbers, set the LineNumberMarginWidth to 0. The DecorationSchemes class contains two incomplete decoration schemes, one for SQL Server 2008 and one for C# 3.0. They can serve as good examples of how to define decorations.

Sample Application - ColoredWordPad

For a sample application, I created a very simple word processor that allows us to place decorations on the text. Because I wanted to make it useful, it has a nontrivial feature. The text displayed can be exported to an image file, without the line for the insertion point. Again, if anyone is interested, let me know and I will add a more detailed explanation.

Updates

10/9/2009

Conclusion

Version 1 of the CodeBox should be thought of as a proof of concept, while this version should probably be considered an alpha release for a control. Hopefully, this one will prove good enough to serve in your applications.

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralDude, you rock!
gregbacchus
20:55 13 Jan '10  
Great control!
GeneralHow to make text bold
Matinca Florin
8:07 24 Nov '09  
Hi Ken,

It is possible to make a selection bold ?

Thanks.
GeneralBackground problem
Matinca Florin
7:01 24 Nov '09  
Hi Ken,

I have some code running on the TextChanged event and there I add an ExplicitDecoration to color a portion of the text.

After that I change the background color but after I do that, the whole text is hidden or has the same color as background.

There is a solution to change color only for a part of text and also change the background ?

Thanks.
GeneralRe: Background problem
Matinca Florin
8:06 24 Nov '09  
I soleved this by setting the background color on CodeBoxBackground property.
GeneralRe: Background problem
KenJohnson
11:03 25 Nov '09  
Hi,
I hope this is helpfull. This control works by rendering text underneath the text that is normally rendered by the textbox. It is for that reason that both the ForeColor and the Background need to be transparent. I would have prefered to have rendered it above but there are significant challenges to doing that.
Thanks
Ken
QuestionUsing from a Visual Basic project (2008)
derek9999
6:52 15 Oct '09  
Hi,
I'm really interested in using this control for my project but I can't create an instance of the codebox control of my form. I'm just newing up the control with:

Dim textBox1 As New CodeBoxControl.CodeBox()


and then assign some text to it, but the control is invisible (or not there). I'm sure I'm missing something pretty fundamental!
AnswerRe: Using from a Visual Basic project (2008)
KenJohnson
11:01 15 Oct '09  
Are you trying to use this in a WPF App or a windows Form app. If you give me some more details I will put up some sample code.

Thanks
Ken
GeneralRe: Using from a Visual Basic project (2008)
derek9999
1:02 16 Oct '09  
Hi,
It's just a windows form app. I made sure that the references are there but I can't see the control when I run the app.

Thanks.
GeneralRe: Using from a Visual Basic project (2008)
KenJohnson
18:58 16 Oct '09  
Hi Derek
I uploaded a very simple sample application, WinFormsHostingCodeBox.rar There is both a vb and a C# version. Please let me know how it works.

Thanks
Ken
GeneralRe: Using from a Visual Basic project (2008)
derek9999
2:40 17 Oct '09  
Hi Ken,
Thanks for the sample code, I got my simple test app working fine. The only problem is there's quite a bit of screen flicker while I'm typing, is there some double buffering (i'm guessing here) or something that I need to enable? By the way, the sample code you sent me flickers too, but your main demo doesn't.

In case you're interested, here's the project I want to use your control for : www.ajordison.co.uk

Regards,
Arthur.
GeneralRe: Using from a Visual Basic project (2008)
KenJohnson
11:22 17 Oct '09  
Hi Arthur,
What features of the CodeBox were you using. I have an older winforms version (actually more than one) if that would be of help.
Thanks
Ken
GeneralRe: Using from a Visual Basic project (2008)
derek9999
9:49 18 Oct '09  
Hi Ken,
Basically I only want the syntax highlighting for BASIC and assembly programs, so I need to be able to configure the control to use my own decoration schemes. They are both quite small instruction sets though. The line numbers would be nice but it's not a must-have! I don't need any of the other 'fancier' features.

Cheers,
Arthur.
GeneralUsing from Silverlight
AndrusM
7:59 13 Oct '09  
How to use this in Silverlight application ?

Andrus.

Andrus

GeneralRe: Using from Silverlight
KenJohnson
11:10 15 Oct '09  
Silverlight is a completely different beast. A large portion of the methods and properties used in the CodeBox do not exist. I have though of making a Silverlight version, but it requires actual calculation of the text wrapping, making it a significant project.

Thanks
Ken
GeneralRe: Using from Silverlight
AndrusM
23:13 15 Oct '09  
For starting point, can we create simplified version for Silverlight which contains features which can easily immplemented ?
E.q witohut text wrapping.

Andrus.

Andrus

GeneralRe: Using from Silverlight
slyi
4:57 25 Oct '09  
Try something like http://cid-289eaf995528b9fd.skydrive.live.com/self.aspx/Public/XamlBox.zip[^]
GeneralRe: Using from Silverlight
AndrusM
8:58 26 Oct '09  
I run sample but this does not allow editing.

How to enable editing in this sample ?

Andrus

GeneralRe: Using from Silverlight
slyi
12:06 29 Oct '09  
See editable sample on http://forums.silverlight.net/forums/t/138293.aspx[^]
GeneralRe: Using from Silverlight
KenJohnson
14:32 29 Oct '09  
Thanks for all the interest, I'll make up a Silverlight version. In a day or so my WinForms CodeBox will be up. After that I will start on the Silverlight one.
Thanks
Ken Red faced
NewsI blogged about using Ken Johnson's TextBox trick to achieve shadowed text
Dale Barnard
9:16 25 Sep '09  
I blogged about using Ken Johnson's TextBox trick to achieve shadowed text:

http://dalebarnardonwpf.wordpress.com/2009/09/25/customizing-wpf-textbox/[^]

Thanks,

Dale
AnswerRe: I blogged about using Ken Johnson's TextBox trick to achieve shadowed text
KenJohnson
19:04 9 Oct '09  
This is really inspiring to hear. I put up code in the hope that people will go out and use it for stuff, preferably things that never crossed my mind (like the shadowed text). I sent a slightly updated version of the CodeBox to be put up.
Thanks
Ken
GeneralSeconding Biegal's request
Bob Bedell
6:57 5 Sep '09  
I will definitely be using this approach a lot in code library apps. Was wondering, if you have a moment at some point, if you could indicate how one might go about adding tab key support, or even a tab indent setting in the UI (ala VS). As far as I can tell, CodeBox2 can't accept tab key presses. Tried a variety of key combinations.

Thanks again for this,

Bob
GeneralRe: Seconding Biegal's request
KenJohnson
12:22 11 Sep '09  
Thanks for the interest int the CodeBox.
The CodeBox inherits from the TextBox so it has the textbox's properties. In particular the AcceptsTab property is still there. I tried, and when it is set to true, tabs are accepted. I hope this answers your question.

If what you are working on is not confidential I would be glad to see it.
Thanks
Ken
GeneralRe: Seconding Biegal's request
Bob Bedell
15:45 11 Sep '09  
Thanks Ken,

Simple and exactly what I was looking for.

I'd be happy to send you the code library app I'm working on. However, I don't have A WPF version yet. The current version is a Windows Forms version that uses a RichTextBox control (that doesn't support syntax highlighting) to display code snippets.

The code snippets are stored in a SQL Server database. They are loaded into the app as nodes of a TreeView control. The TreeView nodes themselves support editing and drag-and-drop, and are searchable.
A TreeNode ContextMenu allows edits/deletes, or adds a new folder or code snippet to the TreeView hierarchy. All TreeNode modifications are written to the db.

Selecting a TreeNode displays its associated code snippet in the RichTextBox.

A search dialog accepts criteria, and displays the results in a ListView control. Double-clicking a search result item expands the TreeView to the appropriate node, and displays the appropriate code snippet.

Its a work in progress, and I don't have the good fortune of doing this stuff full-time. I've assembled most of the ingrdients for a WPF version - hopefully using MVVM - and have just begun work on it. I'll be sure and send you a copy when I integrate CodeBox.

Meanwhile, if you'd like the Windows Forms version, let me know how to send it to you, and I'd be happy to.

Bob
Generalindent size (tab size)
Biegal
9:19 14 Aug '09  
Hi!

It`s a great piece of code, and a great idea especially.
But is there any way to change the indent size (tab size) ?
I`m writing kind of xml editor, and after few levels, indents
takes a lot of editing space.

Thanks in advance!


Last Updated 10 Oct 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010