Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#4.0

Installer and Patching Program using Visual Studio 2010

5.00/5 (11 votes)
8 Nov 2010CPOL18 min read 68.3K   2.2K  
A How-To Guide for replacing the integrated ClickOnce technology built into Visual Studio, and control the install/update process using Visual Studio tools

Introduction

My goal was to create a solution that was capable of installing a .NET application for the first time, and keep it updated throughout its life cycle. I have previously attempted to use ClickOnce for these tasks, but found it cumbersome and had quite a few issues with certificates and signing that invalidated my previous install (so it would not update), particularly when multiple developers worked on the same solution. I was intent on creating an installer and updater that would not ask for administrative privileges every time it ran, and would handle most real world tasks while allowing me to keep my installation simple for the end user. It must also be compatible with all commonly used Windows technologies and allow me to launch production and BETA updates. I am a self-taught programmer and fully expect to learn from my mistakes, so don't hesitate to point them out if you find one. I have tested this extensively and believe I have found any/all bugs.

Credit for Contributions (I used these code snippets/libraries)

Versioning

I wanted to be able to handle the application Version from the patcher utility so credit for my AssemblyInfoUtil.cs class goes to this article from CodeProject to create a class that modifies version numbers and recompiles as I patch. My program uses these Version Numbers.

Compression and Password Protection

I needed to be able to compress and at least semi-protect the output out on the web, so I used the ICSharp library from here to do so.

Automatic Launching of Application on Setup Exit

I needed the ability to "update" the setup and keep the flow of the application alive, so I opted to use a script to automatically launch the application. From this CodeProject article, I found a link to this MSDN Blog where I learned how to modify the end of the Visual Studio installer with a checkbox to automatically launch the program. When run in /passive mode from my launcher, it just closes and executes the updated launcher with no user feedback, and on first install with user feedback.

Use Options

  • Option 1 - Fire and Forget Deployment
    • Download the Zip File
    • Create a website in IIS or web server of your choice, turning web caching off.
      • If you do not have local access (i.e. \\web1\d$\websites), you will have to modify the code for FTP usage
    • Modify the launcher form with logo, copyright, etc.
    • Add your project references to SelfUpdatingProgram
    • Modify SelfUpdatingProgram Program.cs to launch your project instead of Form1.cs
    • Edit Updater.settings in SelfUpdatingSettings and ensure all settings match your settings
      • Your Application Executable is going to be SelfUpdatingProgram.EXE unless you change that project name.
      • The Company Name will be the folder your subfolder and files will reside in (typically on the C:, but conforms to their system)
    • Rebuild Entire Solution in Release Mode, then build the Setup solution
      • Modify the Setup Properties for Company Name, Support, Developer, etc.
      • Click on View Folders in the Setup Area and rename the shortcut links appropriately.
      • Note: In order to update the SETUP after the initial deployment, you must click on the setup project, go to Properties (F4) and increment one of the Version Numbers.
      • When prompted, choose YES to update the Product Code.
      • You must build your setup project manually after this each time you make changes to the Launcher program.
    • Set SelfUpdatingPatcher as the StartupProject and run it
    • Set your starting Version and Click on Production
    • Verify all files were deployed to website correctly (you can do this from Patcher if it's a local drive)
    • Basic Setup can be achieved by a link directly to the production MSI file (only file not zipped)
      • Note: By running the .msi, it will NOT check and assist in the download of needed components (such as .NET)
    • Advanced Setup can be used to package setup.exe and .msi file into an IEXPRESS package and using this as your permanent install link.
      • Warning: If you do this from a 64 bit machine, the package will not work on 32 bit machines. Conversely, if you do this on a 32 bit machine, it will work on both.
      • From a command prompt, type IEXPRESS.exe and follow the instructions, setting the starting file to be setup.exe
      • After creating the IEXPRESS package, delete the new EXE file created, edit the .SED file created, and modify the setup.exe line to read "setup.exe /passive".
      • Run the IEXPRESS loading the SED one more time but skipping modification and only building your output (/passive is ignored if not modified into the SED).
      • Your setup is now unattended after the user clicks run (Additional Screenshots and Instructions at the end of article)
  • Option 2 - Roll your Own
    • Download the source package and create a mimic project line by line, making changes as needed for full customization.
    • Rather than attempt to modify the downloaded package, I suggest creating a new solution from scratch and implementing the pieces, customizing along the way.
    • Once complete, see Option 1 with the new Solution.

Project Layout

SelfUpdatingProgram

  • This project is the actual Software you would be running
  • Note: This project for me was an MDI parent, that contains multiple references to other projects I want to include.
    • All associated projects this level or higher must be compiled in "Release Mode" or the patcher will not find them.
    • No detail below for this project, it's as simple as this is your primary project or the code that executes your primary project.
      • It is not necessary to "replace this project", instead just add a reference to your existing project and point Program.cs to start it.
      • Projects that you add via DLL to this project will update automatically when you recompile them and rebuild this project.
        • The DLL you add will be self-updating, i.e. if you add a DLL from the bin/release directory of a solution, every time you compile that solution in release, it will update the reference and cause a PUSH on the next Patcher run of the new files.

SelfUpdatingLauncher

  • This project is what will be in the setup file, and launches your Software Project after it patches. All files used in your application will be stored in the root directory of the user's "Program Files" or "Program Files<x86>" drive. It should be noted that the default implementation does NOT check for "available" space and is very liberal with real error messages. There are a number of articles available that cover this extensively. The default form I've got setup works great and allows for a lot of customization. I have seen some XP machines where it shows the transparent borders incorrectly, but still not horrible. The default button setup on the left is the "options" button which stops autoclose, and clicked again stops autolaunch. The X as it indicates closes the form.
Image 1

SelfUpdatingPatcher

  • This project updates your Version #, Zips your files, Password Protects them, and uploads them. The width of the boxes may need to be adjusted to match your versioning scheme.
Image 2

SelfUpdatingSettings

This is a generic project I can use in all other projects to re-use classes and settings.

SelfUpdatingSetup

This is a setup project that should almost never change, but if it does will be downloaded as a patch, it only contains the Launcher's output, required .NET version, and any components that need to be installed in the GAC.

If you must update the Setup File (i.e. components updated that need to be in the GAC, bug fixes, or .NET upgrades) you must also increase the version number of the setup project. To do this, click on the setup project, and hit F4 for properties. Increase the version by one, and choose yes when prompted for Product Code. The launcher will detect a newer setup file, the setup file will launch but it will not update unless the version number has increased.

To update the Launcher, you will have to include a new Setup file. Make your changes to the Launcher, click on SelfUpdatingSetup and hit F4. Click in the Version field and increase the Version by 1, so if it says 1.0.0 change it to 1.0.1 like this.

Image 3

You will then be prompted to update the Product Code like this.

Image 4

Then you need to rebuild the Setup Project in order for the Patcher to recognize the change.

Image 5

Additional Considerations

You will need a web server to host the patches, I am using IIS in Windows Server 2008, but any should do. Important setting here is to turn off cache, otherwise the client will not see your updates until a considerable amount of time has passed. You will also notice I am using direct paths to upload my patches, you may need to replace this with FTP to your IIS folder (plenty of examples here on CodeProject but I highly suggest using WebClient even for FTP), I chose direct path as our servers are blocked from the outside, so I can only upload a file there if I am in the building or have VPN on.

For ease of use, I suggest having all projects in the same parent folder (you will see the settings requirements in SelfUpdatingSettings).

It is imperative if you roll your own Setup or Launcher that you include the EnableLaunchApplication.js in the Setup folder and edit it to include the new name of your Launcher Program.EXE (refer to the above article if necessary)

Create the Projects

At this point, you probably have a project already in mind for this, simply add the additional projects to it, Launcher, Patcher, and Settings. We'll create the setup a little later.

Settings Project

  • Note: I added a folder and a copy of ICSharpCode binary for later use to ensure I always used the same version of binary.
  • Click Add New Project, Class Library Project.
  • Delete the default class
  • Select the new Project, and click Add -> New Item -> Settings file. Name it appropriately (mine is Updater.settings)
  • Set the Access Modifier to Public and add these string settings under the Application Scope:
    • ApplicationExecutable  -> SelfUpdatingProgram.EXE
    • UriRepositoryDefault  ->  http://xxx.xxx.xxx/SUP/Repository/
    • ProjecctReleaseFolder  ->  ..\..\..\SelfUpdatingProgram\bin\Release
    • SetupReleaseFolder  ->  ..\..\..\SelfUpdatingSetup\Release
    • AppSubFolderBeta  ->  Beta
    • AppSubFolderProd  ->  Prod
    • AppSubFolderSetup  ->  Setup
    • ZipFilePassword  ->  [good password here]
    • VersionFile  ->  ..\..\..\SelfUpdatingProgram\Properties\AssemblyInfo.cs
    • RepositoryDefault -> //web1/d$/websites/website/Repository/  (should be replaced with your direct connection or block of code with FTP)
    • RepositoryExistsPath  ->  //web1/d$/websites/website/
    • SoftwareManifestFileName  ->  software_manifest.xml
    • Company Name -> Sup Inc (this is the folder name used to store your files, obviously no illegal characters)
    • MSBuild_WorkingDirectory -> MSBuild is necessary to incorporate your Version changes without having to load the Patcher twice.
Click to enlarge image
  • Add the required Classes (preferably under a folder called Classes)
    • AssemblyInfoUtil.cs, ExecuteModeEnum.cs, CheckForUpdate.cs, and StaticClasses.cs (see below)

AssemblyInfoUtil.cs

This class simply loads the Assembly Version information from SelfUpdatingProgram and optionally writes new version information to the file, then executes the newly compiled file. It is primarily used in the Patcher Program. On class instantiation, it will use the Settings for the default, or you can override it as necessary. One it has the filename it checks for VB or CS, and then reads the lines of the file looking for the ones that modify Versions. It stores these versions in 4 public int variables on the class. It contains a Writeline which executes the same code that reads it with an optional switch to "Write" Changes.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Diagnostics;

namespace SelfUpdatingSettings.Classes
{
    public class AssemblyInfoUtil
    {
        /// <summary>
        /// Loads the AssemblyUtil.cs file from the Updater.sessings 
        /// location to read and then optionally update the version number
        /// </summary>
        /// <param name="fullFileLocation">Default Blank - 
        /// Sets the location of the version file</param>
        public AssemblyInfoUtil(string fullFileLocation = "")
        {
            if (fullFileLocation == string.Empty)
                fullFileLocation = Updater.Default.VersionFile;
            projectFullFileLocation = fullFileLocation;

            // Make sure file exists
            if (!System.IO.File.Exists(projectFullFileLocation))
            {
                MessageBox.Show("Invalid File Name for Assembly File: 
		\r\n" + projectFullFileLocation, "Failed to Load Versioning Assembly", 
		MessageBoxButtons.OK);
                return;
            }

            // Determine if assembly is in VB
            if (Path.GetExtension(projectFullFileLocation).ToLower() == ".vb")
                isVB = true;

            ReadFile();
        }

        private string versionStr = string.Empty;
        public string VersionStr
        {
            get
            {
                return version1.ToString() + "." + version2.ToString() + 
		"." + version3.ToString() + "." + version4.ToString();
            }
        }

        private int incParamNum = 0;

        private int version1 = -1;
        public int Version1
        {
            get { return version1; }
            set
            {
                version1 = value;
                fileisDirty = true;
            }
        }
        private int version2 = -1;
        public int Version2
        {
            get { return version2; }
            set
            {
                version2 = value;
                fileisDirty = true;
            }
        }
        private int version3 = -1;
        public int Version3
        {
            get { return version3; }
            set
            {
                version3 = value;
                fileisDirty = true;
            }
        }
        private int version4 = -1;
        public int Version4
        {
            get { return version4; }
            set
            {
                version4 = value;
                fileisDirty = true;
            }
        }

        public bool fileisDirty = false;

        public string projectFullFileLocation = string.Empty;
        public bool isVB = false;

        /// <summary>
        /// Processes the file line by line to read the version information
        /// </summary>
        private void ReadFile()
        {
            // Read file
            StreamReader reader = new StreamReader(projectFullFileLocation);
            String line;
            try
            {
                while ((line = reader.ReadLine()) != null)
                {
                    line = ProcessLine(line, false);
                }
            }
            catch (Exception exd)
            {
                MessageBox.Show(exd.Message + "\r\n" + exd.StackTrace, 
			"Failed to Load Versioning Information");
            }
            reader.Close();
        }

        /// <summary>
        /// Processes the file line by line an replaces the 
        /// version information with the settings class.
        /// After it is complete it deletes the old assembly, 
        /// writes a new one, then runs the MSBuild compiler on the default project.
        /// </summary>
        public void WriteFile()
        {
            if (fileisDirty)
            {
                // Read file
                StreamReader reader = new StreamReader(projectFullFileLocation);
                StreamWriter writer = new StreamWriter(projectFullFileLocation + ".out");
                String line;

                while ((line = reader.ReadLine()) != null)
                {
                    line = ProcessLine(line, true);
                    writer.WriteLine(line);
                }
                reader.Close();
                writer.Close();
                File.Delete(projectFullFileLocation);
                File.Move(projectFullFileLocation + ".out", projectFullFileLocation);
                using (Process p = new Process())
                {
                    p.StartInfo = new ProcessStartInfo("MSBuild.exe");
                    p.StartInfo.WorkingDirectory = 
			Path.GetFullPath(Updater.Default.MSBUILD_WorkingDirectory);
                    p.StartInfo.Arguments = "/t:Rebuild /p:Configuration=Release";
                    p.Start();
                    p.WaitForExit(50000);
                }
            }
        }

        /// <summary>
        /// Reads a line of the file and optionally will change the version 
        /// information to match the public version settings
        /// </summary>
        /// <param name="line">The incoming file line in string</param>
        /// <param name="performChange">Whether or not to perform a change 
        /// on the line version parameter</param>
        /// <returns>Returns a new line with the changed version</returns>
        private string ProcessLine(string line, bool performChange)
        {
            if (isVB)
            {
                line = ProcessLinePart(line, "<Assembly: AssemblyVersion(\"", false);
                line = ProcessLinePart(line, "<Assembly: AssemblyFileVersion(\"", false);
            }
            else
            {
                line = ProcessLinePart(line, "[assembly: AssemblyVersion(\"", false);
                line = ProcessLinePart(line, "[assembly: AssemblyFileVersion(\"", false);
            }
            return line;
        }

        /// <summary>
        /// Actually processes the line part that patches Assembly Version Information
        /// </summary>
        /// <param name="line">The line in question</param>
        /// <param name="part">The assembly part reading/writing the version</param>
        /// <param name="performChange">Wether or not to commit changes</param>
        /// <returns>Returns a new line with the changed version</returns>
        private string ProcessLinePart(string line, string part, bool performChange)
        {
            int spos = line.IndexOf(part);
            // Make sure line isn't commented out
            int failIndex = line.IndexOf("//");
            if (spos >= 0 && failIndex == -1)
            {
                spos += part.Length;
                int epos = line.IndexOf('"', spos);

                versionStr = line.Substring(spos, epos - spos);
                StringBuilder str = new StringBuilder(line);
                str.Remove(spos, epos - spos);
                str.Insert(spos, VersionStr);
                line = str.ToString();

                string[] version = versionStr.Split((".").ToCharArray());
                foreach (string s in version)
                {
                    if (version1 == -1)
                    {
                        version1 = int.Parse(s);
                        continue;
                    }
                    if (version2 == -1)
                    {
                        version2 = int.Parse(s);
                        continue;
                    }
                    if (version3 == -1)
                    {
                        version3 = int.Parse(s);
                        continue;
                    }
                    if (version4 == -1)
                    {
                        version4 = int.Parse(s);
                        continue;
                    }
                }
            }
            return line;
        }
    }
}        

ExecuteModeEnum.cs

Parameter to control the Execute Mode (Production or Beta). Having it generic allows me to use my settings project in all my projects and have access to the Execute Mode.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SelfUpdatingSettings.Classes
{
    public enum ExecuteModeEnum
    {
        //None = 0, // Force a selection
        //Test = 1, // Not Used
        Beta = 2,
        Production = 3
    }
} 

StaticClasses.cs

A small collection of static classes to handle argument passing and finding the default hard drive to use. To add additional size requirement checking, and other features you can do so here. The argument passing feature allows your setup to add a shortcut to "Program Beta" and execute the launcher as BETA, which then executes the program as BETA. It would also pass any other arguments you may use to customize your solution.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace SelfUpdatingSettings.Classes
{
    public static class StaticClasses
    {

        private static string installPath = string.Empty;
        public static string InstallPath
        {
            get
            {
                if (installPath == string.Empty)
                {
                    // obtain the default program files folder based 
                    // upon whether 32 or 64-bit architecture is in play
                    if (8 == IntPtr.Size || (!String.IsNullOrEmpty
		    (Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432"))))
                    {
                        installPath = Environment.GetEnvironmentVariable
					("ProgramFiles(x86)");
                        if (installPath == null)
                        {
                            installPath = "C:\\Program Files (x86)";
                        }
                    }
                    else
                    {
                        installPath = Environment.GetEnvironmentVariable("ProgramFiles");
                        if (installPath == null)
                        {
                            installPath = "C:\\Program Files";
                        }
                    }
                    installPath = Path.GetPathRoot(installPath) + 
				Updater.Default.CompanyName + "\\";
                }
                return StaticClasses.installPath;
            }
        }

        // convert a string array into a string with the specified 
        // separator between each value
        public static string getStringArrayValuesAsString(string[] stringArray, 
		string separator, bool omitEmptyGuid)
        {
            StringBuilder sb = new StringBuilder();
            bool hasEmptyGuid = false;
            foreach (string s in stringArray)
            {
                if (s == Guid.Empty.ToString())
                    hasEmptyGuid = true;
                if (omitEmptyGuid && s == Guid.Empty.ToString())
                {
                    continue;
                }
                if (sb.Length > 0)
                {
                    sb.Append(separator);
                }
                sb.Append(s);
            }
            if (!omitEmptyGuid &&
                !hasEmptyGuid)
            {
                sb.Append(separator);
                sb.Append(Guid.Empty.ToString());
            }
            return sb.ToString();
        }
    }
}

CheckForUpdate.cs

This is the class that does the heavy lifting of the Updater, this class can be instantiated from the running program in a "Check For Update" button/action, but in this implementation is only used on application load. You will need to add a reference to ICSharpCode.SharpZipLib.dll to the project for this class (included in Resources directory).

  • Important Public Properties in this Class
    • SetupUpdateNeeded (bool) lets you know if there is a new setup file
    • MainUpdateNeeded (bool) lets you know if any other files need updating
    • UpdateNeeded (DataTable) a table listing the files to update
    • FileCount (int) number of files that need to be downloaded
  • Important Public Methods in this Class
    • Class Load, reads the executeMode passed, and the StaticClass InstallPath and sets variables to match what paths to download files from, where to save files, the launcher path, and the setup file for the ExecutionMode you choose. It then finds a nice temporary folder to hold the files in, and creates the DataTable to hold update needed files and executes the method to download the manifest file. (XML file containing files and hash strings). The important note about this class is since the "actual" program we are launching is not RUNNING at the moment, it's easy to check the existing files against the manifest.
    • DownLoadFile (returns bool) Execute on each row of the UpdateNeeded table to handle download. This allows you flexibility on the progress bar from the launcher.
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
using System.Data;
using System.Net;
using System.Security.Cryptography;

namespace SelfUpdatingSettings.Classes
{
    public class CheckForUpdate
    {
        /// <summary>
        /// Instantiate the Class to check for an update, 
        /// it loads a table with files to be updated.
        /// </summary>
        /// <param name="e">The mode of update</param>
        public CheckForUpdate(ExecuteModeEnum e)
        {
            // Determine what we need to look for (Production, Beta, or Test)
            execMode = e;

            // Setup URLs to download proper manifest
            companyDir = StaticClasses.InstallPath;
            switch (execMode)
            {
                case ExecuteModeEnum.Beta:
                    mainUri = Updater.Default.UriRepositoryDefault + 
				Updater.Default.AppSubFolderBeta + "/";
                    downloadUri = Updater.Default.UriRepositoryDefault + "/";
                    setupUri = Updater.Default.UriRepositoryDefault + 
				Updater.Default.AppSubFolderBeta + "/" + 
				Updater.Default.AppSubFolderSetup + "/";
                    launcherPath = companyDir + Updater.Default.AppSubFolderBeta + "\\";
                    setupFilePath = companyDir + Updater.Default.AppSubFolderBeta + 
				"\\" + Updater.Default.AppSubFolderSetup + "\\";
                    break;
                default:
                    mainUri = Updater.Default.UriRepositoryDefault + 
				Updater.Default.AppSubFolderProd + "/";
                    downloadUri = Updater.Default.UriRepositoryDefault + "/";
                    setupUri = Updater.Default.UriRepositoryDefault + 
				Updater.Default.AppSubFolderProd + "/" + 
				Updater.Default.AppSubFolderSetup + "/";
                    launcherPath = companyDir + Updater.Default.AppSubFolderProd + "\\";
                    setupFilePath = companyDir + Updater.Default.AppSubFolderProd + 
				"\\" + Updater.Default.AppSubFolderSetup + "\\";
                    break;
            }
            manifestFileName = Updater.Default.SoftwareManifestFileName;

            // Set working temporary Path
            tempPath = Path.GetTempPath();
            tempPath = tempPath + System.Guid.NewGuid().ToString() + "\\";
            tempDirInfo = new DirectoryInfo(tempPath);
            if (tempDirInfo.Exists)
            {
                tempDirInfo.Delete(true);
            }
            tempDirInfo.Create();

            // Setup updateNeeded table with files needed to update
            updateNeeded.TableName = "UpdateNeededFiles";
            updateNeeded.Columns.Add("sourcePathName", typeof(string));
            updateNeeded.Columns.Add("sourceFileName", typeof(string));
            updateNeeded.Columns.Add("zippedPathName", typeof(string));
            updateNeeded.Columns.Add("zippedFileName", typeof(string));

            // Download Manifest and check for Setup Update Needed
            DownloadManifest();
        }

        private ExecuteModeEnum execMode = ExecuteModeEnum.Production;
        private bool setupUpdateNeeded = false;
        public bool SetupUpdateNeeded
        {
            get { return setupUpdateNeeded; }
        }

        private bool mainUpdateNeeded = false;
        public bool MainUpdateNeeded
        {
            get { return mainUpdateNeeded; }
        }

        public bool checkSuccess = false;
        public Exception CheckException = null;

        private int filecount = 0;
        public int Filecount
        {
            get { return filecount; }
        }

        private DataTable updateNeeded = new DataTable();
        public DataTable UpdateNeeded
        {
            get { return updateNeeded; }
        }

        private string companyDir = string.Empty;
        public string CompanyDir
        {
            get { return companyDir; }
        }

        private string downloadUri = string.Empty;
        public string DownloadUri
        {
            get { return downloadUri; }
        }

        private string setupFilePath = string.Empty;
        public string SetupFilePath
        {
            get { return setupFilePath; }
        }

        private string launcherPath = string.Empty;
        public string LauncherPath
        {
            get { return launcherPath; }
        }

        private string mainUri = string.Empty;
        private string setupUri = string.Empty;
        private string manifestFileName = string.Empty;
        private string tempPath = string.Empty;
        private DirectoryInfo tempDirInfo;

        /// <summary>
        /// Download and Process the entries in the manifest file for the application
        /// </summary>
        private void DownloadManifest()
        {
            try
            {
                string fileName = tempPath + manifestFileName;
                /// WebClient is a very efficient way to retrieve the manifest file
                using (WebClient client = new WebClient())
                {
                    Uri uri =
                        new Uri(mainUri + "/" + manifestFileName);
                    // Make sure temporary manifest File does not exist, 
                    // if it does dump it
                    if (System.IO.File.Exists(fileName))
                    {
                        System.IO.File.Delete(fileName);
                    }
                    client.DownloadFile(uri, fileName);
                }
                // turn manifest into a dataset
                DataSet maniDS = new DataSet();
                maniDS.ReadXml(fileName);
                filecount = maniDS.Tables["Files"].Rows.Count;

                DataRow updateRow;
                foreach (DataRow row in maniDS.Tables["Files"].Rows)
                {
                    string zippedFileName = (string)row["zippedfilename"];
                    string zippedPathName = (string)row["zippedpathname"];
                    string md5HashedValue = (string)row["md5HashedValue"];
                    string sourceFileName = (string)row["sourceFileName"];
                    string sourcePathName = (string)row["sourcePathName"];
                    string sourceFullPath = companyDir + 
			sourcePathName.Substring(1) + "\\" + sourceFileName;
                    // Check the file for an update
                    bool fileNeeded = downloadOfFileNeeded
				(sourceFullPath, md5HashedValue);
                    if (fileNeeded)
                    {
                        if (!mainUpdateNeeded)
                            mainUpdateNeeded = true;
                        // Program needs to know an update is needed 
                        // and the files needed to update
                        updateRow = updateNeeded.NewRow();
                        updateRow["zippedfileName"] = zippedFileName;
                        updateRow["zippedPathName"] = zippedPathName;
                        updateRow["sourcefilename"] = sourceFileName;
                        updateRow["sourcepathname"] = sourcePathName;
                        updateNeeded.Rows.Add(updateRow);

                        if (zippedFileName.EndsWith(".msi"))
                        {
                            // Program needs to let updater know to stop and 
                            // update installer
                            setupUpdateNeeded = true;
                            setupFilePath += zippedFileName;
                        }
                    }
                }
            }
            catch (Exception exd)
            {
                checkSuccess = false;
                CheckException = exd;
            }
        }

        /// <summary>
        /// Process individual file and compare length has to stored length hash
        /// </summary>
        /// <param name="fileName">The filename of the file being checked 
        /// (full path + name of local copy)</param>
        /// <param name="hashString">The stored has value to compare the 
        /// local copy with</param>
        /// <returns></returns>
        private bool downloadOfFileNeeded(string fileName, string hashString)
        {
            bool downloadFile = true;
            if (System.IO.File.Exists(fileName))
            {
                using (FileStream oStream = System.IO.File.OpenRead(fileName))
                {
                    byte[] obuffer = new byte[oStream.Length];
                    oStream.Read(obuffer, 0, obuffer.Length);
                    byte[] hashValue = 
			new MD5CryptoServiceProvider().ComputeHash(obuffer);
                    string hashX = BitConverter.ToString(hashValue);
                    downloadFile = (hashString.ToLower() != hashX.ToLower());
                    oStream.Close();
                }
            }
            return downloadFile;
        }

        /// <summary>
        /// Perform a download of the specified file
        /// </summary>
        /// <param name="uriFileName">The full url + file you need to download</param>
        /// <param name="zipFileName">The zipped up file name</param>
        /// <param name="targetFileName">The target unzipped File Name</param>
        /// <param name="subDirPath">The path you are going to put the file in</param>
        /// <returns>True if Successful, False if UnSuccessful</returns>
        public bool DownloadFile(string uriFileName, 
		string zipFileName, string targetFileName, string subDirPath)
        {
            // Used to verify download
            bool completeDownload = false;
            try
            {
                using (WebClient client = new WebClient())
                {
                    Uri uri = new Uri(uriFileName);

                    if (!System.IO.Directory.Exists(subDirPath))
                    {
                        System.IO.Directory.CreateDirectory(subDirPath);
                    }

                    if (System.IO.File.Exists(targetFileName))
                    {
                        System.IO.File.Delete(targetFileName);
                    }

                    if (System.IO.File.Exists(zipFileName))
                    {
                        System.IO.File.Delete(zipFileName);
                    }

                    client.DownloadFile(uri, zipFileName);
                    if (uriFileName.EndsWith(".zip"))
                    {
                        unzipFileToFolder(zipFileName, targetFileName);
                    }
                }
                if (System.IO.File.Exists(targetFileName))
                {
                    completeDownload = true;
                }
                else
                {
                    this.CheckException = new Exception
			("File Did Not Exist after Download");
                }
            }
            catch (Exception exd)
            {
                this.CheckException = exd;
                completeDownload = false;
            }
            return completeDownload;
        }

        /// <summary>
        /// Unzip a file to the specified folder, 
        /// optionally deleting the zipped file when finished
        /// </summary>
        /// <param name="zippedFileName">The Zipped FileName (including path)</param>
        /// <param name="targetFileName">The Target File Name Unzipped</param>
        /// <param name="deleteZippedFile">
        /// Should the file be deleted after unzipped</param>
        private void unzipFileToFolder(string zippedFileName, 
		string targetFileName, bool deleteZippedFile = true)
        {
            using (ZipInputStream s = new ZipInputStream
			(System.IO.File.OpenRead(zippedFileName)))
            {
                // obtain password to all zip files
                s.Password = Updater.Default.ZipFilePassword;
                ZipEntry theEntry;
                string tmpEntry = string.Empty;
                while ((theEntry = s.GetNextEntry()) != null)
                {
                    FileStream streamWriter = System.IO.File.Create(targetFileName);
                    int size = 2048;
                    byte[] data = new byte[size];
                    while (true)
                    {
                        size = s.Read(data, 0, data.Length);
                        if (size > 0)
                        {
                            streamWriter.Write(data, 0, size);
                        }
                        else
                        {
                            break;
                        }
                    }
                    streamWriter.Close();
                }
            }
            if (deleteZippedFile)
            {
                if (System.IO.File.Exists(zippedFileName))
                {
                    System.IO.File.Delete(zippedFileName);
                }
            }
        }
    }
}

Patcher Project

  • Click Add New Project, Windows Forms Project
  • Create a new form for the Patcher and link it to Program.cs
  • Make sure you add a project reference to SelfUpdatingSettings
    • Initialize ensures directories exist, and then creates the manifest datatable, manifest file, MD5 hashes, zips, and uploads to Repository.
      • The code executes almost identical to the CheckForUpdate class in reverse with some additions.
    • Allows options for Production and Beta releases, and recompiles all projects prior to being uploaded using msbuild in order to sustain Version changes.
      • If a project is not set to Release - it will NOT be included in this upload.
      • Reminder - If you have external libraries that NEED to be included on the client machine, you may have to edit their properties to Copy Local and/or include them in the Setup Dependencies.
  • Note: I added a folder to MSBUILD here and copied the MSBUILD files from their default location to this folder in order for the patcher to rebuild the assemblyInfo into the versioning of my product. The versioning could be extended beyond the main project if required.

Code Behind for Patcher Project

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Security.Permissions;
using System.IO;

using SelfUpdatingSettings;
using SelfUpdatingSettings.Classes;
using System.Drawing.Drawing2D;
using System.Security.Cryptography;
using ICSharpCode.SharpZipLib.Zip;

namespace SelfUpdatingPatcher
{
    public partial class PatcherDialogBox : Form
    {
        public PatcherDialogBox()
        {
            InitializeComponent();

            try
            {
                // Ensure application is only run local to web by just 
                // checking for local access to directory
                DirectoryInfo r = new DirectoryInfo(Updater.Default.RepositoryExistsPath);
                if (r.Exists)
                {
                    DialogResult = System.Windows.Forms.DialogResult.OK;
                }
                else
                {
                    MessageBox.Show("Repository Cannot Run Outside of Servers, 
		    need local access to " + Updater.Default.RepositoryExistsPath,
                        "Failed to Run", MessageBoxButtons.OK);
                    this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
                }
            }
            catch (Exception exd)
            {
                MessageBox.Show("Repository Cannot Run Outside of Servers, 
		need local access to " + Updater.Default.RepositoryExistsPath,
                    "Failed to Run", MessageBoxButtons.OK);
                MessageBox.Show(exd.StackTrace, exd.Message, MessageBoxButtons.OK);
                this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
            }
        }

        #region Form-centric events

        private void PatcherDialogBox_Load(object sender, EventArgs e)
        {
            // Refuse to open form if there is a problem
            if (this.DialogResult == System.Windows.Forms.DialogResult.Cancel)
            {
                this.Close();
            }
            try
            {
                initFileTable();
                obtainSettingValues();
                webBrowser.Visible = false;
                webBrowser.SetBounds(listResults.Bounds.X, listResults.Bounds.Y, 
			listResults.Bounds.Width, listResults.Bounds.Height);
            }
            catch (Exception ex)
            {
                listResults.Items.Add(ex.Message + "\r\n" + ex.StackTrace.ToString());
            }
            versionUtil = new AssemblyInfoUtil(Updater.Default.VersionFile);
            version1Tbox.Value = versionUtil.Version1;
            version2Tbox.Value = versionUtil.Version2;
            version3Tbox.Value = versionUtil.Version3;
            version4Tbox.Value = versionUtil.Version4;
            version1Tbox.ValueChanged += versionTbox_ValueChanged;
            version2Tbox.ValueChanged += versionTbox_ValueChanged;
            version3Tbox.ValueChanged += versionTbox_ValueChanged;
            version4Tbox.ValueChanged += versionTbox_ValueChanged;
        }

        private void PatcherDialogBox_Paint(object sender, PaintEventArgs e)
        {
            Rectangle BaseRectangle =
                new Rectangle(0, 0, this.Width - 1, this.Height - 1);

            Brush Gradient_Brush =
                new LinearGradientBrush(
                BaseRectangle,
                Color.DarkOrange, Color.LightSlateGray,
                LinearGradientMode.Vertical);
            e.Graphics.FillRectangle(Gradient_Brush, BaseRectangle);
        }

        private void PatcherDialogBox_Resize(object sender, EventArgs e)
        {
            // Invalidate, or last rendered image will just be scaled
            // to new size
            this.Invalidate();
        }

        #endregion

        #region Button-centric events

        private void btnCancel_Click(object sender, EventArgs e)
        {
            DialogResult = System.Windows.Forms.DialogResult.Cancel;
            this.Close();
        }

        private void btnBeta_Click(object sender, EventArgs e)
        {
            listResults.Items.Add("Clicked Beta to Push");
            btnBeta.BackColor = Color.LightGreen;
            btnBeta.Refresh();
            btnProduction.BackColor = DefaultBackColor;
            btnProduction.Refresh();
            makeListResultsVisible();
            versionUtil.WriteFile();
            executePushToRepository(ExecuteModeEnum.Beta);
        }

        private void btnProduction_Click(object sender, EventArgs e)
        {
            listResults.Items.Add("Clicked Production to Push");
            DialogResult dialogResult = MessageBox.Show
		("Confirm patching of PRODUCTION?", "Patcher Question", 
		MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation);
            if (dialogResult == DialogResult.OK)
            {
                btnBeta.BackColor = DefaultBackColor;
                btnBeta.Refresh();
                btnProduction.BackColor = Color.LightGreen;
                btnProduction.Refresh();
                makeListResultsVisible();
                versionUtil.WriteFile();
                executePushToRepository(ExecuteModeEnum.Production);
                if (MessageBox.Show(this, 
		"Beta will now be pushed to ensure uniformity.", 
		"Pushing Beta", MessageBoxButtons.OKCancel) ==
                         System.Windows.Forms.DialogResult.OK)
                    btnBeta_Click(null, null);
            }
            else
            {
                listResults.Items.Add("Production Push not Confirmed");
            }
        }

        private void btnViewRepositorySite_Click(object sender, EventArgs e)
        {
            Cursor.Current = Cursors.WaitCursor;
            if (btnViewRepositorySite.Tag == null)
            {
                listResults.Visible = false;
                webBrowser.Visible = true;
                webBrowser.SetBounds(listResults.Bounds.X, 
		listResults.Bounds.Y, listResults.Bounds.Width, 
		listResults.Bounds.Height);
                webBrowser.Navigate(Updater.Default.UriRepositoryDefault);
                btnViewRepositorySite.Text = "Hide &Repository Site";
                btnViewRepositorySite.Tag = "Hide";
                btnViewRepositorySite.BackColor = Color.PaleTurquoise;
                btnViewRepositorySite.Refresh();
                btnViewTempFolder.BackColor = DefaultBackColor;
                btnViewTempFolder.Refresh();
            }
            else
            {
                makeListResultsVisible();
            }
            Cursor.Current = Cursors.Default;
        }

        private void btnViewTempFolder_Click(object sender, EventArgs e)
        {
            btnViewRepositorySite.BackColor = DefaultBackColor;
            btnViewRepositorySite.Refresh();
            btnViewTempFolder.BackColor = Color.PaleTurquoise;
            btnViewTempFolder.Refresh();
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.InitialDirectory = pathTempCompany;
            openFileDialog.Filter = "All files (*.*)|*.*";
            openFileDialog.ShowDialog();
        }

        #endregion

        #region Form-level variables

        private string appFolder = string.Empty;
        private string companyName = string.Empty;
        private string repositoryFolder = string.Empty;
        private string launcherFolder = string.Empty;
        private string setupFolder = string.Empty;
        private string softwareManifestFileName = string.Empty;
        private DirectoryInfo projReleaseDirectoryInfo;
        private DirectoryInfo projSetupDirectoryInfo;
        private DirectoryInfo projLauncherDirectoryInfo;
        private DirectoryInfo repositoryInfo;
        private string pathTempCompany = string.Empty;
        private AssemblyInfoUtil versionUtil = null;

        private DataTable fileTable = new DataTable("Files");

        #endregion

        /// <summary>
        /// Mainline Logic to invoke upon clicking of a Production/Beta button
        /// </summary>
        /// <param name="chosenVersionToPush">Execution Mode</param>
        private void executePushToRepository(ExecuteModeEnum chosenVersionToPush)
        {
            Cursor.Current = Cursors.WaitCursor;
            try
            {
                switch (chosenVersionToPush)
                {
                    case ExecuteModeEnum.Beta:
                        appFolder = Updater.Default.AppSubFolderBeta;
                        repositoryFolder = Updater.Default.RepositoryDefault + 
				@"\" + Updater.Default.AppSubFolderBeta;
                        break;
                    case ExecuteModeEnum.Production:
                        appFolder = Updater.Default.AppSubFolderProd;
                        repositoryFolder = Updater.Default.RepositoryDefault + 
				@"\" + Updater.Default.AppSubFolderProd;
                        break;
                }

                initFileTable();
                DataSet dataSet = new DataSet("SoftwareManifest");

                // Make sure external Repository exists
                if (!repositoryInfo.Exists)
                    repositoryInfo.Create();

                // walk through the chosen app folder
                // and zip the files therein to the appropriate temp folder

                if (projReleaseDirectoryInfo.Exists)
                {
                    if (projSetupDirectoryInfo.Exists)
                    {
                        // Make sure temp folder is empty so we don't undo a previous push
                        DirectoryInfo di = new DirectoryInfo(pathTempCompany);
                        if (di.Exists)
                        {
                            foreach (DirectoryInfo prevDirectoryInfo 
				in di.GetDirectories())
                            {
                                if (prevDirectoryInfo.Exists)
                                    prevDirectoryInfo.Delete(true);
                            }
                        }
                        // zip the files in the main application folder
                        zipFilesInFolder(projReleaseDirectoryInfo.FullName, 
			pathTempCompany + "\\" + appFolder, 
			projReleaseDirectoryInfo, true, true);
                        listResults.Items.Add(" ");

                        // zip the files in the setup Folder (msi will be skipped)
                        zipFilesInFolder(projSetupDirectoryInfo.FullName, 
			pathTempCompany + "\\" + appFolder + "\\" + setupFolder, 
			projSetupDirectoryInfo, true, true);
                        listResults.Items.Add(" ");

                        // create and zip the software manifest file
                        string manifestFile = pathTempCompany + "\\" + 
				appFolder + "\\" + softwareManifestFileName;
                        if (File.Exists(manifestFile))
                        {
                            File.Delete(manifestFile);
                        }
                        listResults.Items.Add("Creating Software Manifest File:  " + 
			softwareManifestFileName);
                        listResults.Items.Add(" ");
                        dataSet.Tables.Add(fileTable);
                        dataSet.WriteXml(manifestFile);

                        // Write files to repository folder (cleaned above)
                        UpdateRepository(repositoryInfo);
                        listResults.Items.Add("Program Completed...");
                        listResults.Items.Add(" ");
                    }
                    else
                    {
                        listResults.Items.Add("ProjSetupFolder| " + 
			projSetupDirectoryInfo.FullName + " does not exist!");
                    }
                }
                else
                {
                    listResults.Items.Add("ProjReleaseFolder| " + 
			projReleaseDirectoryInfo.FullName + " does not exist!");
                }

            }
            catch (Exception ex)
            {
                listResults.Items.Add(ex.Message);
                listResults.Items.Add(ex.StackTrace.ToString());
            }
            listResults.Refresh();
            listResults.TopIndex = listResults.Items.Count - 1;
            Cursor.Current = Cursors.Default;
        }

        /// <summary>
        /// Clear Repository, Transfer Directorys, then Transfer Files
        /// </summary>
        /// <param name="repositoryInfo">Repository DirectoryInfo</param>
        private void UpdateRepository(DirectoryInfo repositoryInfo)
        {

            // 1) Create directories in Repository and wipe out any existing data
            listResults.Items.Add("Clearing Existing Repository: " + 
			repositoryInfo.FullName);
            DirectoryInfo updateDir = new DirectoryInfo(repositoryFolder);
            if (updateDir.Exists)
            {
                listResults.Items.Add("  Deleting Directory and SubDirectories: " + 
			updateDir.FullName);
                updateDir.Delete(true);
            }
            listResults.Items.Add(" ");

            listResults.Items.Add("Transferring Directories");
            DirectoryInfo workingDir = new DirectoryInfo(pathTempCompany);

            TransferDirectories(workingDir, workingDir.FullName);
            TransferFiles(workingDir, workingDir.FullName);
        }

        /// <summary>
        /// Move the entire directories and files to the target Repository
        /// </summary>
        /// <param name="directoryInfo">Source DirectoryInfo</param>
        /// <param name="baseDir">The To Directory Target</param>
        private void TransferDirectories(DirectoryInfo directoryInfo, string baseDir)
        {
            DirectoryInfo workingDirectoryInfo;
            string workingDir = string.Empty;
            foreach (DirectoryInfo di in directoryInfo.GetDirectories())
            {
                workingDir = Updater.Default.RepositoryDefault + 
			di.FullName.Remove(0, baseDir.Length + 1);
                workingDirectoryInfo = new DirectoryInfo(workingDir);
                if (!workingDirectoryInfo.Exists)
                    workingDirectoryInfo.Create();
                TransferDirectories(di, baseDir);
                listResults.Items.Add("  Transferred Directory: " + di.FullName);
                TransferFiles(di, baseDir);
                listResults.Items.Add(" Transferred Files from " + di.FullName);
                listResults.Items.Add(" ");
                listResults.Refresh();
                listResults.TopIndex = listResults.Items.Count - 1;
            }
        }

        /// <summary>
        /// Transfer files from the DirectoryInfo to the baseDirectory Repository
        /// </summary>
        /// <param name="directoryInfo">The From Directory to copy files</param>
        /// <param name="baseDir">The To Directory target</param>
        private void TransferFiles(DirectoryInfo directoryInfo, string baseDir)
        {
            FileInfo workingFile;
            string workingDir = string.Empty;
            foreach (FileInfo f in directoryInfo.GetFiles())
            {
                workingFile = new FileInfo(f.FullName);
                workingDir = workingFile.FullName.Remove(0, baseDir.Length + 1);
                workingDir = Updater.Default.RepositoryDefault + workingDir;
                workingFile.CopyTo(workingDir);
            }
        }

        /// <summary>
        /// Create File Table with the appropriate columns
        /// </summary>
        private void initFileTable()
        {
            fileTable = new DataTable("Files");
            fileTable.Columns.Add(new DataColumn("sourcePathName", typeof(string)));
            fileTable.Columns.Add(new DataColumn("sourceFileName", typeof(string)));
            fileTable.Columns.Add(new DataColumn("zippedPathName", typeof(string)));
            fileTable.Columns.Add(new DataColumn("zippedFileName", typeof(string)));
            fileTable.Columns.Add(new DataColumn("md5HashedValue", typeof(string)));
        }

        /// <summary>
        /// Make the listResults listbox visible and set the Text, Tag, 
        /// and BAckColor of various buttons as needed
        /// </summary>
        private void makeListResultsVisible()
        {
            webBrowser.Visible = false;
            webBrowser.Navigate(string.Empty);
            listResults.Visible = true;
            btnViewRepositorySite.Text = "View &Repository Site";
            btnViewRepositorySite.Tag = null;
            btnViewRepositorySite.BackColor = DefaultBackColor;
            btnViewRepositorySite.Refresh();
            btnViewTempFolder.BackColor = DefaultBackColor;
            btnViewTempFolder.Refresh();
        }

        /// <summary>
        /// Obtain and Manipulate the program's settings from misc. values
        /// </summary>
        private void obtainSettingValues()
        {
            string pathTempFolder = string.Empty;
            try
            {
                listResults.Items.Clear();

                // By putting the setup in the beta/prod folders 
                // it allows you to process a "Beta" setup file without
                // touching users in production
                setupFolder = Updater.Default.AppSubFolderSetup;

                // This is the xml file managing your patches
                softwareManifestFileName = Updater.Default.SoftwareManifestFileName;

                // local path to your release project
                projReleaseDirectoryInfo = new DirectoryInfo
				(Updater.Default.ProjecctReleaseFolder);

                // local path to your Setup Project
                projSetupDirectoryInfo = new DirectoryInfo
				(Updater.Default.SetupReleaseFolder);

                // local path to your repository
                repositoryInfo = new DirectoryInfo(Updater.Default.RepositoryDefault);

                // create folders and zip files
                // as needed in a temporary folder

                pathTempFolder = Path.GetTempPath();
                DirectoryInfo di = new DirectoryInfo(pathTempFolder);

                pathTempCompany = pathTempFolder + Updater.Default.CompanyName;
                di = new DirectoryInfo(pathTempCompany);
                if (di.Exists)
                {
                    di.Delete(true);
                }
                di.Create();
            }
            catch (Exception ex)
            {
                listResults.Items.Add(ex.Message + "\r\n" + ex.StackTrace.ToString());
            }
        }

        /// <summary>
        /// Zip all of the files in the source folder to the 
        /// specified temporary folder deriving MD5 hash values along the way, 
        /// saving them to a datatable for later usage in outputting the manifest XML file.
        /// </summary>
        /// <param name="sourceFolderFullName">Source Folder Full Name</param>
        /// <param name="tempFolder">Temporary Folder Path</param>
        /// <param name="directoryInfo">DirectoryInfo object</param>
        /// <param name="omitVsHostExecutables">Whether to omit unneeded 
        /// Visual Studio files</param>
        /// <param name="dontZipInstallers">Whether or not to zip the MSI file</param>
        private void zipFilesInFolder(string sourceFolderFullName, 
		string tempFolder, DirectoryInfo directoryInfo, 
		bool omitVsHostExecutables, bool dontZipInstallers)
        {
            string targetSubFolderName = directoryInfo.FullName.Substring
		(sourceFolderFullName.Length);
            while (targetSubFolderName.StartsWith("\\"))
            {
                targetSubFolderName = targetSubFolderName.Substring
				(1, targetSubFolderName.Length - 1);
            }
            string targetFolderName = tempFolder + "\\" + targetSubFolderName;
            while (targetFolderName.EndsWith("\\"))
            {
                targetFolderName = targetFolderName.Substring
				(0, targetFolderName.Length - 1);
            }
            // zip each file/sub-folder in the present folder
            foreach (FileInfo fi in directoryInfo.GetFiles())
            {
                // Get MD5 Hash
                string md5Hash = string.Empty;
                using (FileStream oStream = System.IO.File.OpenRead(fi.FullName))
                {
                    byte[] obuffer = new byte[oStream.Length];
                    oStream.Read(obuffer, 0, obuffer.Length);
                    byte[] hashValue = 
			new MD5CryptoServiceProvider().ComputeHash(obuffer);
                    md5Hash = BitConverter.ToString(hashValue).ToLower();
                    oStream.Close();
                }

                // if .vshost executables are allowed
                // or the filename does not contain "vshost"
                // zip or copy the file as needed
                if (!omitVsHostExecutables || !fi.Name.ToLower().Contains("vshost"))
                {
                    // if MSI installers are not to be zipped
                    // then copy them unmolested to the target folder
                    if (dontZipInstallers && fi.Name.ToLower().EndsWith(".msi"))
                    {
                        string tgtFileName = targetFolderName + "\\" + fi.Name;
                        if (File.Exists(tgtFileName))
                        {
                            File.Delete(tgtFileName);
                        }
                        FileInfo tgtFileInfo = new FileInfo(tgtFileName);
                        if (!tgtFileInfo.Directory.Exists)
                        {
                            Directory.CreateDirectory(tgtFileInfo.Directory.FullName);
                        }
                        fi.CopyTo(tgtFileName);

                        addFileRow(tgtFileInfo.Directory.FullName.Substring
				(pathTempCompany.Length), fi.Name,
                            tgtFileInfo.Directory.FullName.Substring
				(pathTempCompany.Length), tgtFileInfo.Name,
                            md5Hash, 0);
                    }
                    //otherwise
                    else
                    {
                        listResults.Items.Add("Zipping file " + fi.FullName);
                        string zipFileName = targetFolderName + "\\" + fi.Name + ".zip";
                        listResults.Items.Add("    into " + zipFileName + "...");
                        listResults.Items.Add(" ");
                        listResults.Refresh();
                        listResults.TopIndex = listResults.Items.Count - 1;
                        if (File.Exists(zipFileName))
                        {
                            File.Delete(zipFileName);
                        }
                        FileInfo zipFileInfo = new FileInfo(zipFileName);
                        if (!zipFileInfo.Directory.Exists)
                        {
                            Directory.CreateDirectory(zipFileInfo.Directory.FullName);
                        }
                        using (ZipOutputStream oZipStream = 
			new ZipOutputStream(File.Create(zipFileName)))
                        {
                            oZipStream.Password = Updater.Default.ZipFilePassword;
                            oZipStream.PutNextEntry(new ZipEntry(fi.Name));

                            FileStream oStream = File.OpenRead(fi.FullName);
                            byte[] obuffer = new byte[oStream.Length];
                            oStream.Read(obuffer, 0, obuffer.Length);

                            addFileRow(zipFileInfo.Directory.FullName.Substring
				(pathTempCompany.Length), fi.Name,
                                zipFileInfo.Directory.FullName.Substring
				(pathTempCompany.Length), zipFileInfo.Name,
                                md5Hash);

                            oZipStream.Write(obuffer, 0, obuffer.Length);
                            oZipStream.Finish();
                            oZipStream.Close();
                            oStream.Close();
                        }
                    }
                }
            }
            foreach (DirectoryInfo di in directoryInfo.GetDirectories())
            {
                zipFilesInFolder(sourceFolderFullName, tempFolder, di, 
			omitVsHostExecutables, dontZipInstallers);
            }
        }

        private void addFileRow(string sourcePathName, 
		string sourceFileName, string zippedPathName, string zippedFileName,
            string md5hashedValue,  int insertLoc = -1)
        {
            DataRow dataRow = fileTable.NewRow();
            dataRow["sourcePathName"] = sourcePathName;
            dataRow["sourceFileName"] = sourceFileName;
            dataRow["zippedPathName"] = zippedPathName;
            dataRow["zippedFileName"] = zippedFileName;
            dataRow["md5hashedValue"] = md5hashedValue;

            if (insertLoc > -1)
                fileTable.Rows.InsertAt(dataRow, insertLoc);
            else
                fileTable.Rows.Add(dataRow);
        }

        /// <summary>
        /// Update AssemblyInfoUtil class Version Numbers
        /// </summary>
        private void versionTbox_ValueChanged(object sender, EventArgs e)
        {
            if (!versionUtil.fileisDirty)
                versionUtil.fileisDirty = true;
            if (version1Tbox.Value != versionUtil.Version1)
                versionUtil.Version1 = (int)(version1Tbox.Value);
            if (version2Tbox.Value != versionUtil.Version2)
                versionUtil.Version2 = (int)(version2Tbox.Value);
            if (version3Tbox.Value != versionUtil.Version3)
                versionUtil.Version3 = (int)(version3Tbox.Value);
            if (version4Tbox.Value != versionUtil.Version4)
                versionUtil.Version4 = (int)(version4Tbox.Value);
        }
    }
}

Launcher Project

  • Click Add New Project, Windows Forms Project
  • Create a new form for the Launcher and link it to Program.cs
    • Initialize stores any arguments passed so you can pass them to your actual program.
    • Instantiate and use CheckForUpdate class from earlier to manage downloads of individual files needed. (Rather than patch all every time)
    • These are handled in background workers to ensure the main UI thread is not completely dead to the end user, and gave a lot of flexibility in debugging.

Code Behind for Launcher Form

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Windows.Forms;

using Microsoft.Win32;

using SelfUpdatingSettings.Classes;
using SelfUpdatingSettings;

namespace SelfUpdatingLauncher
{
    public partial class SelfUpdatingLauncher : Form
    {
        public SelfUpdatingLauncher(string[] passingArgs)
        {
            InitializeComponent();
            launchArgs = passingArgs;
            foreach (string arg in launchArgs)
            {
                if (arg.ToLower() == "/beta")
                {
                    execMode = ExecuteModeEnum.Beta;
                }
            }
        }

        private ExecuteModeEnum execMode = ExecuteModeEnum.Production;
        private bool launchSetup = false;
        private CheckForUpdate updateChecker = null;
        private string[] launchArgs;
        private bool closeOnExit = true;
        private BackgroundWorker bg;
        /// <summary>
        /// The Number of Seconds to wait until updating starts.
        /// </summary>
        private int timeout = 1;

        private void Form_Load(object sender, EventArgs e)
        {
            this.TransparencyKey = this.BackColor;
            BackgroundWorker bgStarter = new BackgroundWorker();
            bgStarter.RunWorkerCompleted += new RunWorkerCompletedEventHandler
					(bgStarter_RunWorkerCompleted);
            bgStarter.DoWork += new DoWorkEventHandler(bgStarter_DoWork);
            bgStarter.ProgressChanged += new ProgressChangedEventHandler
					(bgStarter_ProgressChanged);
            bgStarter.WorkerReportsProgress = true;
            bgStarter.RunWorkerAsync(timeout);
        }

        #region Start Program Worker

        private void bgStarter_DoWork(object sender, DoWorkEventArgs e)
        {
            // If we error out that's okay, we aren't 
            // returning anything to the completed Event
            BackgroundWorker bgStarter = (BackgroundWorker)sender;
            for (int i = ((int)e.Argument); i >= 0; i--)
            {
                bgStarter.ReportProgress(0,"Starting Program in " + 
				i.ToString() + " second(s).");
                Thread.Sleep(999);
            }
            bgStarter.ReportProgress(0, "Checking for Update.");
        }

        private void bgStarter_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            updateStatus((string)e.UserState, e.ProgressPercentage);
            listBoxProgress.TopIndex = listBoxProgress.Items.Count - 1;
        }

        private void bgStarter_RunWorkerCompleted
		(object sender, RunWorkerCompletedEventArgs e)
        {
            closeButton.Enabled = true;
            updateChecker = new CheckForUpdate(execMode);
            updateStatus(updateChecker.Filecount.ToString() + " file(s) reviewed, " 
		+ updateChecker.UpdateNeeded.Rows.Count.ToString() + 
		" files found to update.", 0);
            if (updateChecker.MainUpdateNeeded |
                updateChecker.SetupUpdateNeeded)
            {
                progressBar1.Minimum = 0;
                progressBar1.Maximum = updateChecker.UpdateNeeded.Rows.Count;
                updateStatus("Update Starting...", 0);

                bg = new BackgroundWorker();
                bg.DoWork += new DoWorkEventHandler(downloadFile_DoWork);
                bg.RunWorkerCompleted += new RunWorkerCompletedEventHandler
					(downloadFile_Completed);
                bg.ProgressChanged += new ProgressChangedEventHandler
					(downloadFiles_Progress);
                bg.WorkerSupportsCancellation = true;
                bg.WorkerReportsProgress = true;
                bg.RunWorkerAsync(updateChecker.UpdateNeeded);
            }
            else
            {
                updateStatus("Update Not Required...", 0);
                DoClose();
            }
        }

        #endregion

        #region Fileworkers
        private void downloadFiles_Progress(object sender, ProgressChangedEventArgs e)
        {
            updateStatus((string)e.UserState, e.ProgressPercentage);
            listBoxProgress.TopIndex = listBoxProgress.Items.Count - 1;
        }

        private void downloadFile_Completed(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Result is bool)
            {
                if ((bool)e.Result)
                {
                    return;
                }
            }
            if (e.Error == null)
            {
                // Setup Launch will happen on form close if needed
                launchSetup = updateChecker.SetupUpdateNeeded;
            }
            else
            {
                MessageBox.Show(e.Error.Message + "\r\n" + e.Error.StackTrace, 
			"Failed to Update Files!", MessageBoxButtons.OK);
            }
            DoClose();
        }

        private void downloadFile_DoWork(object sender, DoWorkEventArgs e)
        {
            bg = (BackgroundWorker)sender;
            DataTable dt = (DataTable)e.Argument;
            DataRow row;
            for (int i = 0; i < dt.Rows.Count; i++)
            {
                if (bg.CancellationPending)
                {
                    e.Result = true;
                    break;
                }
                row = dt.Rows[i];
                string zippedFileName = (string)row["zippedfilename"];
                string zippedPathName = (string)row["zippedpathname"];
                string sourceFileName = (string)row["sourceFileName"];
                string sourcePathName = (string)row["sourcePathName"];

                string sourceFullPath = updateChecker.CompanyDir + 
		sourcePathName.Substring(1) + "\\" + sourceFileName;
                string zippedPath = updateChecker.CompanyDir + 
		sourcePathName.Substring(1) + "\\" + zippedFileName;
                string zipFullPath = updateChecker.DownloadUri + 
		zippedPathName + "/" + zippedFileName;
                zipFullPath = zipFullPath.Replace("//\\", "//");
                string subdirPath = updateChecker.CompanyDir + sourcePathName.Substring(1);
                if (updateChecker.DownloadFile(zipFullPath, zippedPath, 
			sourceFullPath, subdirPath))
                {
                    bg.ReportProgress(i + 1, sourceFullPath + " updated Successfully.");
                }
                else
                {
                    bg.ReportProgress(i + 1, sourceFullPath + " failed to update!");
                    bg.ReportProgress(i + 1, updateChecker.CheckException.Message + 
			"\r\n" + updateChecker.CheckException.StackTrace);
                }
            }
        }
        #endregion

        private void updateStatus(string statusUpdate, int percentageUpdate)
        {
            listBoxProgress.Items.Add(statusUpdate);
            labelSecondsBeforeUpdate.Text = statusUpdate;
            listBoxProgress.Refresh();
            if (percentageUpdate > 0)
            {
                progressBar1.Value = percentageUpdate;
            }
        }

        private void DoClose()
        {
            updateStatus("Finished Processing...", progressBar1.Maximum);
            if (closeOnExit)
            {
                this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
                this.Close();
            }
        }

        private void Event_FormClosed(object sender, FormClosedEventArgs e)
        {
            // First click of optionsButton sets the Tag to true and 
            // tells the program to not "autoclose" at the end
            // Second click of optionsButton sets the Tag to false and 
            // tells the program to not launch when form is closed
            bool auto = false;
            if (optionsButton.Tag == null)
            {
                auto = true;
            }
            else
            {
                auto = (bool)optionsButton.Tag;
            }
            if (auto)
            {
                try
                {
                    using (Process p = new Process())
                    {
                        // If no arguments were passed to start application 
                        // it was launched from the setup and does not need 
                        // to launch the Setup again
                        if (launchSetup &&
                            launchArgs.Length > 0)
                        {
                            p.StartInfo = new ProcessStartInfo
					(updateChecker.SetupFilePath);
                            p.StartInfo.Arguments = "/passive";
                        }
                        else
                        {
                            p.StartInfo.FileName = updateChecker.LauncherPath + 
					Updater.Default.ApplicationExecutable;
                            p.StartInfo = new ProcessStartInfo
				(updateChecker.LauncherPath + 
				Updater.Default.ApplicationExecutable,
                                StaticClasses.getStringArrayValuesAsString
				(launchArgs, " ", false));
                            p.StartInfo.WorkingDirectory = updateChecker.LauncherPath;
                        }
                        p.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
                        p.Start();
                    }
                }
                catch (Exception exd)
                {
                    MessageBox.Show(exd.Message + "\r\n" + exd.StackTrace, 
			"Failed to Launch", MessageBoxButtons.OK);
                }
            }
        }

        #region Buttons
        private void btnClose_Click(object sender, EventArgs e)
        {
            // Make sure worker is initialized
            if (bg != null)
            {
                // Check if it's busy, and interrupt it if it is
                if (bg.IsBusy)
                    bg.CancelAsync();

                // Need to wait until it drops out
                while (bg.IsBusy)
                    Thread.Sleep(999);
            }

            this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
            this.Close();
        }

        private void optionsButton_Click(object sender, EventArgs e)
        {
            if (optionsButton.Tag == null)
            {
                updateStatus("User Requested Manual Close, 
			downloading will continue.", 0);
                closeOnExit = false;
                optionsButton.Tag = true;
            }
            else if ((bool)optionsButton.Tag)
            {
                updateStatus("User Requested Auto-Launch turned off, 
			downloading will continue.", 0);
                optionsButton.Tag = false;
            }
        }

        #endregion
    }
}

Code Behind for Installer Class (works with Setup to clean out directory on uninstall as stated previously. The actual program we are using is not part of the Launcher, so we have to clean it up manually in the event of an uninstall.)

C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Linq;

using System.Diagnostics;
using System.IO;
using SelfUpdatingSettings.Classes;


namespace SelfUpdatingLauncher
{
    [RunInstaller(true)]
    public partial class SelfUpdatingInstaller : System.Configuration.Install.Installer
    {
        public SelfUpdatingInstaller()
        {
            InitializeComponent();
        }

        private void SelfUpdatingInstaller_AfterUninstall
			(object sender, InstallEventArgs e)
        {
            try
            {
                DirectoryInfo d = new DirectoryInfo(StaticClasses.InstallPath);
                if (d.Exists)
                {
                    d.Delete(true);
                }
            }
            catch { }
        }
    }
}

Setup Project

  • Set Launcher as startup project.
  • Right Click and choose add new project.
  • Under Windows, Other Project Types, Setup and Deployment, Visual Studio Installer
  • Name it appropriately, it will default to the File System folder.
  • Right click on your new setup project and click "Add->Project Output"
  • Choose Primary Output, and ensure your Launcher project is selected in the top dropdown.
  • It should automatically add a dependency to ICSharp, .NET, and SelfUpdatingSettings.dll
  • Click back on the File System window that automatically opened when you created the setup project.
  • Click on User's Program Folder and choose properties, change Always Create to true
  • Right click in the blank area with Name/Type of User's Program Folder and choose Create New Shortcut.
  • Double Click Application Folder and select Project Output
  • Go to Properties of the new shortcut and add /prod to arguments, modify any other properties you may want like Icon and Name.
  • Create another shortcut using the same method but under arguments put /beta to the second one. The name should also indicate this is the Beta Version.
  • Optionally repeat this process for the User's Desktop portion to place a shortcut on the Desktop.
  • Right click on the Setup Project Again and choose View -> Custom Actions
  • In the Custom Actions window right click on Custom Action's again, then Add
  • Select Application Folder and double click on Project Output
  • Right Click on your SelfUpdatingLauncher Project and choose Add -> New Item
  • Select Installer Class from the list and name it appropriately (mine is SelfUpdatingInstaller)
  • Double Click the new Installer Class, go to properties, then events, and add an event for After Uninstall and Committed
  • Go to the newly created events
    • AfterUninstall deletes the custom Directory and all subfolders/files where we are downloading them manually
    • Committed launches the LauncherProgram for the first time (this is optional) after Install is committed
  • Select the Setup Project and go to properties.
  • Ensure Remove Previous Versions is set to TRUE.
  • Ensure Install All Users is set to TRUE.
  • Set the other pertinent options (Name, URLs, support, etc.)
  • Set PostBuildAction as
    • cscript.exe "$(ProjectDir)EnableLaunchApplication.js" "$(BuiltOuputPath)"
  • Copy EnableLaunchApplication.js from the source package, edit the executing program to match your launcher project name, and put this file in the same folder as your Setup .vdproj file.

IExpress Setup

So Windows XP/Vista/7 all include a nifty tool for packaging setups called IEXPRESS. Right click on your Setup project and click Open Folder in Windows Explorer and copy the setup.exe and .msi file to a folder you can get to from command prompt.

Very Important: If you want to be able to run this new setup file from a 32 bit machine, it must be created in a 32 bit environment, it will still work on 64 bit machines and does not affect your application only the setup file.

Click Start, Run, cmd, then navigate to the proper folder.

Note: If you create the SED and installation file in the release directory of your setup program, the SED and related files will also be pushed to your patches (not necessary).

Image 7

Click Next to Create a new Self Extraction Directive file. Click Next again to Extract files and run an installation command. Choose a title for your package and click Next. Choose whether you want to prompt the user and click Next again. Determine if you want to display a license and click Next again. Click Add and add both the setup.exe and .msi file to your package.

Image 8

Click Next and select setup.exe from the Install Program drop down box.

Image 9

Select how you want the window to show while installing (I use the default here) and click next again. Decide if you want to show a message after installation is complete and click next. (I choose No Message as I'm going for an unattended setup). Choose a Name and location for your package on the next screen and click Next. To select a location, click Browse and navigate where you want to save it, and type a new name for it, I used SUPInstaller.

Image 10

Select how you want the Restart after installation handled (I can't think of a reason to restart here, so I change the option to "No restart" and click next. Click next again to save the Self Extraction Directive (SED) file and click next again to create the package. Click Finish. Now from your command prompt, type notepad SUPInstaller.SED and modify the AppLaunched=setup.exe line to read AppLaunched=setup.exe /passive in order to have a completely passive installation. Note by running the setup.exe program instead of the MSI, all appropriate components (such as proper .NET version) will be downloaded and handled as well (these will prompt the user).

Image 11

Simply run iexpress again from the command prompt except this time open an existing SED (the one you just modified) and click next. Then click next again to re-create the package (this time with our /passive option) and click Finish. You can post this setup file in a more readily available site, thus not necessarily showing the end user where patch files are loaded. And you can use this setup file forever without re-creating the IEXPRESS package as our program will automatically patch with the latest setup after first load.

Closing

So I hope this helps a lot of people and I was able to give back some of what I have gained by using sites such as CodeProject for my programming needs as both a timesaver and an extraordinary learning resource.

History

  • 7th November, 2010: Initial version

License

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