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

A LINQ Tutorial: WPF Data Binding with LINQ to SQL

By , 11 Dec 2009
 

Note: Requires SQL Server Express 2008 and .NET 3.5 to run.

Book Catalog Application

Introduction

This is the final part of a three-part series on using LINQ to SQL:

These tutorials describe how to map your classes to your tables manually (rather than with an automated tool like SqlMetal) so that you can have support for M:M relationships and data binding against your entity classes. Even if you do choose to auto-generate your classes, understanding how these techniques work will allow you to expand the code to better fit your application's needs and to better troubleshoot issues when they arise.

The purpose of this final article is to complete the introduction to LINQ to SQL by showing how to make your LINQ to SQL classes work with WPF data bindings.

Getting Started

This article builds on top of: A LINQ Tutorial: Adding/Updating/Deleting Data, to add INotifyPropertyChanged events to your entity classes so they'll work with WPF's data binding. Please refer to the latest version of that article to see how the application has been setup.

Simple Data Binding

WPF data binding allows you to bind to any CLR object, including the classes you've mapped to your tables with LINQ to SQL. Let's start with a quick look at how it's used in the attached Book Catalog application.

The main window, BookCatalogBrowser.xaml, displays a list of book catalog items in a ListView named Listing (see the bottom of the file):

<ListView Name="Listing" 
   ItemsSource="{Binding}" 
   HorizontalContentAlignment="Stretch"/> 

The DisplayList() method in its code-behind takes a list of (any type of) items and sets Listing's DataContext to that list:

private void DisplayList( IEnumerable dataToList ) {
    Listing.DataContext = dataToList;
}

BookCatalogBrowser.xaml defines DataTemplates for each type (Book, Author, and Category) to define how they should be displayed. For example, here's part of the template for Category:

<!-- How to display Category listings -->
<DataTemplate DataType="{x:Type LINQDemo:Category}">
  <Border Name="border" BorderBrush="ForestGreen" 
         BorderThickness="1" Padding="5" Margin="5">
    <StackPanel>
      <TextBlock Text="{Binding Path=Name}" 
           FontWeight="Bold" FontSize="14"/>
      <ListView ItemsSource="{Binding Path=Books}" 
           HorizontalContentAlignment="Stretch" BorderThickness="0" >
        <ListView.ItemTemplate>
          <DataTemplate>
            <TextBlock>
              <Hyperlink Click="LoadIndividualBook" 
                    CommandParameter="{Binding}" 
                    ToolTip="Display book details">
                <TextBlock Text="{Binding Path=Title}"/></Hyperlink>
...

This is how each Category instance within Listing's list will be displayed. The DataTemplate itself is selected based on the data type (DataType="{x:Type LINQDemo:Category}"), and everything within the template is bound to an individual Category instance.

It displays that Category's data via bindings:

  • The Category's name: {Binding Path=Name}
  • The Category's list of books (for a ListView): {Binding Path=Books}
  • Each book's title (for an individual book in the ListView): {Binding Path=Title}

This results in a list of categories, each one displaying their name and list of books:

Category Listing

Updating the Display When the Data Changes

This works great... until you try to update the data and it fails to get reflected in the UI. Even telling the data bindings to refresh fails to display any changes at all. Everything just seems broken.

The problem is that data binding requires the objects its bound to to provide notifications whenever they change. You can fix this by implementing the INotifyPropertyChanged interface on your classes.

Implementing INotifyPropertyChanged

In order for WPF data binding to automatically reflect data updates, the objects it binds to need to provide change notification to signal when their values have changed. The most common method for doing this is for your classes to implement the INotifyPropertyChanged interface to report changes to their data.

Implement Interface

In A LINQ Tutorial (Part 1), we created classes for Book, Author, and Category, and mapped them to their database tables.

Let's walk through adding the INotifyPropertyChanged interface to your classes using Book as an example.

1. Add the INotifyPropertyChanged interface to the class declaration

[Table( Name = "Books" )]
public class Book : INotifyPropertyChanged

2. Add a public PropertyChangedEventHandler that callers can use to register for change notifications

public event PropertyChangedEventHandler PropertyChanged;

3. Add the OnPropertyChanged method to notify callers of changes

This method will check to see if there is a PropertyChanged delegate and, if so, invokes that delegate, passing it the name of the field that has changed.

private void OnPropertyChanged( string name ) {
    if( PropertyChanged != null ) {
        PropertyChanged( this, new PropertyChangedEventArgs( name ) );
    }
}

Do this for each of your public entity classes (in our example: Book, Author, and Category).

You can skip the M:M Join classes as they're not part of your public interface, so you're probably not binding to them.

Call OnPropetyChanged() for Each Public [Column] Attribute

These are the public properties with a [Column] attribute, mapping them directly to a database column.

The [Column] public properties in Book are:

  • Title
  • Price

Call OnPropertyChanged from set() just after you set the value, passing it the name of the property that changed.

Always be sure to call OnPropetyChanged() after you've set the field. Otherwise, the caller will get the alert and check the field for its value before you've had a chance to update it.

If you've been using automatic properties as we have, you'll sadly need to split them into backing field + property:

private string _title;
[Column] public string Title {
    get { return _title; }
    set {
        _title = value;
        OnPropertyChanged( "Title" );
    }
}

private decimal _price;
[Column] public decimal Price {
    get { return _price; }
    set {
        _price = value;
        OnPropertyChanged( "Price" );
    }
}

Do this for all your public [Column] attributes. In BookCatalog, this would be:

  • Author: Name
  • Category: Name

If you have a public Id that's an identity column, you should be able to skip that since, by definition, it won't change for a given instance.

Call OnPropetyChanged() for Each Public Single Reference (1:M) [Association] Attribute

This is the singleton side of any 1:M relationship. For example, Book holds a single Category.

Category:Books M:1 Association

Call OnPropertyChanged() just after setting your EntityRef backing field to its new value.

For example, in our Book class' Category property, call it right after setting _category.Entity:

public Category Category {
    ...
    set {
            ...

            // set category to the new value
            _category.Entity = newCategory;
            OnPropertyChanged( "Category" );

...

Call OnPropertyChanged() for Each Public Collection (M:1 and M:M) [Association] Attribute

Collection associations (e.g., Book.Categories and Book.Authors) need to do two things:

  1. Return a collection that implements INotifyCollectionChanged.
  2. Call OnPropertyChanged() whenever the collection changes.

Step #1 is used by WPF when it binds to the collection to determine what has changed. For example, the list of Category.Books we bind to when displaying a Category's data.

Step #2 is used when WPF binds to your object to determine when it changes. For example, if Category.Name changes.

M:M Collection of Reference Associations

Books:Authors M:M Association

In A LINQ Tutorial (Part 2), we set up our M:M public classes (e.g., Book and Author) to return an ObservableCollection, which already implements INotifyCollectionChanged, so Step #1 is already done for Book (as shown below) and for Author:

public class Book : INotifyPropertyChanged
{
    ...

    public ICollection Authors {
        get {
          var authors = new ObservableCollection( from ba in BookAuthors select ba.Author );
          authors.CollectionChanged += AuthorCollectionChanged;
          return authors;
        }
    }

When you create the collection, you register to receive notifications for it (authors.CollectionChanged += AuthorCollectionChanged). This will call your AuthorCollectionChanged method whenever the collection is changed.

Update AuthorCollectionChanged to call OnPropertyChanged() at the end:

private void AuthorCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) {
    if( NotifyCollectionChangedAction.Add == e.Action ) {
        foreach( Author addedAuthor in e.NewItems ) {
            OnAuthorAdded( addedAuthor );
        }
    }

    if( NotifyCollectionChangedAction.Remove == e.Action ) {
        foreach( Author removedAuthor in e.OldItems ) {
            OnAuthorRemoved( removedAuthor );
        }
    }
    
   // Call OnPropertyChanged() after updating Authors
    OnPropertyChanged( "Authors" );
}

And then, mirror these changes on the other side of the M:M relationship (Author.Books).

M:1 Collection of Reference Associations

That leaves our M:1 collections - such as Category.Books - which we have not yet wrapped in an ObservableCollection.

So do that now: in Category, update the get() for Books to return an ObservableCollection just as you did for Book.Authors:

public class Category : INotifyPropertyChanged
{
    ...
    public ICollection Books {
        get {
            var books = new ObservableCollection<Book>( _books );
            books.CollectionChanged += BookCollectionChanged;
            return books;
        }

And, update its set() to call OnPropertyChanged() after assigning the value:

   set {
        _books.Assign( value );
        OnPropertyChanged( "Books" );
    }
}

In A LINQ Tutorial (Part 2), we created two delegate methods: OnBookAdded and OnBookRemoved, that handle synchronization whenever a Category's books change.

Create your BookCollectionChanged method. Have it invoke OnBookAdded and OnBookRemoved, and call OnPropertyChanged("Books") at the end:

private void BookCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) {
    if( NotifyCollectionChangedAction.Add == e.Action ) {
        foreach( Book addedBook in e.NewItems ) {
            OnBookAdded( addedBook );
        }
    }

    if( NotifyCollectionChangedAction.Remove == e.Action ) {
        foreach( Book removedBook in e.OldItems ) {
            OnBookRemoved( removedBook );
        }
    }
    OnPropertyChanged( "Books" );
}

Since you wrapped your backing field (_books), you need to handle updating it whenever you receive a change notification.

Update your OnBookAdded and OnBookRemoved methods to update the underlying _books collection accordingly:

private void OnBookAdded( Book addedBook ) {
    _books.Add( addedBook );
    addedBook.Category = this;
}

private void OnBookRemoved( Book removedBook ) {
    _books.Remove( removedBook );
    removedBook.Category = null;
}

Finally, since BookCollectionChanged is now invoking OnBookAdded and OnBookRemoved, it would be redundant (and recursive!) to also invoke it from _books.

Remove the Action arguments from your new EntitySet constructor call:

public Category( ){
    _books = new EntitySet<Book>( <del>OnBookAdded, OnBookRemoved )</del>;
}

Updating the Display When the Data Changes: Once More With Feeling

The attached BookCatalog application includes an EditDetails.xaml UserControl to allow editing any of the LINQ data types.

I'm sure I got entirely too clever for my own good here in trying to handle Books, and Authors and Categories using the same set of methods and UserControls - so I'm not suggesting you model your designs after what is here. :-) But I hope it serves its purpose by providing an example of how you can data bind to LINQ to SQL classes and make sure all of the data is synchronized throughout your application.

In EditDetails.xaml.cs, there is a BindDataToEditForm() that sets up the binding from the UserControl to the dataItem to edit (this could be a Book, Author, or Category):

private void BindDataToEditForm( ) {
    Binding binding = new Binding( );
    binding.Source = dataItem;
    binding.Mode = BindingMode.OneWay;
    binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
    EditForm.SetBinding( DataContextProperty, binding );
}

Like the main window, EditDetails.xaml uses DataTemplates to determine how to display the edit form. For example, here's part of the template for Category:

<!-- How to display Category details -->
<DataTemplate DataType="{x:Type LINQDemo:Category}">
  ...
  <Border Name="border" BorderBrush="ForestGreen" 
         BorderThickness="1" Padding="10" 
         DockPanel.Dock="Right">
    <StackPanel>
      <DockPanel>
        <TextBlock FontWeight="Bold" 
            DockPanel.Dock="Left" 
            VerticalAlignment="Center">Name:</TextBlock>
        <TextBox Text="{Binding Path=Name}" Margin="5 0" 
            VerticalAlignment="Center" DockPanel.Dock="Right"/>
      </DockPanel>
...

This is similar to the main window except it uses TextBoxes instead of TextBlocks to display the data, such as {Binding Path=Name}.

The difference is that since Category now implements INotifyPropertyChanged, when you edit Name in the UI, it automatically updates the underlying Category instance.

Edit Category

The Save button calls SaveDetails(), which calls SubmitChanges() on our DataContext:

private void SaveDetails( object sender, RoutedEventArgs e ) {
    BookCatalog.SubmitChanges();
    CloseDialog();
}

The Cancel button calls CancelUpdate() which:

  1. Does not submit changes on the DataContext, so those changes will be discarded as a new DataContext (BookCatalog) instance is created each time you open EditDetails.
  2. Does call the CancelChanges() method we added to BookCatalog in A LINQ Tutorial (Part 2) to cancel any pending changes to our M:M Join tables.
private void CancelUpdate( object sender, RoutedEventArgs e ){
    BookCatalog.CancelChanges( );
    CloseDialog( false );
}

When the dialog is closed, the main window, BookCatalogBrowser.xaml, gets a fresh DataContext instance to refresh its listing to pick up any changes that you might have saved.

A Known Issue

I discovered the following issue occurs if you choose to use a separate DataContext to delete your M:M Join records:

If you delete and then re-add the same M:M relationship within the same "transaction", you'll get a DuplicateKeyException.

For example, if you edit a Book and first remove an author and then re-add that same author before calling SubmitChanges():

BookCatalog bookCatalog = new BookCatalog( );
Book xpExplained = bookCatalog.Books.Single( 
  book => book.Title.Contains("Extreme Programming Explained") );
Author kentBeck = bookCatalog.Authors.Single( author => author.Name == "Kent Beck" );


xpExplained.Authors.Remove( kentBeck );
xpExplained.Authors.Add( kentBeck );

// This will throw a DuplicateKeyException
bookCatalog.SubmitChanges();

One way to handle this is to prevent that scenario from occurring -- don't allow a removed relationship to be re-added until after you call SubmitChanges() to persist the deletion. Then, you're free to add it back without error.

The Book Catalog application does this. If you open a Book to edit and delete one of its authors - you simply aren't given the choice to re-add that author until you click the Save button. The same is true on the other side when editing an Author to change which Books they have.

Note that this is only the case for M:M Join records that you remove with a separate DataContext, as described in A LINQ Tutorial (Part 2). There are no limitations to, for example, removing and then re-adding the same book to a Category since this is a M:1 (rather than M:M) relationship.

A Note on the Design

I purposefully chose to provide the View with direct access to the entity classes so that it would be as clear as possible of an example for how the bindings and DataContexts work in an application.

Obviously, in a real application, you'll want to put a layer between your view and the model. You can put your business logic there. You can also hide the details of working with the DataContext (e.g., when to refresh, when to call SubmitChanges()) there, so the view doesn't have to understand anything about LINQ to SQL.

The Model-View-ViewModel pattern is a great way to handle this. See Sacha Barber's MVVM Tutorials here on CodeProject.

Thank You!

If you read this far, wow, you should totally get yourself some of that new CodeProject reputation for that. Just sayin'... :-) Thanks for reading.

History

  • 12/09/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

Abby Fichtner (Hacker Chick)
Software Developer Microsoft
United States United States
Member
Abby Fichtner is a Microsoft Developer Evangelist and author of The Hacker Chick Blog.
 
She's been developing custom software applications, wearing every hat imaginable, since 1994. Although, technically, she got her start at the age of 8 when her father brought home an Atari 800. In the evenings, they would sit together and type in the machine code from the Atari magazines – because that was the way serious geeks got their computer games!
 
Today, she works for Microsoft as a Developer Evangelist to the startup community - helping them to create the next generation of software.

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   
Questionthrowing DuplicateKeyExceptionmemberDon Dausi12 May '13 - 20:51 
Still throws this error.This is because my DB has a table with primary key that auto-increments.
How do I resolve this.
Thanks in Advance
GeneralMy vote of 5memberCelPlusPlus3 May '13 - 4:10 
Good article. I particularly like the part "A Known Issue". Always good to know about possible problems before the occur.
QuestionGood tutorial on Linq to SQLmemberMember 985116021 Feb '13 - 9:21 
What a step by step teach yourself Linq to SQl tutorial using Visual C#.
Thanks, it really helped me.
GeneralMy vote of 5memberMember 985116021 Feb '13 - 9:16 
What an excellent tutorial on Linq to SQL it really helped me
GeneralMy vote of 5memberJosh Hawley2 Jan '13 - 10:14 
Excellent tutorial on LINQ! Thank you for writing it. It obviously took quite a bit of your time.
GeneralMy vote of 5memberVipin_Arora28 Apr '12 - 6:45 
Nice Info...
QuestionGreatmemberMember 867889326 Feb '12 - 5:57 
A great work
QuestionMy vote of 5memberAbinash Bishoyi14 Nov '11 - 5:52 
Great!!!
QuestionMulti-user environment?memberWPFNub30 Jun '11 - 19:08 
Great Article! Thank you!
 
I have a question as to applications in multi-user environment. How can Linq-SQL be used to ensure that the data being shown is current? Or will we still have to synchronize/refresh the data periodically somehow when the underlying data has changed on the sql server (e.g) ?
GeneralFantastic Article! Question on Association Tables thoughmemberworkindeveloper13 Mar '11 - 19:16 
Hi Hacker Chick!
 
I really love your article! It's very informative and concise at the same time. I'm working on a new project and this article has helped me immensely.
 
I do however have a question regarding exposing the Association Table. My Association table has a data member and so when I databind my user interface I'm using the ICollection of the Association table and not the ObservableCollection of just Author objects i.e. My code uses the equivalent of
private EntitySet<BookAuthor> _bookAuthors = new EntitySet<BookAuthor>( );
[Association( Name = "FK_BookAuthors_Books", Storage = "_bookAuthors", OtherKey = "bookId", ThisKey = "Id" )]
internal ICollection<BookAuthor> BookAuthors {
    get { return _bookAuthors; }
    set { _bookAuthors.Assign( value ); }
}
 
instead of
public ICollection<Author> Authors {
    get {
          var authors = new ObservableCollection<Author>(from ba in BookAuthors select ba.Author );
          authors.CollectionChanged += AuthorCollectionChanged;
          return authors;
        }
    }
 
That in itself is all fine. My problem is when I remove the mapping, the ICollection BookAuthors does seem to fire off any notifications to the UI to let it know that one or more items have been removed. I've tried changing ICollection to ObservableCollection but that seems to cause errors when SubmitChanges() executes.
 
Is there any way I can work around this so that a delete will automatically notify the UI?
GeneralMy vote of 5memberAminMaredia8 Feb '11 - 19:58 
very good article for object model wpf
GeneralDearmemberboobalan6 Jan '11 - 17:16 
You are rocking..
 
Really it is good article to understand about LINQ.
 

I am Very happy to Read your article.i hope you should carry forward to write these kind of article further..
 
Have a nice Year.. Happy New YearRose | [Rose] Rose | [Rose]
GeneralRe: DearmemberHacker Chick7 Jan '11 - 2:16 
Aww, thank you so much!
 
And I am thinking it's about time for some new articles... maybe around Win Phone 7? Stay tuned! Smile | :)
GeneralMy vote of 5memberboobalan6 Jan '11 - 17:14 
Really Super Buddy,
I wish you need to continue this kind of article further.
 
You are Rocking....
 

Have a nice Year....
QuestionGreat article - where next?memberJohn Stines5 Dec '10 - 3:34 
Hi Abbey,
 
First, thank you very much for taking the time to write such a fantastical series of articles. I find in genuinely heartening (from someone that works in financial services!) that people give up their own time to help others (especially those with knowledge far below their's) share in their knowledge - and clearly you thought through carefully the best way to structure this introduction to (to my mind) a very opaque subject matter.
 
I've been messing about with the linq to SQL tool not having the faintest how it was working underneath and now I think I might possibly just maybe know what's perhaps happening some of the time!
 
Which is huge progress but alas I think I need to read a lot more before I'll know enough for what I want to do.
 
From the article itself and the responses you've made in the comments section I thought I might try and appeal to your clearly very generous nature and request some advice from yourself.
At the moment I'm attempting to build my first real user application (having build hundreds of modules in VBA in Excel/Access and one or two in .Net) and I'm currently a bit stuck on these points
 
a) having UI update when rows are ADDED to a "linq to SQL" datatable eg how the Dickens does your application display an added Book eg in BookCatalogBrowser.xaml from EditDetails.xaml (is it just a sort of manual refresh using the Displaylist method in BookCatalogBrowser.xaml.cs)?
 
b) working out how to allow (for a multi-user system) for users to be informed when other users made changes to data they are viewing
 
Do you know of any reference material that would cover these (and databinding more generally with linq to SQL) in a way a dummy like me could follow?
 
Thanks in advance
 
John
GeneralDear Abby :)memberMember 32645628 Apr '10 - 10:10 
Abby, quite the presentation, isn't it.
The truth is I have not very often read anything, anywhere this constructive
and layered in such a well mannered way, to instruct someone into a complex
subject like this.
It is not the subject itself, but really the way you explain it all
which struck me as fantastic.
GeneralRe: Dear Abby :)memberHacker Chick30 Apr '10 - 3:37 
Thank you soooo much. That is so kind of you to say! Blush | :O I'm really glad you found it helpful!
 

abby
QuestionI used O/R designer - how do i get notified when data changes?memberAnna Tran26 Mar '10 - 12:41 
Hi,
 
I used O/R Designer built-in with VS2008 to auto-generate all the code to model my Database tables. I get something like this:
 
[Table(Name="dbo.Impact")]
public partial class Impact : INotifyPropertyChanging, INotifyPropertyChanged
{

private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
......
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
partial void OnImpact_LevelChanging(int value);
partial void OnImpact_LevelChanged();
partial void OnDescriptionChanging(string value);
partial void OnDescriptionChanged();
........
/* constructor*/ ....
/* getter and setter */
[Column(Storage="_Impact_Level", DbType="Int NOT NULL", IsPrimaryKey=true)]
public int Impact_Level
{
get
{
return this._Impact_Level;
}
set
{
if ((this._Impact_Level != value))
{
this.OnImpact_LevelChanging(value);
this.SendPropertyChanging();
this._Impact_Level = value;
this.SendPropertyChanged("Impact_Level");
this.OnImpact_LevelChanged();
}
}
}
 
.....
public event PropertyChangingEventHandler PropertyChanging;

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void SendPropertyChanging()
{
if ((this.PropertyChanging != null))
{
this.PropertyChanging(this, emptyChangingEventArgs);
}
}

protected virtual void SendPropertyChanged(String propertyName)
{
if ((this.PropertyChanged != null))
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
 
=====================================================
I also implemented a middle layer between my view and the auto-generated model. I have a ComponentRepository.cs and a Component.cs.
I've been trying to bind the data to the UI so that my UI will automatically update to reflect changes in the database with no success. By looking at your post, it sounds like I have to implement those partial functions, but i'm not sure if that will solve my issue. I also read about SQLDependancy/Query Notification/caching etc.... But still very confused.
 
Could you please help me out here?
GeneralCertainly helped mememberRugbyLeague15 Dec '09 - 0:22 
Thanks a lot
GeneralRe: Certainly helped mememberHacker Chick15 Dec '09 - 13:24 
Yay! So glad to hear that, thanks for letting me know! Smile | :)
GeneralGreat examplememberStephen Trinder14 Dec '09 - 16:36 
It's great that your example was for a complex set of tables.
Your article reflects a real-world situation, as compared to a lot of articles that only work at a simplistic level.
 
Nicely laid out too, and well explained.
Thanks!
GeneralRe: Great examplememberHacker Chick14 Dec '09 - 17:50 
Thanks, Stephen! I really appreciate your saying so! Cool | :cool:
 
abby
GeneralEnjoyed the presentationmemberrht34114 Dec '09 - 7:10 
As with the other articles in the series, I appreciated the time that was taking to explain everything.
 
Thank you for taking the time to write this series!
GeneralRe: Enjoyed the presentationmemberHacker Chick14 Dec '09 - 17:51 
Thank you so much! Big Grin | :-D
GeneralnicememberZoran Gelić13 Dec '09 - 11:06 
Well chicka(hacker) I like these series very much, I'm in process of learning these things and those articles are very helpful. Thanks for your effort Smile | :)

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 11 Dec 2009
Article Copyright 2009 by Abby Fichtner (Hacker Chick)
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid