Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Enterprise Library Wrapper Class to Implement Run-time Dynamic Connection Strings Without Using Config File

4.00/5 (3 votes)
2 Jul 2009CPOL4 min read 46.7K   875  
Enterprise Library wrapper class to implement run-time dynamic connection strings without using config file

Introduction

This is an Enterprise Library wrapper class to implement run-time dynamic connection strings without using config file.

Background

Enterprise Library out of the box recommends storing connection strings and provider information in a config file. Our applications generate connection strings dynamically during run-time. So I developed this enterprise library wrapper class to allow us to use the enterprise library with run-time dynamic connection strings.

Using the Code

Download the EntLibWrapper class and the Common functions.

The downloaded VB files can be included in a .NET project to use the EntLibWrapper class. I am giving below details as to how we can use the Enterprise Library wrapper class with run-time dynamic connection strings:

I use the following Enterprise Library wrapper class to use the library with run-time generated connection strings:

VB.NET
Imports Microsoft.Practices.EnterpriseLibrary.Data
Imports system.Data.Common
Imports system.Data.Odbc
Imports System.Data.OleDb
Imports System.Data.OracleClient
Imports System.Data.SqlClient

''' <summary>
''' This is a wrapper class for enterprise library.
''' </summary>
''' <remarks>
''' This is a wrapper class for enterprise library to avoid web.config 
''' and to make connectionStrings runtime configurable using Global.asax 
''' application variables which have the connectionStrings.
''' In Global.asax you can retrieve any passwords stored in web.config 
''' and assemble the connectionString
''' and store in application variables when the application starts.
''' These connection strings can then be passed in when this class 
''' is instantiated along with the required provider factory to use.
''' appSetting in web.config and connection strings stored in 
''' Application variables can be encrypted for security purposes.
''' </remarks>
Public Class EntLibWrapper

    Private db As Database
    Private dbCommand As DbCommand
    Private dbConn As DbConnection
    Private dbread As IDataReader
    Private conString As String
    Private dbProvider As enumProviderFactory
    Private dbRowsAffected As Integer
    Private dbIdentity As Integer

    ''' <summary>
    ''' Enumeration of the providers supported by the Enterprise Library wrapper class.
    ''' </summary>
    ''' <remarks>
    ''' This class currently supports only the following providers
    ''' i.e. all the providers supported by Enterprise Library - 
    ''' January 2006 and installed in our systems.
    ''' Others can be added on later as required.
    ''' </remarks>
    Public Enum enumProviderFactory
        ''' <summary>
        ''' System.Data.OleDb
        ''' </summary>
        ''' <remarks>Use for a OleDb connection.</remarks>
        OleDb = 1
        ''' <summary>
        ''' System.Data.SqlClient
        ''' </summary>
        ''' <remarks>Use for a SQL Server connection.</remarks>
        Sql = 2
        ''' <summary>
        ''' System.Data.Odbc
        ''' </summary>
        ''' <remarks>Use for an ODBC connection.</remarks>
        Odbc = 3
        ''' <summary>
        ''' System.Data.OracleClient
        ''' </summary>
        ''' <remarks>Use for an Oracle connection.</remarks>
        Oracle = 4
    End Enum

    ''' <summary>
    ''' Instantiate a new Enterprise Library wrapper.
    ''' </summary>
    ''' <param name="connectionString">
    ''' Pass in the connection string at run time.
    ''' The connection string can be stored by the user in 
    ''' Application / Session variables in an encrypted
    ''' format for security purposes. When calling this procedure pass 
    ''' in the decrypted connection string.
    ''' </param>
    ''' <param name="Provider">
    ''' Using the public enum enumProviderFactory choose which 
    ''' ProviderFactory you want to use.
    ''' </param>
    ''' <remarks>
    ''' User can store the password in appSettings in an 
    ''' encrypted format or retrieve the same
    ''' from a database to format the connection string at run-time dynamically.
    ''' </remarks>
    Public Sub New(ByVal connectionString As String, _
			ByVal Provider As enumProviderFactory)

        Select Case Provider
            Case enumProviderFactory.OleDb
                db = GetDatabase(connectionString, "System.Data.OleDb")
            Case enumProviderFactory.Sql
                db = GetDatabase(connectionString, "System.Data.SqlClient")
            Case enumProviderFactory.Odbc
                db = GetDatabase(connectionString, "System.Data.Odbc")
            Case enumProviderFactory.Oracle
                db = GetDatabase(connectionString, "System.Data.OracleClient")
        End Select
        conString = connectionString
        dbProvider = Provider

    End Sub

    ''' <summary>
    ''' To close the instance of this class.
    ''' </summary>
    ''' <remarks>
    ''' Always call the Close method after you are finished to free up all used resources.
    ''' </remarks>
    Public Sub Close()
        db = Nothing
        dbCommand = Nothing
        If Not dbread Is Nothing Then
            dbread.Close()
        End If
        If Not dbConn Is Nothing Then
            dbConn.Close()
        End If

    End Sub

    ''' <summary>
    ''' Gives access to all the other overloads in the 
    ''' Enterprise Library not exposed by this class.
    ''' </summary>
    ''' <value>
    ''' Database
    ''' </value>
    ''' <returns>
    ''' Returns a database.
    ''' </returns>
    ''' <remarks>
    ''' This will enable the user to use all the other methods and 
    ''' properties of the generic database which are not handled in this class 
    ''' - this class handles only the most frequently used
    ''' methods to make it simpler for use.
    ''' </remarks>
    Public ReadOnly Property Database() As Database
        Get
            Return db
        End Get
    End Property

    ''' <summary>
    ''' Retrieves the rows affected by a DoActionQuery.
    ''' </summary>
    ''' <value>Integer</value>
    ''' <returns>Returns an integer representing the rows affected 
    ''' after DoActionQuery.</returns>
    ''' <remarks>
    ''' You can retrieve the rows affected after a DoActionQuery.
    ''' </remarks>
    Public ReadOnly Property RowsAffected() As Integer
        Get
            Return dbRowsAffected
        End Get
    End Property

    ''' <summary>
    ''' Use to retrieve the id of the last row inserted after a DoActionQuery.
    ''' </summary>
    ''' <value>Integer</value>
    ''' <returns>Returns an integer representing the id of the 
    ''' last row inserted after a DoActionQuery.</returns>
    ''' <remarks>
    ''' You can retrieve the id of the last row inserted after a DoActionQuery.
    ''' <param></param>
    ''' The Identity property returns the @@IDENTITY for OleDb, 
    ''' the SCOPE_IDENTITY() for Sql and zero for the
    ''' other two providers, ODBC and Oracle.
    ''' <param></param>
    ''' For ODBC, setting this to zero as the ODBC connection string 
    ''' may be pointing to a text file for example.
    ''' <param></param>
    ''' For Oracle, there is no such thing as scope identity - 
    ''' have to use sequences, so returning zero for this too.
    ''' </remarks>
    Public ReadOnly Property Identity() As Integer
        Get
            Return dbIdentity
        End Get
    End Property

    ''' <summary>
    ''' This is a private function of this class. 
    ''' Provides the hook into the Enterprise Library.
    ''' </summary>
    ''' <param name="connectionString">The connection string to use 
    ''' for connecting to the database.</param>
    ''' <param name="dbProviderFactoryString">Based on the provider selected 
    ''' by the user the dbProviderFactoryString to use for the connection.</param>
    ''' <returns>A generic database</returns>
    ''' <remarks>
    ''' This is a private function of this class. Provides the hook 
    ''' into the Enterprise Library.
    ''' </remarks>
    Private Function GetDatabase(ByVal connectionString As String, _
		ByVal dbProviderFactoryString As String) As Database

        Return New GenericDatabase(connectionString, _
	System.Data.Common.DbProviderFactories.GetFactory(dbProviderFactoryString))

    End Function

    ''' <summary>
    ''' Pass in a sql string to get an IDataReader.
    ''' </summary>
    ''' <param name="sql">The sql string to use.</param>
    ''' <returns>An IDataReader</returns>
    ''' <remarks>
    ''' Depending on the provider factory chosen when the class was instantiated, 
    ''' the returned data reader is converted to the appropriate type 
    ''' - this is required for example to ensure that the HasRows method can be
    ''' used in OleDbDataReaders.
    ''' </remarks>
    Public Function GetDataReader(ByVal sql As String) As IDataReader

        dbCommand = db.GetSqlStringCommand(sql)
        Select Case dbProvider
            Case enumProviderFactory.OleDb
                dbread = CType(db.ExecuteReader(dbCommand), OleDbDataReader)
            Case enumProviderFactory.Sql
                dbread = CType(db.ExecuteReader(dbCommand), SqlDataReader)
            Case enumProviderFactory.Odbc
                dbread = CType(db.ExecuteReader(dbCommand), OdbcDataReader)
            Case enumProviderFactory.Oracle
                dbread = CType(db.ExecuteReader(dbCommand), OracleDataReader)
        End Select
        Return dbread

    End Function

    ''' <summary>
    ''' Pass in a sql string and tablename to get a dataset.
    ''' </summary>
    ''' <param name="sql">The sql string to use for getting the data.</param>
    ''' <param name="TableName">The tablename to assign for the table in the dataset.
    ''' </param>
    ''' <returns>A dataset</returns>
    ''' <remarks>Use when you need a new dataset; Use LoadDataSet 
    ''' if you want to load a new table
    ''' in an existing dataset.
    ''' </remarks>
    Public Function GetDataSet(ByVal sql As String, ByVal TableName As String) As DataSet

        Dim ds As DataSet = New DataSet
        ds = db.ExecuteDataSet(CommandType.Text, sql)
        ds.Tables(0).TableName = TableName
        Return ds

    End Function

    ''' <summary>
    ''' Pass in a sql string, an existing dataset (ByRef) 
    ''' and the name of the table to be added to the dataset.
    ''' </summary>
    ''' <param name="sql">The sql string to use for getting the data.</param>
    ''' <param name="ds">An existing database to which the data     
    ''' is to be added as a table.</param>
    ''' <param name="TableName">The table name to use when adding 
    ''' the data to the dataset.</param>
    ''' <remarks>
    ''' Use when you want to load a new table to an existing dataset. 
    ''' Use GetDataSet when you need a new dataset.
    ''' </remarks>
    Public Sub LoadDataSet(ByVal sql As String, ByRef ds As DataSet, _
						ByVal TableName As String)

        db.LoadDataSet(CommandType.Text, sql, ds, New String() {TableName})

    End Sub

    ''' <summary>
    ''' Pass in a sql string and tablename to get a datatable.
    ''' </summary>
    ''' <param name="sql">The sql string to use for getting the data.</param>
    ''' <param name="TableName">The tablename to assign for the returned datatable.
    ''' </param>
    ''' <returns>A DataTable</returns>
    ''' <remarks>Use to get a DataTable. See all GetTableWithZero method.</remarks>
    Public Function GetDataTable(ByVal sql As String, _
			ByVal TableName As String) As DataTable

        Dim ds As DataSet = New DataSet
        ds = db.ExecuteDataSet(CommandType.Text, sql)
        ds.Tables(0).TableName = TableName
        Return ds.Tables(TableName)

    End Function

    ''' <summary>
    ''' Use to get an IDbDataAdapter.
    ''' </summary>
    ''' <returns>Returns an IDbDataAdapter</returns>
    ''' <remarks>When you instantiated the class 
    ''' you have already passed in the connection string and
    ''' which provider to use - so you don't need to pass this info. again!
    ''' </remarks>
    Public Function GetDataAdapter() As IDbDataAdapter

        Return db.GetDataAdapter()

    End Function

    ''' <summary>
    ''' Pass in sql string containing an action query.
    ''' </summary>
    ''' <param name="sql">Sql string containing action query.</param>
    ''' <returns>Returns the word "Success" if the action query completed successfully;
    ''' If the action query did not complete successfully 
    ''' the return string will contain the error message.
    ''' </returns>
    ''' <remarks>
    ''' User to check return value and handle any errors as appropriate.
    ''' <param>
    ''' A template as follows is suggested for handling errors within a Try Catch block:
    ''' </param>
    ''' <param>
    ''' <code>
    '''        retval = ELW.DoActionQuery(sql)
    '''        If Not retval = "Success" Then
    '''             Err.Raise(513, , retval)
    '''             Exit Try
    '''        End If
    '''
    ''' </code>
    ''' </param>
    ''' Use the RowsAffected property to retrieve the number of rows 
    ''' affected after a DoActionQuery.
    ''' <param></param>
    ''' Use the Identity property to retrieve the id of the last row 
    ''' inserted after an insert query is executed.
    ''' <param></param>
    ''' The Identity property returns the @@IDENTITY for OleDb, 
    ''' the SCOPE_IDENTITY() for Sql and zero for the
    ''' other two providers, ODBC and Oracle.
    ''' <param></param>
    ''' For ODBC, setting this to zero as the ODBC connection string 
    ''' may be pointing to a text file for example.
    ''' <param></param>
    ''' For Oracle, there is no such thing as scope identity - 
    ''' have to use sequences, so returning zero for this too.
    ''' </remarks>
    Public Function DoActionQuery(ByVal sql As String) As String

        Dim retval As String
        Dim cmdGetIdentity As DbCommand

        Select Case dbProvider
            Case enumProviderFactory.OleDb
                dbConn = New OleDbConnection(conString)
                dbConn.Open()
                dbCommand = New OleDbCommand(sql, dbConn)
                cmdGetIdentity = New OleDbCommand()
                cmdGetIdentity.CommandText = "SELECT @@IDENTITY"
                cmdGetIdentity.Connection = dbConn
            Case enumProviderFactory.Sql
                dbConn = New SqlConnection(conString)
                dbConn.Open()
                dbCommand = New SqlCommand(sql, dbConn)
                cmdGetIdentity = New SqlCommand()
                cmdGetIdentity.CommandText = _
			"SELECT SCOPE_IDENTITY() AS [SCOPE_IDENTITY]"
                cmdGetIdentity.Connection = dbConn
            Case enumProviderFactory.Odbc
                dbConn = New OdbcConnection(conString)
                dbConn.Open()
                dbCommand = New OdbcCommand(sql, dbConn)
            Case enumProviderFactory.Oracle
                dbConn = New OracleConnection(conString)
                dbConn.Open()
                dbCommand = New OracleCommand(sql, dbConn)
        End Select

        dbRowsAffected = 0

        Try
            dbRowsAffected = dbCommand.ExecuteNonQuery()
            Select Case dbProvider
                Case enumProviderFactory.OleDb, enumProviderFactory.Sql
                    dbIdentity = cmdGetIdentity.ExecuteScalar().ToString()
                Case enumProviderFactory.Odbc, enumProviderFactory.Oracle
                    dbIdentity = 0
                    ' For ODBC setting this to zero as the ODBC connection string may be
                    ' pointing to a text file for example
                    ' For Oracle there is not such thing as scope identity - 
                    ' have to use sequences
                    ' so returning o for this too
            End Select
            retval = "Success" ' you can check for this in the string and do whatever
        Catch exc As Exception
            retval = "Error: " & exc.Message & "<br><br>" & _
			exc.InnerException.Message & "."
        Finally
            dbCommand = Nothing
            cmdGetIdentity = Nothing
            dbConn.Close()
        End Try

        Return retval

    End Function

    ''' <summary>
    ''' Pass in the SQLString, ValueField (first column) and TextField 
    ''' (second column) to get a DataTable with an additional zero record
    ''' containing "FirstDummyRecord" for the first column and an empty string 
    ''' for the second column.
    ''' </summary>
    ''' <param name="SQLString">The sql string to use for getting the data.</param>
    ''' <param name="ValueField">The name of the value field 
    ''' which will be assigned to the first column.</param>
    ''' <param name="TextField">The name of the text field which will be 
    ''' assigned to the second column.</param>
    ''' <returns>A DataTable</returns>
    ''' <remarks>
    ''' This function returns a data table including a first dummy record which is empty.
    ''' This data table can then be bound to a dropdown list to enable 
    ''' the user to unselect by choosing the first empty record;
    ''' Then in code we can check for "FirstDummyRecord"
    ''' and act appropriately. See all GetDataTable method.
    ''' </remarks>
    Public Function GetTableWithZero(ByVal SQLString As String, _
	ByVal ValueField As String, ByVal TextField As String) As DataTable

        Dim dtTable As DataTable = New DataTable
        Dim dr As DataRow

        Dim dcDescription As New DataColumn("description")
        Dim dcID As New DataColumn("id")
        dtTable.Columns.Add(dcID)
        dtTable.Columns.Add(dcDescription)

        ' Add zero record
        dr = dtTable.NewRow()
        dr(0) = "FirstDummyRecord"
        dr(1) = ""
        dtTable.Rows.Add(dr)

        dbread = db.ExecuteReader(CommandType.Text, SQLString)
        While (dbread.Read())
            dr = dtTable.NewRow()
            dr(0) = dbread(ValueField)
            dr(1) = dbread(TextField)
            dtTable.Rows.Add(dr)
        End While
        dbread.Close()
        Return dtTable
    End Function

End Class

The following helper functions are used to encrypt / decrypt appSettings in web.config as well as application variables which are used to assemble and store connection strings in memory during run-time dynamically:

VB.NET
Imports System.Configuration
Imports system.Web.Configuration
Imports Microsoft.Practices.EnterpriseLibrary.Security.Cryptography

Module Common

    Private Const symmProvider As String = "SelfServiceCryptoProvider"

    Public Sub ProtectSection(ByVal sectionName As String, _
		ByVal provider As String, ByVal VirtualPath As String)

        Dim config As Configuration = _
		WebConfigurationManager.OpenWebConfiguration(VirtualPath)
        Dim section As ConfigurationSection = config.GetSection(sectionName)

        If ((section.SectionInformation.IsProtected = False) AndAlso _
            (section.ElementInformation.IsLocked = False)) Then

            ' Protect (encrypt) the section.
            section.SectionInformation.ProtectSection(provider)

            ' Save the encrypted section.
            section.SectionInformation.ForceSave = True
            config.Save(ConfigurationSaveMode.Full)
            'config.Save()
        End If

    End Sub

    Public Sub UnProtectSection(ByVal sectionName As String, _
				ByVal VirtualPath As String)

        Dim config As Configuration = _
		WebConfigurationManager.OpenWebConfiguration(VirtualPath)
        Dim section As ConfigurationSection = config.GetSection(sectionName)

        'If ((section.SectionInformation.IsProtected = False) AndAlso _
        '    (section.ElementInformation.IsLocked = False)) Then

        ' UnProtect (decrypt) the section.
        section.SectionInformation.UnprotectSection()

        ' Save the decrypted section.
        section.SectionInformation.ForceSave = True
        config.Save(ConfigurationSaveMode.Full)
        'config.Save()
        'End If

    End Sub

    Public Function EncryptValue(ByVal UnEncryptedValue As String) As String
        Dim RetVal As String
        If (Not (UnEncryptedValue Is Nothing) AndAlso _
		UnEncryptedValue.Trim().Length > 0) Then
            RetVal = Cryptographer.EncryptSymmetric(symmProvider, UnEncryptedValue & "")
        End If
        Return RetVal
    End Function

    Public Function DecryptValue(ByVal EncryptedValue As String) As String
        Dim RetVal As String
        If (Not (EncryptedValue Is Nothing) _
		AndAlso EncryptedValue.Trim().Length > 0) Then
            RetVal = Cryptographer.DecryptSymmetric(symmProvider, EncryptedValue & "")
        End If
        Return RetVal
    End Function

End Module

I generate my connection strings when the application starts in Global.asax as follows - the password is retrieved from web.config. Note that the connection string is stored in application variables in an encrypted form and decrypted when instantiating the EntLibWrapper class. Also note that the appSettings in web.config is encrypted or decrypted based on a parameter retrieved from a database - this enables the developer to add new appSettings later after decrypting the section:

VB.NET
    Const UserId As String = "YourUserNameGoesHere"

	Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
	    ' Fires when the application is started

	    Dim Password As String = ConfigurationManager.AppSettings("SYSPWD")

	    Application("News") = EncryptValue_
		("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & _
		Server.MapPath("../fpdb/News.mdb") & ";Jet OLEDB:System Database=" _
		& Server.MapPath("../fpdb/SelfService.mdw") & ";User ID=" & _
		UserId & ";Password=" & Password & ";")

	    Dim ELW As New EntLibWrapper(DecryptValue(Application("News")), _
		EntLibWrapper.enumProviderFactory.OleDb)
	    Dim dbread As OleDbDataReader
	    Dim sql As String
	    Dim EncryptAppSettings As Boolean

	    ' GET PARAMETER - EncryptAppSettings
	    sql = "SELECT tblParameters.*"
	    sql = sql & " FROM [tblParameters]"
	    sql = sql & " WHERE (((tblParameters.RecordId)='EncryptAppSettings'));"
	    dbread = ELW.GetDataReader(sql)
	    If dbread.HasRows Then
	        Do While dbread.Read()
	            EncryptAppSettings = dbread.Item("MyBoolean")
	        Loop
	    Else
	        EncryptAppSettings = True
	    End If
	    dbread.Close()

	    ELW.Close()

	    If EncryptAppSettings = True Then
	        'Call ProtectSection("appSettings", _
		"DataProtectionConfigurationProvider", "/timetable/Documentation")
	        'Call ProtectSection("appSettings", _
		"RSAProtectedConfigurationProvider", "/timetable/Documentation")
	        Call ProtectSection("appSettings", _
		"SelfServiceConfigurationProvider", "/timetable/Documentation")
	    Else
	        Call UnProtectSection("appSettings", "/timetable/Documentation")
	    End If

End Sub

Then where I need to get the data, the code is as follows for getting a data reader and binding it to a dropdownlist:

VB.NET
Dim ELW As New EntLibWrapper(DecryptValue(Application("News")), _
				EntLibWrapper.enumProviderFactory.OleDb)
Dim dbread As OleDbDataReader
Dim sql As String

' LOAD ddCountry
sql = "SELECT qryNewsCountry.*"
sql = sql & " FROM qryNewsCountry;"
dbread = ELW.GetDataReader(sql)
ddCountry.DataSource = dbread
ddCountry.DataTextField = "CountryName"
ddCountry.DataValueField = "Country"
If Session("SelectedCountryIndex") Is Nothing Then
    ddCountry.SelectedIndex = 0
Else
    ddCountry.SelectedIndex = Session("SelectedCountryIndex")
End If
ddCountry.DataBind()
dbread.Close()

' Do other things with the ELW object and then finally close it
ELW.Close()

For the encryption / decryption to work, we need web.config entries. The sample web.config you have downloaded contains entries which have been created using the Enterprise Library Configuration tool.

The following links are useful to fully understand issues regarding encryption / decryption. This will also help you to create your encryption keys:

Please note that the EntLibWrapper class can be used even if you are not bothered about encryption / decryption. The class does not force you to encrypt appSettings in web.config or the connection strings stored in application variables. You are also not forced to store the connections strings in application variables - you can also retrieve it from a database if that suits your requirement better.

The EntLibWrapper class has the following methods / properties / enums / references:

Methods

  • Close
  • LoadDataSet
  • New
  • DoActionQuery
  • GetDataAdapter
  • GetDatabase
  • GetDataReader
  • GetDataSet
  • GetDataTable
  • GetTableWithZero

Properties

  • Database - This enables the user to get access to the other methods and properties in the GenericDatabase in Enterprise Library
  • Identity - After calling the DoActionQuery method to insert a record, the user can retrieve the id of the last record inserted using this property.
  • RowsAffected - After calling the DoActionQuery method, the user can retrieve the number of rows affected from this property.

Enum

  • enumProviderFactory

References

  • Microsoft.Practices.EnterpriseLibrary.Common
  • Microsoft.Practices.EnterpriseLibrary.Data
  • Microsoft.Practices.EnterpriseLibrary.Security.Cryptography
  • System.Configuration
  • System.Data.Common
  • System.Data.Odbc
  • System.Data.OleDb
  • System.Data.OracleClient
  • System.Data.SqlClient
  • System.Web.Configuration

The downloaded EntLibWrapper.CHM file should give you more details of the EntLibWrapper class. When you use the class in your project, you will notice that it integrates well with Intellisense and the Object Explorer in Visual Studio. I was also able to generate the CHM file using nDocs easily. In case you are wondering how this was achieved, you can have a look at the following links:

Thanks to those who have given me feedback based on which I was able to improve the class further. Hope this enterprise library wrapper class is of use to others too.

Cheers

Raja Lakshman

History

  • 11th June, 2009
    • Initial post
  • 16th June, 2009
    • RowsAffected and Identity properties added
  • 26th June, 2009
    • Added LoadDataSet method
    • Added Encryption/Decryption functions 
    • Compiled help file added using XML comments in code.

License

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