Click here to Skip to main content
13,560,164 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

274.2K views
31 bookmarked
Posted 19 Dec 2011
Licenced CPOL

Easy Way to Change Collation of All Database Objects in SQL Server

, 19 Dec 2011
Rate this:
Please Sign up or sign in to vote.
How to change collation of all database objects in SQL Server

Have you encountered a problem where you wanted to change your database collation to default or even just change it to a different type? I guess what you had initially done (like me) was to change the collation of the database.

Well, that does not quite work well as the existing columns will not be changed and retain its current collation type, only the newly created objects will use this new collation type. So you are left to the option of changing the columns one at a time by going to the column property and restoring it to default, or choosing the collation type you want.

Well, that’s great if you need to change 10 columns or less but what if you want to change the whole database? What if it’s a primary key or a foreign key? Well, isn’t that a nightmare? Well, I will give you an easy solution and all you need to do is to run 6 easy steps. If you don’t want to recreate the database and pump data by using DTS or SSIS, then this is the solution for you; just make sure to backup and restore everything before making any changes.

Step 1: Prepare your DB and Change the Collation to Your Desired One

Like I had said, backup your database as part of the preparation. Once that’s done, change your collation to the desired type by going to the database properties by right clicking on the database and choosing properties. Once you are on the Properties window, choose options and you can see the collation from there, choose what you want, then hit OK. This will ensure that new objects created will be using the new collation.

Step 2: Create Your Change Collation Script

Next is to create a script to change the collation of every object in your database. You need to use information_schema to extract columns needed to be changed and from there, we run a loop on all objects creating alter scripts on each item. Since it is a collation change, we will only need fields that use character types and text types. What you need is to have a lot of commands similar to this:

ALTER TABLE TABLENAME ALTER COLUMN COLUMNNAME varchar(100) COLLATE Latin1_General_CI_AS NULL

So here is the code to generate that:

OPEN MyTableCursor

FETCH NEXT FROM MyTableCursor INTO @TableName
WHILE @@FETCH_STATUS = 0
    BEGIN
        DECLARE MyColumnCursor Cursor
        FOR 
        SELECT COLUMN_NAME,DATA_TYPE, CHARACTER_MAXIMUM_LENGTH,
            IS_NULLABLE from information_schema.columns
            WHERE table_name = @TableName AND  (Data_Type LIKE '%char%' 
            OR Data_Type LIKE '%text%') AND COLLATION_NAME <> @CollationName
            ORDER BY ordinal_position 
        Open MyColumnCursor

        FETCH NEXT FROM MyColumnCursor INTO @ColumnName, @DataType, 
              @CharacterMaxLen, @IsNullable
        WHILE @@FETCH_STATUS = 0
            BEGIN
            SET @SQLText = 'ALTER TABLE ' + @TableName + ' ALTER COLUMN [' + @ColumnName + '] ' + 
              @DataType + '(' + CASE WHEN @CharacterMaxLen = -1 THEN 'MAX' ELSE @CharacterMaxLen END + 
              ') COLLATE ' + @CollationName + ' ' + 
              CASE WHEN @IsNullable = 'NO' THEN 'NOT NULL' ELSE 'NULL' END
            PRINT @SQLText 

        FETCH NEXT FROM MyColumnCursor INTO @ColumnName, @DataType, 
              @CharacterMaxLen, @IsNullable
        END
        CLOSE MyColumnCursor
        DEALLOCATE MyColumnCursor

FETCH NEXT FROM MyTableCursor INTO @TableName
END
CLOSE MyTableCursor
DEALLOCATE MyTableCursor

Run it, then save the script for later use. Let us call the script “ChangeCollation.sql”.

If you don’t have relationships, primary keys, and foreign keys, then you don’t need to do the next step.

Step 3: Create a Stored Procedure to Script Indexes and Relationships

Well, if you have relationships, primary keys, and foreign keys, then that’s a good practice but you need to script them as you need to drop those before changing the collation. Initially, I thought I can do this with the wizard and choose to script indexes but it does not create on its own the table creation is always included so with a little help from Google, I don’t have to write a single piece of code. I found this really good script to do it and I got it from  here.

I only separated the Create Indexes and Drop Indexes as we need to run a process in the middle.

Here is the Create Index script, courtesy of Adam Machanic:

/*
Script Table Keys
(C) 2010 Adam Machanic - amachanic@gmail.com
http://sqlblog.com/blogs/adam_machanic/archive/2010/04/04/
       rejuvinated-script-creates-and-drops-for-candidate-keys
       -and-referencing-foreign-keys.aspx
This script produces a script of all of the candidate keys (primary keys or unique 
constraints) as well as referencing foreign keys, for the target table. To use, put
SSMS into "results in text" mode and run the script. The output will be a formatted
script that you can cut and paste to use elsewhere.

Don't forget to configure the maximum text size before using. The default is 256
characters--not enough for many cases.

Tools->Options->Query Results->Results to Text->Maximum number of characters->8192
*/
CREATE PROC [dbo].[ScriptCreateTableKeys]
    @table_name SYSNAME
AS
BEGIN
    SET NOCOUNT ON

    --Note: Disabled keys and constraints are ignored
    --TODO: Drop and re-create referencing XML indexes, FTS catalogs

    DECLARE @crlf CHAR(2)
    SET @crlf = CHAR(13) + CHAR(10)
    DECLARE @version CHAR(4)
    SET @version = SUBSTRING(@@VERSION, LEN('Microsoft SQL Server') + 2, 4)
    DECLARE @object_id INT
    SET @object_id = OBJECT_ID(@table_name)
    DECLARE @sql NVARCHAR(MAX)

    IF @version NOT IN ('2005', '2008')
    BEGIN
        RAISERROR('This script only supports SQL Server 2005 and 2008', 16, 1)
        RETURN
    END

    SET @sql = '' +
        'SELECT ' +
            'CASE ' +
                'WHEN 1 IN (i.is_primary_key, i.is_unique_constraint) THEN ' +
                    '''ALTER TABLE '' + ' +
                        'QUOTENAME(OBJECT_SCHEMA_NAME(i.object_id)) + ''.'' + ' +
                        'QUOTENAME(OBJECT_NAME(i.object_id)) + @crlf + ' +
                    '''ADD '' + ' +
                        'CASE k.is_system_named ' +
                            'WHEN 0 THEN ''CONSTRAINT '' + QUOTENAME(k.name) + @crlf ' +
                            'ELSE '''' ' +
                        'END + ' +
                    'CASE k.type ' +
                        'WHEN ''UQ'' THEN ''UNIQUE'' ' +
                        'ELSE ''PRIMARY KEY'' ' +
                    'END + '' '' + ' +
                    'i.type_desc  + @crlf + ' +
                    'kc.key_columns + @crlf ' +
                'ELSE ' +
                    '''CREATE UNIQUE '' + i.type_desc + '' INDEX '' + ' +
                        'QUOTENAME(i.name) + @crlf + ' +
                    '''ON '' + ' +
                        'QUOTENAME(OBJECT_SCHEMA_NAME(i.object_id)) + ''.'' + ' +
                        'QUOTENAME(OBJECT_NAME(i.object_id)) + @crlf + ' +
                    'kc.key_columns + @crlf + ' +
                    'COALESCE ' +
                    '( ' +
                        '''INCLUDE '' + @crlf + ' +
                        '''( '' + @crlf + ' +
                            'STUFF ' +
                            '( ' +
                                '( ' +
                                    'SELECT ' +
                                    '( ' +
                                        'SELECT ' +
                                            ''','' + @crlf + '' '' + QUOTENAME(c.name) AS [text()] ' +
                                        'FROM sys.index_columns AS ic ' +
                                        'JOIN sys.columns AS c ON ' +
                                            'c.object_id = ic.object_id ' +
                                            'AND c.column_id = ic.column_id ' +
                                        'WHERE ' +
                                            'ic.object_id = i.object_id ' +
                                            'AND ic.index_id = i.index_id ' +
                                            'AND ic.is_included_column = 1 ' +
                                        'ORDER BY ' +
                                            'ic.key_ordinal ' +
                                        'FOR XML PATH(''''), TYPE ' +
                                    ').value(''.'', ''VARCHAR(MAX)'') ' +
                                '), ' +
                                '1, ' +
                                '3, ' +
                                ''''' ' +
                            ') + @crlf + ' +
                        ''')'' + @crlf, ' +
                        ''''' ' +
                    ') ' +
            'END + ' +
            '''WITH '' + @crlf + ' +
            '''('' + @crlf + ' +
                ''' PAD_INDEX = '' + ' +
                        'CASE CONVERT(VARCHAR, i.is_padded) ' +
                            'WHEN 1 THEN ''ON'' ' +
                            'ELSE ''OFF'' ' +
                        'END + '','' + @crlf + ' +
                'CASE i.fill_factor ' +
                    'WHEN 0 THEN '''' ' +
                    'ELSE ' +
                        ''' FILLFACTOR = '' + ' +
                                'CONVERT(VARCHAR, i.fill_factor) + '','' + @crlf ' +
                'END + ' +
                ''' IGNORE_DUP_KEY = '' + ' +
                        'CASE CONVERT(VARCHAR, i.ignore_dup_key) ' +
                            'WHEN 1 THEN ''ON'' ' +
                            'ELSE ''OFF'' ' +
                        'END + '','' + @crlf + ' +
                ''' ALLOW_ROW_LOCKS = '' + ' +
                        'CASE CONVERT(VARCHAR, i.allow_row_locks) ' +
                            'WHEN 1 THEN ''ON'' ' +
                            'ELSE ''OFF'' ' +
                        'END + '','' + @crlf + ' +
                ''' ALLOW_PAGE_LOCKS = '' + ' +
                        'CASE CONVERT(VARCHAR, i.allow_page_locks) ' +
                            'WHEN 1 THEN ''ON'' ' +
                            'ELSE ''OFF'' ' +
                        'END + ' +
                CASE @version
                    WHEN '2005' THEN ''
                    ELSE             
                        ''','' + @crlf + ' +
                        ''' DATA_COMPRESSION = '' + ' +
                            '( ' +
                                'SELECT ' +
                                    'CASE ' +
                                        'WHEN MIN(p.data_compression_desc) = 
                                          MAX(p.data_compression_desc) 
                                          THEN MAX(p.data_compression_desc) ' +
                                          'ELSE ''[PARTITIONS USE 
                                          MULTIPLE COMPRESSION TYPES]'' ' +
                                    'END ' +
                                'FROM sys.partitions AS p ' +
                                'WHERE ' +
                                    'p.object_id = i.object_id ' +
                                    'AND p.index_id = i.index_id ' +
                            ') '
                END + '+ @crlf + ' +
            ''') '' + @crlf + ' +
            '''ON '' + ds.data_space + '';'' + ' +
                '@crlf + @crlf COLLATE database_default AS [-- Create Candidate Keys] ' +
        'FROM sys.indexes AS i ' +
        'LEFT OUTER JOIN sys.key_constraints AS k ON ' +
            'k.parent_object_id = i.object_id ' +
            'AND k.unique_index_id = i.index_id ' +
        'CROSS APPLY ' +
        '( ' +
            'SELECT ' +
                '''( '' + @crlf + ' +
                    'STUFF ' +
                    '( ' +
                        '( ' +
                            'SELECT ' +
                            '( ' +
                                'SELECT ' +
                                    ''','' + @crlf + '' '' + QUOTENAME(c.name) AS [text()] ' +
                                'FROM sys.index_columns AS ic ' +
                                'JOIN sys.columns AS c ON ' +
                                    'c.object_id = ic.object_id ' +
                                    'AND c.column_id = ic.column_id ' +
                                'WHERE ' +
                                    'ic.object_id = i.object_id ' +
                                    'AND ic.index_id = i.index_id ' +
                                    'AND ic.key_ordinal > 0 ' +
                                'ORDER BY ' +
                                    'ic.key_ordinal ' +
                                'FOR XML PATH(''''), TYPE ' +
                            ').value(''.'', ''VARCHAR(MAX)'') ' +
                        '), ' +
                        '1, ' +
                        '3, ' +
                        ''''' ' +
                    ') + @crlf + ' +
                ''')'' ' +
        ') AS kc (key_columns) ' +
        'CROSS APPLY ' +
        '( ' +
            'SELECT ' +
                'QUOTENAME(d.name) + ' +
                    'CASE d.type ' +
                        'WHEN ''PS'' THEN ' +
                            '+ ' +
                            '''('' + ' +
                                '( ' +
                                    'SELECT ' +
                                        'QUOTENAME(c.name) ' +
                                    'FROM sys.index_columns AS ic ' +
                                    'JOIN sys.columns AS c ON ' +
                                        'c.object_id = ic.object_id ' +
                                        'AND c.column_id = ic.column_id ' +
                                    'WHERE ' +
                                        'ic.object_id = i.object_id ' +
                                        'AND ic.index_id = i.index_id ' +
                                        'AND ic.partition_ordinal = 1 ' +
                                ') + ' +
                            ''')'' ' +
                        'ELSE '''' ' +
                    'END ' +
            'FROM sys.data_spaces AS d ' +
            'WHERE ' +
                'd.data_space_id = i.data_space_id ' +
        ') AS ds (data_space) ' +
        'WHERE ' +
            'i.object_id = @object_id ' +
            'AND i.is_unique = 1 ' +
            --filtered and hypothetical indexes cannot be candidate keys
            CASE @version
                WHEN '2008' THEN 'AND i.has_filter = 0 '
                ELSE ''
            END +
            'AND i.is_hypothetical = 0 ' +
            'AND i.is_disabled = 0 ' +
        'ORDER BY ' +
            'i.index_id '

    EXEC sp_executesql
@sql,
        N'@object_id INT, @crlf CHAR(2)',
        @object_id, @crlf

    SELECT
        'ALTER TABLE ' + 
            QUOTENAME(OBJECT_SCHEMA_NAME(fk.parent_object_id)) + '.' + 
            QUOTENAME(OBJECT_NAME(fk.parent_object_id)) + @crlf +
        CASE fk.is_not_trusted
            WHEN 0 THEN 'WITH CHECK '
            ELSE 'WITH NOCHECK '
        END + 
            'ADD ' +
                CASE fk.is_system_named
                    WHEN 0 THEN 'CONSTRAINT ' + QUOTENAME(name) + @crlf
                    ELSE ''
                END +
        'FOREIGN KEY ' + @crlf + 
        '( ' + @crlf + 
            STUFF
(
(
                    SELECT
(
                        SELECT 
                            ',' + @crlf + ' ' + QUOTENAME(c.name) AS [text()]
                        FROM sys.foreign_key_columns AS fc
                        JOIN sys.columns AS c ON
                            c.object_id = fc.parent_object_id
                            AND c.column_id = fc.parent_column_id
                        WHERE 
                            fc.constraint_object_id = fk.object_id
                        ORDER BY
                            fc.constraint_column_id
                        FOR XML PATH(''), TYPE
                    ).value('.', 'VARCHAR(MAX)')
                ),
                1,
                3,
                ''
            ) + @crlf + 
        ') ' +
        'REFERENCES ' + 
            QUOTENAME(OBJECT_SCHEMA_NAME(fk.referenced_object_id)) + '.' + 
            QUOTENAME(OBJECT_NAME(fk.referenced_object_id)) + @crlf +
        '( ' + @crlf + 
            STUFF
(
(
                    SELECT
(
                        SELECT 
                            ',' + @crlf + ' ' + QUOTENAME(c.name) AS [text()]
                        FROM sys.foreign_key_columns AS fc
                        JOIN sys.columns AS c ON
                            c.object_id = fc.referenced_object_id
                            AND c.column_id = fc.referenced_column_id
                        WHERE 
                            fc.constraint_object_id = fk.object_id
                        ORDER BY
                            fc.constraint_column_id
                        FOR XML PATH(''), TYPE
                    ).value('.', 'VARCHAR(MAX)')
                ),
                1,
                3,
                ''
            ) + @crlf + 
        ');' + 
            @crlf + @crlf COLLATE database_default AS [-- Create Referencing FKs]
    FROM sys.foreign_keys AS fk
    WHERE
        referenced_object_id = @object_id
        AND is_disabled = 0
    ORDER BY
        key_index_id

END

Step 4: Create a Stored Procedure to Script Drop Indexes and Relationships

Now you also need to create the drop scripts, this is the other half of Adam Machanic’s script.

CREATE PROC [dbo].[ScriptDropTableKeys]
    @table_name SYSNAME
AS
BEGIN
    SET NOCOUNT ON

    --Note: Disabled keys and constraints are ignored
    --TODO: Drop and re-create referencing XML indexes, FTS catalogs

    DECLARE @crlf CHAR(2)
    SET @crlf = CHAR(13) + CHAR(10)
    DECLARE @version CHAR(4)
    SET @version = SUBSTRING(@@VERSION, LEN('Microsoft SQL Server') + 2, 4)
    DECLARE @object_id INT
    SET @object_id = OBJECT_ID(@table_name)
    DECLARE @sql NVARCHAR(MAX)

    IF @version NOT IN ('2005', '2008')
    BEGIN
        RAISERROR('This script only supports SQL Server 2005 and 2008', 16, 1)
        RETURN
    END

    SELECT
        'ALTER TABLE ' + 
            QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + 
            QUOTENAME(OBJECT_NAME(parent_object_id)) + @crlf +
        'DROP CONSTRAINT ' + QUOTENAME(name) + ';' + 
            @crlf + @crlf COLLATE database_default AS [-- Drop Referencing FKs]
    FROM sys.foreign_keys
    WHERE
        referenced_object_id = @object_id
        AND is_disabled = 0
    ORDER BY
        key_index_id DESC

    SET @sql = '' +
        'SELECT ' +
            'statement AS [-- Drop Candidate Keys] ' +
        'FROM ' +
        '( ' +
            'SELECT ' +
                'CASE ' +
                    'WHEN 1 IN (i.is_unique_constraint, i.is_primary_key) THEN ' +
                        '''ALTER TABLE '' + ' +
                            'QUOTENAME(OBJECT_SCHEMA_NAME(i.object_id)) + ''.'' + ' +
                            'QUOTENAME(OBJECT_NAME(i.object_id)) + @crlf + ' +
                        '''DROP CONSTRAINT '' + QUOTENAME(i.name) + '';'' + ' +
                            '@crlf + @crlf COLLATE database_default ' +
                    'ELSE ' +
                        '''DROP INDEX '' + QUOTENAME(i.name) + @crlf + ' +
                        '''ON '' + ' +
                            'QUOTENAME(OBJECT_SCHEMA_NAME(object_id)) + ''.'' + ' +
                            'QUOTENAME(OBJECT_NAME(object_id)) + '';'' + ' +
                                '@crlf + @crlf COLLATE database_default ' +
                'END AS statement, ' +
                'i.index_id ' +
            'FROM sys.indexes AS i ' +
            'WHERE ' +
                'i.object_id = @object_id ' +
                'AND i.is_unique = 1 ' +
                --filtered and hypothetical indexes cannot be candidate keys
                CASE @version
                    WHEN '2008' THEN 'AND i.has_filter = 0 '
                    ELSE ''
                END +
                'AND i.is_hypothetical = 0 ' +
                'AND i.is_disabled = 0 ' +
        ') AS x ' +
        'ORDER BY ' +
            'index_id DESC '

    EXEC sp_executesql 
@sql,
        N'@object_id INT, @crlf CHAR(2)',
        @object_id, @crlf

END

Step 5: Bringing Them All Together

Now you have the two Stored Procedures, all you have to do is to loop though all the tables in your database and pass that as the parameter of the Stored Procedure. First, we use ScriptCreateTableKeys.

DECLARE @TableName nvarchar(255)
DECLARE MyTableCursor Cursor
FOR 
SELECT name FROM sys.tables WHERE [type] = 'U' and name <> 'sysdiagrams' ORDER BY name 
OPEN MyTableCursor

FETCH NEXT FROM MyTableCursor INTO @TableName
WHILE @@FETCH_STATUS = 0
    BEGIN
    EXEC ScriptCreateTableKeys @TableName

    FETCH NEXT FROM MyTableCursor INTO @TableName
END
CLOSE MyTableCursor
DEALLOCATE MyTableCursor

Then let us use ScriptDropTableKeys:

DECLARE @TableName nvarchar(255)
DECLARE MyTableCursor Cursor
FOR 
SELECT name FROM sys.tables WHERE [type] = 'U' and name <> 'sysdiagrams' ORDER BY name 
OPEN MyTableCursor

FETCH NEXT FROM MyTableCursor INTO @TableName
WHILE @@FETCH_STATUS = 0
    BEGIN
     EXEC ScriptDropTableKeys @TableName

    FETCH NEXT FROM MyTableCursor INTO @TableName
END
CLOSE MyTableCursor
DEALLOCATE MyTableCursor

Just make sure when you execute them, output the results as text so you can easily copy and paste the results. Save the first results as “CreateKeysAndIndexes.sql” and the second as “DropKeysAndIndexes.sql”.

Step 6: Run Your Saved Scripts

In this order, run your scripts and wait for the results, time wait might vary depending on your database size.

  1. DropKeysAndIndexes.sql
  2. ChangeCollation.sql
  3. CreateKeysAndIndexes.sql

License

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

Share

About the Author

Raymund Macaalay
Technical Lead
New Zealand New Zealand
http://nz.linkedin.com/in/macaalay
http://macaalay.com/

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionMissing schema support in Step 5 Pin
blakey_w22-May-18 21:57
memberblakey_w22-May-18 21:57 
GeneralMy vote of 4 Pin
Daniel Jann3-Nov-16 0:14
memberDaniel Jann3-Nov-16 0:14 
QuestionMissing features Pin
Member 1251471410-May-16 4:26
memberMember 1251471410-May-16 4:26 
QuestionGreat solution, but step 2 is missing some code? Pin
Member 1210072030-Oct-15 3:45
memberMember 1210072030-Oct-15 3:45 
AnswerRe: Great solution, but step 2 is missing some code? Pin
Member 1210072030-Oct-15 4:01
memberMember 1210072030-Oct-15 4:01 
QuestionAnd non-unique indexes? Pin
pgfiore19-Aug-15 23:40
memberpgfiore19-Aug-15 23:40 
SuggestionFix for database schemas Pin
DaveBoltman1-Jul-15 2:56
memberDaveBoltman1-Jul-15 2:56 
SuggestionApply to All databases in the one server Pin
Member 117551199-Jun-15 21:58
memberMember 117551199-Jun-15 21:58 
GeneralMy vote of 1 Pin
Member 99825083-Mar-15 4:32
memberMember 99825083-Mar-15 4:32 
QuestionSame thing for SQL Server 2012? Pin
Agent__0075-Mar-14 19:52
professionalAgent__0075-Mar-14 19:52 
SuggestionFix for the issue of nText columns being incorrectly assigned a size (changes in bold) Pin
Member 899056030-Sep-13 5:05
memberMember 899056030-Sep-13 5:05 
SuggestionSQL 2012 Pin
Member 1018022130-Jul-13 10:21
memberMember 1018022130-Jul-13 10:21 
Questiondo you really need to drop & recreate the indexes? Pin
MacGinitie16-Jul-13 7:39
memberMacGinitie16-Jul-13 7:39 
QuestionModified the scripts a little bit Pin
parpagnas11-Jun-13 10:18
memberparpagnas11-Jun-13 10:18 
GeneralGreat Thanks!! Pin
Member 82248878-May-13 1:35
memberMember 82248878-May-13 1:35 
GeneralRe: Great Thanks!! Pin
Daniel Jann2-Nov-16 23:59
memberDaniel Jann2-Nov-16 23:59 
QuestionGreat thanks Pin
db_developer6-Dec-12 10:47
memberdb_developer6-Dec-12 10:47 
AnswerRe: Great thanks Pin
parpagnas6-Jun-13 23:57
memberparpagnas6-Jun-13 23:57 
QuestionWhat about Check Constraints? Pin
Member 959983314-Nov-12 8:50
memberMember 959983314-Nov-12 8:50 
QuestionTo Step 2 this should help Pin
MiguelCouto21-May-12 23:27
memberMiguelCouto21-May-12 23:27 
AnswerRe: To Step 2 this should help Pin
parpagnas6-Jun-13 23:58
memberparpagnas6-Jun-13 23:58 

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

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

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.180527.3 | Last Updated 19 Dec 2011
Article Copyright 2011 by Raymund Macaalay
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid