.NET Setup Deployment - MSI, Cassini, SQL Server, NTFS

Creating any moderately complex MSI based installation with the Visual Studio .NET 2005 setup project is a real pain. Today's tips will include how to easily package up a single installation file, setup SQL Server 2005, execute large sql scripts, launch the application at the end of setup, configure NTFS permissions, trigger another MSI file, and auto install and configure UltiDev's Cassini Web Server.

As I'm sure you've read elsewhere, you can hook into the installer's events and trigger your own C#/VB.NET code.  The easiest way to do this is to create a separate class library project in your solution and add the class below:

using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Windows.Forms;
using System.Configuration.Install;
using System.Collections;
using System.IO;
using System.Diagnostics;

namespace YourNamespace
{
    [System.ComponentModel.RunInstallerAttribute(true)]
    public class MyInstall : System.Configuration.Install.Installer
    {
        const string ASSEMBLYPATH_STATENAME = "assemblypath";
        private Container components=null;
 
        public MyInstall()
        {
         // This call is required by the Designer.
         InitializeComponent();
         this.Committed += new InstallEventHandler(MyInstall_Committed);
        }
 
        private void InitializeComponent()
        {

        }
 
       private void WriteTextFile(string fileName,
                                  string contents)
       {
         try
         {

           DeleteFile(fileName);

           using (StreamWriter sw = new StreamWriter(fileName))
           {
             sw.Write(contents);
           }
         }
         catch (Exception) { throw; }
       }
 
       private void DeleteFile(string fileName)
       {
         try
         {

          if (File.Exists(fileName))
          {
            File.Delete(fileName);
          }

         }
         catch (Exception) { throw; }
        }
 
       public override void Install(IDictionary stateSaver)
       {
         base.Install(stateSaver);
       }
 
       public override void Rollback(IDictionary savedState)
       {
         base.Rollback(savedState);
       }

       public override void Commit(IDictionary savedState)
       {
        base.Commit(savedState);
       }
 
       public override void Uninstall(IDictionary savedState)
       {
         base.Uninstall(savedState);
       }

       /// <summary> 
       /// Clean up any resources being used.
       /// </summary>
       protected override void Dispose(bool disposing)
       {
         if (disposing)
         {
           if (components != null)
           {
              components.Dispose();
           }
         }
         base.Dispose(disposing);
       }
 
      private void MyInstall_Committed(object sender,
                                       InstallEventArgs e)
      {
        string path = "";
        string appName = "";
        string args = "";
        System.Diagnostics.Process process = null;

        try
        {
      
          path = GetParameter("assemblypath");
          path = path.Replace(@"\TheClassLibraryThisInstallerClassIsIn.dll","");
          appName =Path.Combine(path,"YourWindowsFormsApplicationName.exe");

          args = "\"" + path + "\"";

          // Why am I passing the applications install
          // directory as an argument?  For some reason,
          // an application launched from an MSI will "think"
          // its execution path is C:\windows\system32 if
          // from "inside" the application you try to determine
          // its app path.  Very odd...
 
          // autostart is a radio button parameter
          // from the installer dialog window you
          // created that asked if the user wanted
          // to auto start or not.  And, you added

// the following line in the CustomActionData
// property of your custom action:
// /autostart=[AUTOSTART]


// If your command line argument can have
// spaces, your CustomActionData would look like this:
// /autostart="[AUTOSTART]"
if (GetParameter("autostart") == "1") { process = new System.Diagnostics.Process(); process.Start(appName, args); process.StartInfo.FileName = appName; process.StartInfo.Arguments = args; process.Start(); // If you ever want to launch another // app or even an msi, you can use // process.WaitForExit(); To wait for

// each one to finish.
} } catch (Exception ex) { WriteTextFile(Path.Combine(path,"install.log"), ex.Message); } } private string GetParameter(string parameterKey) { if (Context.Parameters[parameterKey] == null) { return String.Empty; } return Context.Parameters[parameterKey].Trim(); } } }

Then, add that project/assembly to the list of primary output items
for the installer.  In the installer's Custom Item section, add
a custom item for each of the events and target it to your newly
added installation "hook" project. You'll find a reference to it
in the Application Folder when creating the Custom Item.

That's it. You can now run anything you want straight from the installer.

Now the real fun begins. You want to package up the dotnetfx.exe,
sqlexpr32.exe, the .msi and anything else you need to one file. Why?

If you choose to have .net and sql server installed as a later
download, you run the risk of install interruptions. Plus,
the .NET install routes the user to a web site where they must
choose the right version. Do you really want users to have to guess?

Ideally, we want the user to download one file and have the setup
take care of the rest. So, the only real reliable way to do this is to
spend $50 and buy Winzip's zip self extraction tool. You set your
installer to use the local copy of the prerequisites and build your
installation.

Upon completion, zip up all the files into myapplication.zip.
Then, run winzip's self extraction creator and set it to run as a software installation. When the user downloads your .exe, everything
will happen easily and automatically for your user.

After installing SQL Server 2005 Express, you are going to get a nasty
little surprise that you should have expected. The windows account
"Network Service" that the SQL Server services runs under most likely doesn't
have write access to the folder you will eventually run CREATE DATABASE on.

So, you'll need to set NTFS permissions on that folder first. Here's
a quick .NET 2.0 sample:


using System;
using System.Collections.Generic;
using System.Text;
using System.Security.AccessControl; 
using System.IO;

public static bool GrantModifyAccessToFolder(string windowsAccountUserName,
                                             string folderName)
{
  DirectoryInfo directory = null;
  DirectorySecurity directorySecurity = null;
  FileSystemAccessRule rule = null;

  try
  {

    if (windowsAccountUserName.Length <1) { return false; }
    if (folderName.Length <1) { return false; }
    if (!Directory.Exists(folderName)) { return false; }

    directory = new DirectoryInfo(folderName);

    directorySecurity = directory.GetAccessControl();

    rule = new FileSystemAccessRule(windowsAccountUserName,
                                    FileSystemRights.Modify,
                                    InheritanceFlags.None |
                                    InheritanceFlags.ContainerInherit |
                                    InheritanceFlags.ObjectInherit,
                                    PropagationFlags.None,
                                    AccessControlType.Allow);

    directorySecurity.SetAccessRule(rule);

    directory.SetAccessControl(directorySecurity);

    return true;

  }
  catch (Exception) { throw; }
}



Cassini Web Server Installation

You'll want to include UltiDev.com's CassiniExplorerSetup.msi
and CassiniServer2Setup.msi as files to deploy in your application
setup .msi file. Whenever you are ready, you pass in the root
folder of where your application will be installed and it will
create a subfolder called "website" and set it to be the root
folder of the web application.
private void InstallWebServer(string appPath)
{

  string cassiniLocation = "";
  string cassiniExplorer = "";
  string args = "";
  System.Diagnostics.Process msi = null;

  try
  {

   cassiniLocation = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.ProgramFiles), @"UltiDev\Cassini Web Server for ASP.NET 2.0\UltiDevCassinWebServer2.exe"); cassiniExplorer = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.ProgramFiles), @"UltiDev\Cassini Web Server Explorer\LocalStart.htm"); if (System.IO.File.Exists(cassiniLocation)) { // Cassini is already installed return; } msi = new System.Diagnostics.Process(); msi.StartInfo.FileName = "msiexec"; msi.StartInfo.Arguments ="/passive /i \"" + Path.Combine(appPath, "CassiniExplorerSetup.msi") + "\""; msi.Start(); msi.WaitForExit(); msi = new System.Diagnostics.Process(); msi.StartInfo.FileName = "msiexec"; msi.StartInfo.Arguments = "/passive /i \"" + Path.Combine(appPath, "CassiniServer2Setup.msi") + "\""; msi.Start(); msi.WaitForExit(); } catch (Exception) { // decide what you want to have happen here? // IIS maybe? throw; } try { // UltiDev's cassini wants a GUID as the website // identifier and the 90210 is a hard coded port // (use any port you want) when registering // your website. args = "/register \"" + Path.Combine(appPath, "website"); args += "\" someGUIDgoeshere Default.aspx 90210 /DontKeepRunning"; msi = new System.Diagnostics.Process(); msi.StartInfo.FileName = cassiniLocation; msi.StartInfo.Arguments = args; msi.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; msi.Start(); msi.WaitForExit(); MessageBox.Show("Cassini web server installed."); } catch (Exception ex) { if (System.IO.File.Exists(cassiniExplorer)) { Process.Start(cassiniExplorer); } } }
// Sometimes, you'll need to execute large SQL Server scripts 
// with GO statements.  The following method will execute
// them without the need to parse the string:

using Microsoft.SqlServer.Management.Smo;

using Microsoft.SqlServer.Management.Common;

public void ExecSql(string sql, string connectionString,string dataBaseNameToPrepend) { try { sql = sql.Trim(); if (sql.Length <1 ) { return; } if (dataBaseNameToPrepend != null) { if (dataBaseNameToPrepend.Trim().Length > 0) { sql = "USE ["+ dataBaseNameToPrepend.Trim() + "]\nGO\n" + sql; } } using (SqlConnection conn = new SqlConnection(connectionString)) {
conn.Open(); Server server = new Server(new ServerConnection(conn)); server.ConnectionContext.ExecuteNonQuery(sql); server.ConnectionContext.Disconnect(); } } catch (Exception ex) {
// You'll need to pass back the inner exception
// to get anything useful for errors thrown using
// Microsoft.SqlServer.Management.Smo throw new Exception(ex.InnerException.Message); } }

There you have it. You should be able to drastically improve your installation capabilities without having to resort to some expensive installation software that is even more of a pain to learn and use.
By Robbe Morris   Popularity  (10144 Views)
Picture
Biography - Robbe Morris
Robbe has been a Microsoft MVP in C# since 2004. He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.  Robbe also loves to scuba dive and go deep sea fishing in the Florida Keys or off the coast of Daytona Beach. Microsoft MVP