Visual Studio Setup and Deployment Project Tips and Tricks
Setup and deployment projects were a welcome addition to the Visual Studio project templates. They have allowed many a developer an easy way to quickly produce installers for their applications. However, as with most of Microsoft's offerings, you only get so far out of the box and most real-world use cases require one to delve a little further into the workings. So, let us delve!
Having recently created an installer for a web application using the Setup and Deployment project template and after spending much time on Google searching for ways to do certain things, I thought it might be helpful to collect some of my findings together so that others may benefit (and so I have somewhere central to find these answers again next time!).
There are two main areas with respect with respect to deployment projects that I would like to focus on - the use of Custom Actions and Bootstrapping. For an introduction to and basic information regarding Setup and Deployment projects, you can take a look here and here.
1. Custom Actions
Briefly, custom actions allow you to hook into the installation pipeline of the Microsoft Installer. They allow developers to add customized logic (such as creating a database) as part of the installation process and it is through them that you will enhance your installer. Through custom actions, you can add custom logic to any or all of the following phases of installation:- Install
- Commit
- Rollback
- Uninstall
Now that we understand that custom actions are the means by which we can extend Setup and Deployment projects and add additional actions to be performed during installation, let's take a look at some of the basic tasks I have needed to do within an installer that were enabled through custom actions:
- Creating a database
- Importing data
- Adding a desktop shortcut
- Updating web.config
- Adding ASP.NET services
- Handling errors
- Debugging your custom actions
-
Creating a Database
While creating a SQL Server database does not require large amounts of math, here I mainly wanted to highlight the awesomeness of the SqlConnectionStringBuilder class. The existence of www.connectionstrings.com proves that I'm not the only one who draws a blank sometimes on proper connection string format! The SqlConnectionStringBuilder class builds it for you (surprise!) which can be very helpful (especially for SQL Express databases). Note the creation of a Trusted connection (connStrBuilder.IntegratedSecurity = true;)
private void CreateDB(string dbServer, string dbName)
{
SqlConnectionStringBuilder connStrBuilder = new SqlConnectionStringBuilder();
connStrBuilder.InitialCatalog = "master";
connStrBuilder.DataSource = dbServer;
connStrBuilder.ConnectTimeout = 5;
connStrBuilder.IntegratedSecurity = true;
string sql = string.Format("CREATE DATABASE {0}", dbName);
SqlCommand cmd = new SqlCommand(sql, conn);
//Init connection, open and set db
conn.ConnectionString = connStrBuilder.ConnectionString;
cmd.Connection.Open();
try
{
cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
throw ex;
}
finally
{
cmd.Connection.Close();
}
}
You would call the CreateDB from the Install method as follows:
public override void Install(IDictionary stateSaver)
{
serverName = this.Context.Parameters["dbServer"];
databaseName = this.Context.Parameters["dbName"];
CreateDB(serverName, databaseName);
}
You'll note the references to Context.Parameters - the Parameters collection is how information collected by the installer is made available to you in your custom actions. In this example, I had added a custom dialog box to the installer with two textboxes for gathering the names of the database server and database to create. On the Custom Actions view of my Setup and Deployment project, I have set the CustomActionData property to indicate that I want the value from textbox CUSTOMTEXTA1 to be available as dbServer and the value from textbox CUSTOMTEXTA2 to be available as dbName. I have also set the selected target virtual directory name as TGTVDIR. These values are then made available to me in Context.Parameters in my custom action.
/dbServer=[CUSTOMTEXTA1] /dbName=[CUSTOMTEXTA2] /TGTVDIR=[TARGETVDIR]
-
Importing Data
Any developer who has experience with MySQL, specifically with backing up (bless you, mysqldump) and restoring a database, simply cannot understand why the task is not as smooth with SQL Server. One will quite often wish to create a database and fill it with data as part of the installation process, which with MySQL is dead simple.
As a solution, Microsoft now has their Database Publishing Wizard, which makes things much easier - here at last is an easy way to script a database. However, I ran into an issue in using (finding) the tool and in my search engine travels I noticed I wasn't the only one so I'll call out the obvious now. The original Database Publishing Wizard was introduced as an easily found and downloadable add-on to SQL Server 2005. Since the introduction of SQL Server 2008, the downloadable version was apparently done away with and the functionality is now an included feature. Thus you will find many Microsoft blogs/articles etc. insisting that Database Publishing Wizard should show up as a context menu item in both Visual Studio 2008 and within SQL Management Studio when you right-click on a database node - it does not. At least not for myself and a host of other developers with access to google (see here for an example). However, right-clicking on a database node and selecting Tasks->Generate Scripts will give you the payoff you're looking for. I never did find out why the discrepancy and I lost interest after I found the indicated solution.
My main reason for wanting to talk about importing data is due to an issue I encountered with running my database install script. It seems to be specific to SQL Server 2008, but I am not 100% certain of that. The problem I encountered is that SQL Server would run out of memory while running my import script! The exact error I was getting is "There is insufficient system memory in resource pool 'internal' to run this query". I was trying to execute the script in one go using the ExecuteNonQuery() method of SqlCommand class and even though it wasn't a particularly long script, I kept encountering the problem (there appears to be some known issue with SQL Server in this regard - see this article for more information). Suffice it to say that my interest lay (and lies) not in tracking down or even understanding SQL Server issues but in writing code so I needed a solution that didn't involve me tinkering with or reading any more about SQL Server 2008. The long and short of it is that in order to run the data import script, I had to break my script into individual commands and run them one by one (oh, and restarting the SQL Server service is also helpful). The code snippet below shows this process:
private void ExecuteSql(string dbServer, string dbName, string sql)
{
SqlConnectionStringBuilder connStrBuilder = new SqlConnectionStringBuilder();
connStrBuilder.UserID = "MyDBUserId";
connStrBuilder.Password = "MyDBUserPassword";
connStrBuilder.InitialCatalog = "master";
connStrBuilder.DataSource = dbServer;
connStrBuilder.ConnectTimeout = 5;
SqlCommand cmd = new SqlCommand(sql, conn);
//Init connection, open and set db
conn.ConnectionString = connStrBuilder.ConnectionString;
cmd.Connection.Open();
cmd.Connection.ChangeDatabase(dbName);
try
{
string[] commands = sql.Split(new string[] { "GO\r\n", "GO", "GO\t" }, StringSplitOptions.RemoveEmptyEntries);
foreach (string c in commands)
{
cmd.CommandText = c;
cmd.ExecuteNonQuery();
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
cmd.Connection.Close();
}
}
Hope this is helpful to someone.
-
Adding a Desktop Shortcut
For desktop applications, creating a shortcut using the Setup and Deployment project template is quite simple. However, to create a shortcut to a web application is a little more involved (or can be). For a recently developed web application, I created an installer that allowed the user to set the web server and the virtual directory into which to install the application. I then needed to dynamically create a shortcut to the application using the user-set values. I accomplished this through a custom action as shown below.
private void CreateShortcut()
{
string vdirPath = this.Context.Parameters["TGTVDIR"];
System.Configuration.Configuration config = WebConfigurationManager.OpenWebConfiguration(string.Format(@"/{0}", vdirPath));
string path = config.FilePath.Replace("web.config","");
string shortcutFileName = string.Format(@"{0}\MyWebApplication.lnk", Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory));
WshShellClass shell = new WshShellClass();
IWshRuntimeLibrary.IWshShortcut shortcut = (IWshRuntimeLibrary.IWshShortcut)shell.CreateShortcut(shortcutFileName);
shortcut.TargetPath = string.Format("http://localhost/{0}", vdirPath);
shortcut.Description = "Launch MyWebApplication";
shortcut.IconLocation = string.Format(@"{0}MyWebApplication.ico",path);
shortcut.Save();
}
NOTE: In order to use the WshShellClass class, you will need to add a reference to the Windows Script Host Object Model COM object.
-
Updating web.config
Sometime you want to update the web.config your application ships with to reflect the user's environment without having the users edit the file themselves. For example, after asking the user for the name of the database server/database and creating the database, you will want to then update the web.config with new connection string information. How/where should this be done? You guessed it! In a custom action:
private void UpdateWebConfig(string connectionString)
{
//virtual directory passed in from installer in CustomActionData
string vdirPath = this.Context.Parameters["TGTVDIR"];
try
{
//open the web.config
System.Configuration.Configuration config = WebConfigurationManager.OpenWebConfiguration(string.Format(@"/{0}", vdirPath));
//change the connectionString for the DB
config.ConnectionStrings.ConnectionStrings["MyAppsConnectionStrings"].ConnectionString = connectionString;
config.Save();
}
catch (Exception ex)
{
throw ex;
}
}
-
Adding ASP.NET Services
Adding ASP.NET services (membership, profiles etc.) to your SQL Server database is normally accomplished using the aspnet_reqsql.exe command line tool, but you can also accomplish this programmatically within your custom action. Instead of trying to execute the actual aspnet_regsql.exe using System.Diagnostics.Process.Start(), there is an easier way built into the .NET framework:
//Add ASP.NET membership service
SqlConnectionStringBuilder connStrBuilder = new SqlConnectionStringBuilder();
connStrBuilder.InitialCatalog = "master";
connStrBuilder.DataSource = serverName;
connStrBuilder.IntegratedSecurity = true;
SqlServices.Install(dbName, SqlFeatures.All, connStrBuilder.ConnectionString);
Where the SqlFeatures enum has the following values:
- None
- Membership
- Profile
- RoleManager
- Personalization
- SqlWebEventProvider
- All
-
Handling Errors
When something unexpected goes wrong during the installation process, the installer automatically rolls back any changes it has made during the incomplete installation. However, this does not extend to any custom actions performed by you. If there are any changes that your custom action code performed that should be undone, you must undo them yourself by hooking into the installer's Rollback phase. To hook into the Rollback phase, you must override and implement the Rollback method in your custom actions. For example, the following custom action Rollback code deletes the database that was created in the Install method:
public override void Rollback(IDictionary savedState)
{
base.Rollback(savedState);
//Remove database if we are rolling back for
//any reason other than because the selected
//DB exists (if it does, we don't want to delete an existing DB -
//it might have useful data
if (!((bool)savedState["dbExists"]))
{
SqlConnection.ClearAllPools();
Server server = new Server((string)savedState["serverName"]);
try
{
server.Databases[(string)savedState["dbName"]].Drop();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
-
Debugging Your Custom Actions
Before we leave our discussion of useful tasks to perform in a Custom Action, you may want to know how you go about debugging your custom actions (remember, your custom action code is executing within the context of Microsoft Installer, so you can't simply press F5 in Visual Studio and start debugging). Like debugging any other external code, you can attach directly to the running installer process using the 'Attach to Process' command from the Visual Studio Debug menu, or (simpler) you can force a debugger break in your custom action code which will invoke the debugger and start debugging from the point of the break. See below:
public override void Install(IDictionary stateSaver)
{
System.Diagnostics.Debugger.Break();
//Custom action code...
}
So there you have it - hopefully the explanation of some of these common installation activities that can be performed using custom actions has been useful for you. Now let's talk a little about installer prerequisites.
2. Prerequisites and Bootstrapping
Most applications developed with Visual Studio will have some sort of dependency upon one component or another and will require that said components are installed on the machine before the application will run/install (e.g. a particular version of the .NET framework, Crystal Reports, Sql Server etc.). Setup and Deployment projects allow you to install other components as part of the installation of your application (a process known as 'bootstrapping'). Visual Studio comes with several components to get you going and, with a little effort you can create your own bootstrap packages for components not included with Visual Studio. Visual Studio 2008 comes with the following prerequisites:
- .NET Framework 2.0
- Visual C++ Runtime Libraries
- Windows Installer 3.1
- .NET Framework Client Profile
- .NET Framework 3.0
- .NET Framework 3.5
- Crystal Reports Basic for Visual Studio 2008
- Microsoft Office 2007 Primary Interop Assemblies
- Microsoft Visual Studio 2008 Report Viewer
- Sql Server Compact 3.5
- Microsoft Visual Basic PowerPacks 1.2
- Visual Studio Tools for the Office system 3.0 Runtime Service Pack 1
- SQL Server 2005 Express Edition SP2
All the required information for the various Bootstrapper packages are defined using xml files that Visual Studio consumes to find and understand everything about a package (what files are included, any related products or prerequisites, what to do on install error etc.).
-
Specifying Prerequisites
To specify prerequisites for your application, select Properties for your Setup and Deployment project and click the "Prerequisites" button.
This will bring up a list of available bootstrapper packages on your machine.
Note the section for specifying install location. If you wish to include the prerequisite binaries as part of your installer instead of the user needing to download them as needed (e.g. think of an installation that must be accomplished without internet access), then you can tell Visual Studio to download them from the same location as your application, which will package the prerequisite with your installer (and potentially bloat the size tremendously - e.g. the .NET Framework 3.5 SP1 package adds 191MB to the installer!).
-
Custom Prerequisites
It is possible for a developer to create their own bootstrap packages that will be detected by Visual Studio and thereafter added as a prerequisite to a Setup and Deployment project. The list of boostrapper packages available to Visual Studio is generated based upon available packages in the \Program Files\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages\ directory.
In order to make new prerequisites available to your Setup and Deployment projects, you must create your own bootstrapper packages and deploy them into this directory. While it is possible to create the requisite xml describing the package by hand, you will make your life so much easier if you make use of a Microsoft tool called the Bootstrapper Manifest Generator (BMG), available here. The BMG tool guides you through various options for your package using an easy GUI and once you have made various selections regarding your package, it will then compile the results for you into xml files (product.xml and package.xml). See the included help file for more information. Oh, by the way make sure you explicitly set the exit code for success - I had a package that kept failing with the message "Installation successful" (very confusing, that!). It turns out it was because relying on the default treatment of system exit codes was not sufficient and I had to explicitly set exit code 0 as a success code.
As an example, here is the product.xml and package.xml code for a bootstrapper package I created for the ASP.NET MVC runtime:
product.xml:
?<?xml version="1.0" encoding="utf-8"?>
<Product ProductCode="ASP.NET.MVC" xmlns="http://schemas.microsoft.com/developer/2004/01/bootstrapper">
<RelatedProducts>
<DependsOnProduct Code="Microsoft.Net.Framework.3.5.SP1" />
<DependsOnProduct Code="Microsoft.Windows.Installer.3.1" />
</RelatedProducts>
package.xml:?<?xml version="1.0" encoding="utf-8"?>
<Package Name="DisplayName" Culture="Culture" xmlns="http://schemas.microsoft.com/developer/2004/01/bootstrapper">
<PackageFiles CopyAllPackageFiles="false">
<PackageFile Name="aspnetmvc1.msi" HomeSite="http://www.asp.net/mVC/download/" PublicKey="3082010A0282010100BD72B489E71C9F85C774B8605C03363D9CFD997A9A294622B0A78753EDEE463AC75B050B57A8B7CA05CCD34C77477085B3E5CBDF67E7A3FD742793679FD78A034430C6F7C9BAC93A1D0856444F17080DF9B41968AA241CFB055785E9C54E072137A7EBCE2C2FB642CD2105A7D6E6D32857C71B7ACE293607CD9E55CCBBF122EBA823A40D29C2FBD0C35A3E633DC72C490B7B7985F088EF71BD435AE3A3B30DF355FB25E0E220D3E79A5E94A5332D287F571B556A0C3244EF666C6FF0389CEF02AD9AA1DD9807100E3C1869E2794E4614E0B98CD0756D9CAC009C2D42F551B85AF4784583E92E7C2BBB5DCD196128AD94430AC56A42FFB532AEA42922DE16E8D30203010001" />
</PackageFiles>
<Commands Reboot="Defer">
<Command PackageFile="aspnetmvc1.msi" EstimatedInstallSeconds="30">
<ExitCodes>
<ExitCode Value="0" Result="Success" />
<DefaultExitCode Result="Fail" String="Anunexpectedexitcodewasr" FormatMessageFromSystem="true" />
</ExitCodes>
</Command>
</Commands>
<Strings>
<String Name="Culture">en</String>
<String Name="DisplayName">ASP.NET MVC 1.0</String>
<String Name="Anunexpectedexitcodewasr">An unexpected exit code was returned from the installer. The installation failed.</String>
</Strings>
</Package>
This article showing how to include the .NET Framework 3.5 SP1 as a prerequisite is a good reference for the process.
I hope these tips will be helpful for you in extending the usefulness of Setup and Deployment Projects and the breadth of available prerequisite packages.
Awesome, thanks for tips man love it
Posted by: Web Application Developer | May 06, 2011 at 07:18 AM