Build a .NET 1.1 Automatic Application MSI Updater

by Peter A. Bromberg, Ph.D.

Peter Bromberg
"A teacher affects eternity; he can never tell where his influence stops."-- Henry B. Adams

Recently I was working with ClickOnce, which is a pretty nifty little technology, and wondering why there isn't something similar for .NET 1.1. Well, actually there is (The "Updater Application Block" and its derivatives), but it is difficult to set up, and I wonder how many people really use it. There are obviously a large number of .NET developers who, for various reasons, aren't exactly jumping into .NET 2.0 in droves. It might be something as simple as that your boss is hesitant to roll out a new technology until "The first Service Pack" (yes, I heard they are actually planning on one) , or it might just be that your organization has a lot of .NET 1.1 apps that don't really need to be upgraded to the next platform.



So I thought about it for a while and I remembered something: When you build an MSI installer, there is a property "Remove Previous Versions" that can be set with a new Installer Build number (not the .NET application version number). So then I reasoned, how about if I could put together a little class library that could be added to any .NET app, and in the app's main Form's Load event, you would call a static method on this. The method would check a WebService and get the latest MSI build version number, and check this against a build number stored in the app's AssemblyInfo file - let's say the Configuration property, which can be used for anything you want? And if the two version strings were different, the Updater method would pop up a timed Modal Form telling you there was a newer version. If you chose to update the app, it would get the network location path of the newest MSI installer from another column in the entry row for that app, and execute it. Since you had set the "Remove previous versions" property, it would automatically uninstall the current version, install the newest version, and then when the install is complete, we would execute the new app EXE and kill off the currently running one. Could it be done?

The answer is, "yes, it can" -- and it is so surprisingly easy, I wonder why I never thought of this before.

So now I present for your reading and codiing pleasure, the first version of my AppUpdater class library, which, with just a few lines of code and some instructions on the setup, can be added to virtually any application! Just think of an app you have on 100 workstations on the network. And you have a new version. And now somebody has to go around to each user and coach them on how to install the newest version. With this AppUpdater library, instead, you put the new MSI installer on the network over the old one, set a row in a database table, and now each user sees something like this:

If the user clicks the UPDATE NOW button, the above procedure takes place. If they don't do anything for 20 seconds, the popup form disappears and they see:

-- or whatever your real Application looks like! So now let's see how this can be done. First, the AppUpdater class library code, then an explanation:

namespace PAB.Utils

{

    using System;

    using System.Configuration;

    using System.Data;

    using System.Diagnostics;

    using System.Reflection;

    using System.Windows.Forms;

    using PAB.Utils.UpdaterWS;

 

    /// <summary>

    /// App Updater Utility Class

    /// </summary>

    public class Updater

    {

        private Updater()

        {

        }

 

        public static bool UserAccept = false;

 

        private static string CheckVersion(Assembly asm)

        {

            object[] objArray = asm.GetCustomAttributes(false);

            string confi = String.Empty;

            foreach (object obj in objArray)

            {

                AssemblyConfigurationAttribute conf =

                    obj as AssemblyConfigurationAttribute;

                if (conf != null)

                    confi = conf.Configuration;

            }

            return confi;

        }

 

        public static bool CheckForUpdate()

        {

            bool retval = false;

            string appName = Assembly.GetEntryAssembly().GetName().Name;

            UpdateService svc = new UpdateService();

            svc.Url = ConfigurationSettings.AppSettings["webServiceUri"].ToString();

            DataSet ds = svc.CheckAppStatus(appName);

            DataTable dt = ds.Tables[0];

            DataRow dRow = dt.Rows[0];

            string lastVersion = CheckVersion(Assembly.GetEntryAssembly());

            string currentVersion = (string) dRow["CurrentVersion"];

            if (currentVersion != lastVersion)

            {

                UpdateInfo inf = new UpdateInfo();

                inf.BringToFront();

                DialogResult res = inf.ShowDialog();

 

                if (Updater.UserAccept == true)

                {

                    Process p = new Process();

                    p.StartInfo.FileName = (string) dRow["MSIFileNetworkPath"];

                    string appExeName = (string) dRow["appExeName"];

                    p.Start();

                    p.WaitForExit();

                    Process.Start(appExeName);

                    Environment.Exit(0);

                }

                else if ("Cancel" == res.ToString())

                {

                    retval = false;

                }

                retval = true;

            }

            return retval;

        }

    }

}

Now here is how the static CheckForUpdate method is called from your "real" Application, usually in the Form_Load event of its main UI form:

private void Form1_Load(object sender, EventArgs e)
{
Updater.CheckForUpdate();
}

How much easier could it get than that! Literally, just add a reference to the AppUpdater assembly, the one line of code, and you are on your way. Of course, there is some initial setup. You must create the database, create the table and sprocs, and you must set up the WebService. All of this is included in the download below, and once you have set it up in "demo mode" as it is built, it should be very easy for most developers to modify and improve on.

Here are the steps required to get the AppUpdater class library working for your application:

1) Create a new SQL Server database "AppUpdater". Run the supplied SQL Script which will install the AppData Table and the basic CRUD stored procs. The table looks like this:

Note that each row has the actual AppName, the App EXE name, DateLastUpdated, the current (newest) version string, and a network path that uses the Administrative share (C$) pointing to the machine and path of the newest installer. What happens is that a call is made to the Webservice by the AppUpdater component, which checks the AssemblyConfiguration attribute of your current app at load time, and compares this to the CurrentVersion of the row in the table. If they are different, that means you have set up a new version, and this is executed right over the network. Since the new MSI has the RemovePreviousVersion set to True, it automatically uninstalls the old before installing the new. This is all done with the Process Class, and the WaitForExit method. You can see this in the code block for the AppUpdater class above. The SQL Script for this table inserts a single row, and before testing, you will need to adjust the MSIFileNetworkPath column's contents to match the location of your AppUpDaterSetup.msi file using the Administrative share on target machine's location. For example, if you decide to keep your MSI installers on a machine named "Elmo", in the Installers folder on C:, the path would look like so:

\\Elmo\C$\Installers\AppUpdaterSetup.msi

2) You need to set up the WebService on a target webserver, and set the Url property in your App.Config to match its Url:

svc.Url = ConfigurationSettings.AppSettings["webServiceUri"].ToString();

3) Finally, you need to adjust the AssemblyConfiguration attribute --

[assembly : AssemblyConfiguration("1.0.1")]

-- in your application's AssemblyInfo.cs class file to the same new version number that you have chosen for your new MSI installer build, and then update the row in the AppData SQL Server table so that the "CurrentVersion" column reflects this new version string. At this point you are ready to build your new MSI and overwrite the original one at the network location specified.

So, to test this out, you would build a version 1.0.0 with the AssemblyConfiguration attribute to match, put it at the specified location, and run the MSI to install the first version. Then, you would increment the AssemblyConfiguration attribute and the new MSI project version number, being sure to set the "Remove Previous Version" property to true, and build a new MSI, and deploy it to the network over the old. Finally, you would update the version string in the SQL Server CurrentVersion Column. Now when your old app is run, the version strings won't match, and the AppUpdater will take over. Try it-- I hope this little trick will prove as useful to you as it has for me. I showed this to my Mother-in-Law (she's a Southerner) and she said, "Dang, butter mah butt and call me a biscuit!" (or something to that effect). I'm just a transplanted New Yorker in Florida, musing at you Northern slobs sloshing around in 27 inches of snow, and counting my blessings... Enjoy!

Download the Visual Studio.NET 2003 solution that accompanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: