Global Application Settings Override (Now with examples!)
My previous posting about TableAdapter connection strings was a pretty big hit with a lot of people. It’s still being referenced on the MSDN forums from time to time when someone needs a solution to the problem of having a connection string for a TableAdapter in the app.config that they need to override at runtime.
Since that post was in VB.Net and over 2 years old now, I’ve decided to resurrect it and work up a quick sample project with both C# and VB.Net code. Someone had request C# and I have no idea why I didn’t include it when I did that post, but better late than never!
Click here for the sample solution. (VS2010) If you don’t have that version of visual studio, you can download the express version from Microsoft, or just open the projects individually in VS2008.
TableAdapter Connection Strings
* 7/29/2010 Update – I have uploaded a sample project here for those of you still struggling with this. It includes C# and VB.Net projects. Enjoy!
I don’t know how a lot of people handle it, but one thing that always bugged me about table adapters and datasets is the way it handles it’s connection strings. It starts out innocent enough. A new dataset is created with it’s associated table adapter. The connection string is saved to the application settings file and that property is saved in the dataset. Running it on the development machine (or on the same network) is no big deal and just works. But what if you send that application to someone else that has their own SQL Server?
Did anyone at Microsoft actually use this scenario in a production environment? What were they thinking?
In order to run that in a production environment you have to set the connection string in the app.config file. While this may be fine for some people, what about the people like me that do not want my users to access that database? I don’t want them to have that username and password… While the chances of a normal user loading SQL Server Management Studio and logging in are slim, it’s still possible and it’s definitely possible if a user purposely wants to get out of having to do work that day.
What are the options? Well, one option is to use the encryption to encrypt the settings in app.config. For me, this option is not ideal. Reports of those settings getting corrupted are quite high, plus you have to deal with the loading and saving of those settings, which isn’t all that easy to do.
The other option is to take the route I was taking for a while… I had a function that would build me a connection string. Then I could: TableAdapter.Connection.ConnectionString = myLibrary.ConnectionStringFunction()
This was great, until that day came where I was in a hurry and added a few more tables to a form but forgot to set the ConnectionStrings. Whoops.
So I needed a solution that would stop me from having to set those ConnectionString properties, keep my connection string out of the app.config, and be easy to use (i.e. Just Works).
I started out by just giving all my datasets the same connection string. Then on application startup, I tried to change that one application setting. Hmm.. It seems those ConnectionString properties are set to friend and are read only.
Upon further investigation, it seems that there are some events that fire, such as SettingsLoaded. This event fires when the app.config is read and all the settings are loaded. When this event fires, it fires inside the MySettings class. This should allow that property to be changed.
Private Sub MySettings_SettingsLoaded(ByVal sender As Object, ByVal e As System.Configuration.SettingsLoadedEventArgs) Handles Me.SettingsLoaded
Me.Item("MyAppConnectionString") = MyLibrary.BuildConnectionString()
End Sub
This will set the MyAppConnectionString setting to the proper connection string. Now, all table adapters will have an up to date connection string.
So what happens if you want to change the connection string later while the application is still running? Well, there is no way to do that. So it’s time to come up with a way to trick it into updating that property.
In looking at the MySettings class, there is another event called PropertyChanged. We can use this event if we create another setting that can be updated anywhere in the application. First, we create a new string setting that has a User scope (I called mine ConnectionString). This will allow the application to update the setting at any time.
Next, we need to create a function that will update that property with our connection string.
Public Shared Sub ChangeConnectionString()
My.Settings.ConnectionString = BuildConnectionString()
End Sub
Now we can change the events in the MySettings class to look like this.
Private Sub MySettings_PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Handles Me.PropertyChanged
If e.PropertyName = "ConnectionString" Then
Me.Item("MyAppConnectionString") = My.Settings.ConnectionString
End If
End Sub
Private Sub MySettings_SettingsLoaded(ByVal sender As Object, ByVal e As System.Configuration.SettingsLoadedEventArgs) Handles Me.SettingsLoaded
MyLibrary.ChangeConnectionString()
End Sub
End Class
Now, every time that ChangeConnectionString() is called, the MyAppConnectionString will be updated. The ChangeConnectionString procedure can be changed so that it can accept a string parameter that is the actual connection string. Then you can build a Connection String anywhere and just pass it to that procedure.
Disposing of memory leaks.
One of the biggest causes of memory leaks that I find from time to time is from forgetting to dispose of some SQLConnection object. Even if the connection is closed, the SQLConnection object seems to stay alive forever, which keeps any form objects alive as well.
This happens in VB a lot if you forget to call the Dispose method of the SQLConnection object or if you don’t use the Using keyword. While cleaning up code, the best way to get in the habit of doing it, seems to be changing everything into a Using statement so I get in the habit of using it…
Instead of:
Dim myCommand As New SqlClient.SqlCommand("Select Count(*) from MyTable")
Dim myConnection As New SqlClient.SqlConnection(ConnectionString)
myCommand.Connection = myConnection
myConnection.Open()
Dim iCount As Integer = CInt(myCommand.ExecuteScalar())
myConnection.Close()
myConnection.Dispose()
myCommand.Dispose()
Do this:
Using myCommand As New SqlClient.SqlCommand("Select Count(*) from MyTable")
Using myConnection As New SqlClient.SqlConnection(ConnectionString)
myCommand.Connection = myConnection
myConnection.Open()
Dim iCount As Integer = CInt(myCommand.ExecuteScalar())
End Using
End Using
Since the IDisposable interface on the SQLConnection object will automatically close open connections, there is no need to close it… However, if you decide to do this:
Using myCommand As New SqlClient.SqlCommand("Select Count(*) from MyTable" _
, New SqlClient.SqlConnection(ConnectionString))
myCommand.Connection.Open()
Dim iCount As Integer = CInt(myCommand.ExecuteScalar())
myCommand.Connection.Close()
End Using
Make sure you close the connection first… Although, I don’t recommend doing it that way, because the IDisposable interface of the SQLCommand object does not call the IDisposable interface of the SqlConnection object that it holds. I’ve read that all the SqlConnection object’s IDisposable interface does is close an open connection, I wouldn’t trust that completely and dispose of it properly.
Windows Installer – SQL Server Installation Issues and a Solution.
Here is the problem in a nutshell: You have a custom client application that uses SQL Server 2005 Express. The server installation is a little bit to be desired. First, you have to install SQL Server Express, then you have to configure the options (unless you did command line options), then you have to execute the scripts to create your database, tables, and possibly populate it with some data. Or conversely, execute scripts or install and use the management console to attach a database. Messy, at best.
So what if you want to have a single installation program that will install all the prerequisites (.Net), then install SQL Server 2005 with your custom options (instance name for example), and then have it execute scripts and fill it with data? After working at it for hours, I came to the conclusion that you could be out of luck. Until I did some reconfiguring.
First things first, you need to get SQL Server Express to install properly. I struggled with this for a while, until I had the bright idea of using a bootstrapper… But can’t we use the one that comes with the Visual Studio Package and Deployment Wizard? Yes you can!
Go into your Visual Studio 8 folder and find the SDK/v2.0/Bootstrapper/Packages folder. Make a copy of the SqlExpress folder and name it something like, MyAppSqlExpress. Inside that folder, there is a product XML file. Edit that product.xml file and change the product code. Mine was Microsoft.Sql.Server.Express.1.0 and I changed it to MyAppName.Microsoft.Sql.Server.Express.1.0 … Just change the MyAppName to the app name of the product it goes with. You will also see a En folder for the English installation. The package.xml file is the one you want to edit in that folder. The first item to edit, is the arguments line. This passes arguments to the SqlExpr32.exe file (the setup for Sql Express). The arguments are the same as any other SQL Express install… So change it to be something like this:
Arguments='-q /norebootchk /qb reboot=ReallySuppress addlocal=all INSTANCENAME=MyApp SECURITYMODE=SQL SAPWD=MyStrongPassword DISABLENETWORKPROTOCOLS=0 SQLAUTOSTART=1'
This will do a silent installation of SQL Server Express using all protocols, a custom instance name (MyApp), turn on the SQL security mode and specify a strong SA password.
Next, there is the strings block at the bottom. Edit the StringName “DisplayName” to look like this:
MyApp SQL Server 2005 Express Edition
Save it, and go into your setup and deployment project you created, go to the project properties, then to the prerequisites, and in the list you should now see your custom install of SQL Server Express!
This does work, I just tested it out today.
So that solves problem #1. What about the problem of executing scripts? That seems to be the easy part… From my earlier post you can incorporate that code into a custom action.
Create a new project, and select Installer Class from the templates. This creates a new installer class for you to augment.
Private Sub ExecuteSQL(ByVal SQL As String, _
ByVal ConnectionString As String)
Dim srv As New Server(New ServerConnection( _
New SqlClient.SqlConnection(ConnectionString)))
srv.ConnectionContext.ExecuteNonQuery(SQL)
srv.ConnectionContext.Disconnect()
End Sub
Private Sub BulkLoad(ByVal ConnectionString As String, _
ByVal TableName As String)
Dim bulk As New SqlClient.SqlBulkCopy(ConnectionString, _
SqlClient.SqlBulkCopyOptions.KeepIdentity)
Dim ds As New DataSet
Dim path As String = My.Application.Info.DirectoryPath
ds.ReadXmlSchema(path & "\" & TableName & ".xsd")
ds.ReadXml(path & "\" & TableName & ".xml")
bulk.BulkCopyTimeout = 0
bulk.DestinationTableName = TableName
bulk.WriteToServer(ds.Tables(TableName))
bulk.Close()
End Sub
Public Overrides Sub Install(ByVal stateSaver As _
System.Collections.IDictionary)
MyBase.Install(stateSaver)
Dim ConnBuilder As New SqlClient.SqlConnectionStringBuilder()
ConnBuilder.UserID = "sa"
ConnBuilder.Password = "MyStrongPassword"
ConnBuilder.DataSource = "localhost\MyApp"
ConnBuilder.InitialCatalog = "master"
ExecuteSQL(My.Resources.CreateDatabaseSQL, _
ConnBuilder.ConnectionString())
ConnBuilder.InitialCatalog = "MyDatabase"
BulkLoad(ConnBuilder.ConnectionString(), "MyTable")
End Sub
In this example, I just stored the data files (an XML data file and an XSD schema file) in the setup project, which gets installed along with the custom installer DLL. The create database SQL stuff was all saved as a store resource in the installer class project. I did this so that some of the structure was hidden, which may or may not be an issue for others.
Now, add the custom action to a setup project and you are finished!

