Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# .NET Autoupdate Application Launcher

0.00/5 (No votes)
8 Dec 2010 1  
Easily lets you post updated versions of your application for remote clients to download without running another setup.

Introduction

When we develop web-based solutions, usually, we are in charge of updating the installations to use a current version. We fix some bugs, add a feature, make a patch, and deploy it so that our customers can continue using our software without interruption. But when we have clients installing our software, we still need to give them updates. Even standalone applications have problems with results from old versions. If an application connects to an external system to push or pull data, the problems can be even worse - even resulting in data loss. Consumers of your application are busy people and they don't always remember to check for updates to your software.

Why do I need an updater?

We wanted a .NET solution that would allow our clients to use the most current version of our software. We had been having problems with a mobile app synching its data across multiple versions, so we enacted a policy to only allow a sync operation from the most current version of the software. This meant that we would have to have a way of easily and quickly updating remote installations.

What does it do?

In short, the Launcher checks the update location for version changes, downloads and unpacks any updates, and then executes the main application. If there aren't any version updates or the update location is not available, the Launcher simply executes the current version of the main application.

How can I write one?

We'll be writing this in .NET 4.0, using Visual Studio 2010 and C#. The unit tests are written in Visual Studio Team Test. A unit test framework of your choice may be used, but the walk through below will use the commands in Team Test.

We store the current version information in a text file. The file is present in the update location, and after an update is successfully downloaded and unpacked, it is saved to the client. When the client's text file and the update location's text file match, the launcher knows the current version is up to date. If they don't match, we'll need to download the update and unpack it. Since our archive can contain multiple files, it'll be easier to use a decompression library other than GZip to unpack it. We'll be using the Ionic DotNetZip library (http://dotnetzip.codeplex.com/).

Let's create our new project. Create a new WPF Application called ArticleAutoUpdater.

Our procedures are small in scope and could easily reside within the application itself, but of course you were going to fully unit test the code, right? It's easier to test a class library, so let's create it.

What does the version text file look like?

app.version
0.11.67.1

As we can see, this is a very simple file. It is one line and all it contains is a full four-value version number. This is only one approach to recording the version. You could also use Reflection or a file system call to identify the version of a particular file, but we're using this file approach so that we can include updates of files that aren't always managed assemblies or even applications.

How do we check our versions?

Let's start by creating a Class Library called ApplicationUpdate. Rename the default Class1 to Versions. Change it to a public static class, since our version operations are all argument-driven.

The first procedure we'll write is to check a version file for the current version number. We know that we need to check two files: one remote via URL, and one local via file path. Let's start with the local version.

public static string LocalVersion(string path)
{
    string lv = "";
    return lv;
}

Now let's create our unit tests for our method.

Our first test case is to open a non-existent file and make sure the version returns null. Let's write a test for it.

Right-click the method and click Create Unit Tests... Make sure the LocalVersion method is selected and select "Create a new Visual C# test project... Click OK. Name your new project ApplicationUpdate.Test. Now rename the test it just generated from LocalVersionTest to LocalVersion_BadFile_Test, since we'll be testing for a bad file.

Since the test framework generated such a nice test for us, we'll just plug some values in and remove the inconclusive line. We'll also check to make sure path doesn't actually point to a real file, since that would invalidate the test.

[TestMethod()]
public void LocalVersionTest()
{
    string path = @"C:\BadFolder\BadVersionFile.version";
    Assert.IsFalse(new System.IO.FileInfo(path).Exists, 
                   "File should not exist.");
    string expected = null;
    string actual;
    actual = Versions.LocalVersion(path);
    Assert.AreEqual(expected, actual);
}

Run the test and make sure we have a failure. Assert.AreEqual failed. Expected:<(null)>. Actual:<>. This is exactly what we expected. Now let's code our method.

If the path points to a file that doesn't exist, we want to return null. That's easy enough, so let's make it happen.

public static string LocalVersion(string path)
{
    string lv = "";

    if (!new System.IO.FileInfo(path).Exists)
    {
        lv = null;
    }

    return lv;
}

Run the test again. Pass! Let's add a couple more bad paths to the test and make sure they all fail. We'll add a drive that doesn't exist, a string that doesn't look like a path, and a null value. All of these are good failure cases. Our test ends up looking like this:

[TestMethod()]
public void LocalVersion_BadFile_Test()
{
    string path = @"C:\BadFolder\BadVersionFile.version";

    Assert.IsFalse(new System.IO.FileInfo(path).Exists, 
                   "File should not exist.");
            
    string expected = null;
    string actual;
    actual = Versions.LocalVersion(path);
    Assert.AreEqual(expected, actual);

    path = @"Z:\ASDcjaksl\fasjldjvalwer";
    actual = Versions.LocalVersion(path);
    Assert.AreEqual(expected, actual);

    path = null;
    actual = Versions.LocalVersion(path);
    Assert.AreEqual(expected, actual);

    path = @"!@#411j320vjal;sdkua@!#^%*(@#AVEW  VRLQ#";
    actual = Versions.LocalVersion(path);
    Assert.AreEqual(expected, actual);
}

Case 2, path = @"Z:\ASDcjaksl\fasjldjvalwer", passes already. For case 3, path = null, we need to do a little validation. We'll just add string.IsNullOrEmpty(path).

public static string LocalVersion(string path)
{
    string lv = "";

    if (string.IsNullOrEmpty(path) || 
        !new System.IO.FileInfo(path).Exists)
    {
        lv = null;
    }

    return lv;
}

The null path passes! We are still failing on illegal characters, though. LINQ to the rescue! Add one more condition:

System.IO.Path. GetInvalidPathChars().Intersect(path.ToCharArray()).Count() != 0

Now LocalVersion looks like this:

public static string LocalVersion(string path)
{
    string lv = "";

    if (string.IsNullOrEmpty(path)
        || System.IO.Path.GetInvalidPathChars().Intersect(
                          path.ToCharArray()).Count() != 0
        || !new System.IO.FileInfo(path).Exists)
    {
        lv = null;
    }

    return lv;
}

And all of our tests are passing. Success! Of course, it's inefficient to keep getting a new FileInfo for every operation - it's just for coding simplicity. Afterall, these tests are pretty small in scope and only do a handful of operations. If you'd like to write them such that they use a single FileInfo instance, please feel free to.

It's time to move on. Now let's open a file that's really there and make sure we handle it correctly. Right-click the method and click Create Unit Tests... again. Make sure the method and the test project are selected, and click OK. Now rename the test to LocalVersion_GoodFile_Test. Let's use the test to actually create (and clean up) a version file so that we know it is testing accurately. Let's not do anything fancy - we'll create a folder and drop the file there.

[TestMethod()]
public void LocalVersion_GoodFile_Test()
{
    string folderPath = "C:\\VersionsTest";

    if (!new System.IO.DirectoryInfo(folderPath).Exists)
    {
        System.IO.Directory.CreateDirectory(folderPath);
    }

    string fileName = "app.version";
    string path = folderPath + "\\" + fileName;
    string expected = "1.2.3.4";

    if (!new System.IO.FileInfo(path).Exists)
    {
        System.IO.File.WriteAllText(path, expected);
        Assert.IsTrue(new System.IO.FileInfo(path).Exists, 
                      "File should exist now.");
    }
                        
    string actual;
    actual = Versions.LocalVersion(path);
    Assert.AreEqual(expected, actual);
}

When we run this, it fails. We expected this, and now we know we aren't accidentally retrieving something. Let's finish up this method.

public static string LocalVersion(string path)
{
    string lv = "";

    if (string.IsNullOrEmpty(path)
        || System.IO.Path.GetInvalidPathChars().Intersect(
                          path.ToCharArray()).Count() != 0
        || !new System.IO.FileInfo(path).Exists)
    {
        lv = null;
    }
    else if (new System.IO.FileInfo(path).Exists)
    {
        string s = System.IO.File.ReadAllText(path);
        if (!string.IsNullOrEmpty(s))
            lv = s.Trim();
    }

    return lv;
}

And our test passes! But what happens if we try to use a file that isn't our version file? We want the version to return null because the file we are checking does not serve our purpose.

[TestMethod()]
public void LocalVersion_GoodFile_Test()
{
    string folderPath = "C:\\VersionsTest";

    if (!new System.IO.DirectoryInfo(folderPath).Exists)
    {
        System.IO.Directory.CreateDirectory(folderPath);
    }

    string fileName = "app.version";
    string path = folderPath + "\\" + fileName;
    string expected = "1.2.3.4";

    if (!new System.IO.FileInfo(path).Exists)
    {
        System.IO.File.WriteAllText(path, expected);
        Assert.IsTrue(new System.IO.FileInfo(path).Exists, 
                      "File should exist now.");
    }

    string actual;
    actual = Versions.LocalVersion(path);
    Assert.AreEqual(expected, actual);

    path = @"C:\Program Files\Microsoft Visual " + 
           @"Studio 10.0\Common7\Tools\errlook.exe";
    expected = null;
    actual = Versions.LocalVersion(path);
    Assert.AreEqual(expected, actual, "This is not a good version file!");
}

We need to validate it! Refactor the code that creates the file and writes it. We'll use this later.

public static string CreateLocalVersionFile(string folderPath, 
                     string fileName, string version)
{
    if (!new System.IO.DirectoryInfo(folderPath).Exists)
    {
        System.IO.Directory.CreateDirectory(folderPath);
    }

    string path = folderPath + "\\" + fileName;

    if (new System.IO.FileInfo(path).Exists)
    {
        new System.IO.FileInfo(path).Delete();
    }

    if (!new System.IO.FileInfo(path).Exists)
    {
        System.IO.File.WriteAllText(path, version);
    }
    return path;
}

Update your tests to run CreateLocalVersionFile instead of duplicating this code. Time for another method. Create your shell:

public static bool ValidateFile(string contents)
{
    bool val = false;
    return val;
}

Now create a unit test. First things first - let's check a valid version string.

[TestMethod()]
public void ValidateFileTest()
{
    string contents = "0.0.0.0";
    bool expected = true;
    bool actual;
    actual = Versions.ValidateFile(contents);
    Assert.AreEqual(expected, actual);
}

And we fail. Regular Expression time. We want to have a set of passing contents as well as a set of failing contents. Let's write that test first.

[TestMethod()]
public void ValidateFileTest()
{
    string[] contentsPass = {
                    "0.0.0.0",
                    "1.2.3.4",
                    "10.2.3.4",
                    "10.20.3.4",
                    "10.20.30.4",
                    "10.20.30.40",
                    "10000.20000.300000000.400000000000000",
                };
    bool expected = true;
    bool actual;
    foreach (string contents in contentsPass)
    {
        actual = Versions.ValidateFile(contents);
        Assert.AreEqual(expected, actual, contents);    
    }

    string[] contentsFail = {
                    "a.0.0.0",
                    "0.b.0.0",
                    "0.0.c.0",
                    "0.0.0.d",
                    "1.4",
                    "10.2.3.4.87",
                    "blah blah",
                    "",
                    null,
                    "!@#^%!*#@($QJVW4.!@$(^T!@#",
                };
    expected = false;
    foreach (string contents in contentsFail)
    {
        actual = Versions.ValidateFile(contents);
        Assert.AreEqual(expected, actual, contents);
    }
}

As always, remember to check for a null. The Regular Expression is pretty simple because we have a very rigid version format (#.#.#.#).

public static bool ValidateFile(string contents)
{
    bool val = false;
    if (!string.IsNullOrEmpty(contents))
    {
        string pattern = "^([0-9]*\\.){3}[0-9]*$";
        System.Text.RegularExpressions.Regex re = 
                    new System.Text.RegularExpressions.Regex(pattern);
        val = re.IsMatch(contents);
    }
    return val;
}

Pass! Now back to the version method; we'll add the validator.

public static string LocalVersion(string path)
{
    string lv = "";

    if (string.IsNullOrEmpty(path)
        || System.IO.Path.GetInvalidPathChars().Intersect(
                          path.ToCharArray()).Count() != 0
        || !new System.IO.FileInfo(path).Exists)
    {
        lv = null;
    }
    else if (new System.IO.FileInfo(path).Exists)
    {
        string s = System.IO.File.ReadAllText(path);
        if (ValidateFile(s))
            lv = s;
        else
            lv = null;
    }
    return lv;
}

Run all of the tests. All green, the code is clean!

What about the remote file?

Now we need to see what the current version is on the update server. The meat of the operation is the same, but we get our file via HTTP instead of from the file system.

Open your IIS Manager and create a new virtual directory called AutoUpdate pointing to the folder we used for our unit test above (C:\VersionsTest). We don't have a MIME type for a .version file, so we'll call it app.txt. Now we'll hit the file from the URL http://localhost/AutoUpdate/app.txt.

Again, we start with a shell of a method.

public static string RemoteVersion(string url)
{
    string rv = "";
    return rv;
}

And the test follows. We're going to reuse the code that created the previous test file, and that works for us because our virtual web folder is a local file system folder.

[TestMethod()]
public void RemoteVersionTest()
{
    string url = "http://localhost/AutoUpdate/app.txt";
    string folderPath = "C:\\VersionsTest";
    string fileName = "app.txt";
    string expected = "11.22.33.44";

    string path = CreateLocalVersionFile(folderPath, 
                                fileName, expected);
            
    string actual;
    actual = Versions.RemoteVersion(url);
    Assert.AreEqual(expected, actual);
}

Naturally, our test fails. We haven't written the method yet! But we're creating our file and we can build, so we are still making progress. Now let's retrieve the contents of our file and validate again.

public static string RemoteVersion(string url)
{
    string rv = "";
            
    System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)
        System.Net.WebRequest.Create(url);
    System.Net.HttpWebResponse response = 
      (System.Net.HttpWebResponse)req.GetResponse();
    System.IO.Stream receiveStream = response.GetResponseStream();
    System.IO.StreamReader readStream = 
      new System.IO.StreamReader(receiveStream, Encoding.UTF8);
    string s = readStream.ReadToEnd();
    response.Close();
    if (ValidateFile(s))
    {
        rv = s;
    }
    return rv;
}

Now that we have a positive test case passing, of course, we need a negative test.

[TestMethod()]
public void RemoteVersion_BadURL_Test()
{
    string url = "http://localhost/AutoUpdate/ZZapp.txt";
    string folderPath = "C:\\VersionsTest";
    string fileName = "app.txt";
    string contents = "11.22.33.44";
    string expected = null;
    string path = 
      CreateLocalVersionFile(folderPath, fileName, contents);

    string actual;
    actual = Versions.RemoteVersion(url);
    Assert.AreEqual(expected, actual);
}

This fails. We aren't handling it. Let's make sure we return null when the file isn't there. We have it easy for our planned usage: if we can't see the file, we assume we are disconnected and, thus, have no update.

public static string RemoteVersion(string url)
{
    string rv = "";

    try
    {
        System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)
        System.Net.WebRequest.Create(url);
        System.Net.HttpWebResponse response = 
          (System.Net.HttpWebResponse)req.GetResponse();
        System.IO.Stream receiveStream = response.GetResponseStream();
        System.IO.StreamReader readStream = 
          new System.IO.StreamReader(receiveStream, Encoding.UTF8);
        string s = readStream.ReadToEnd();
        response.Close();
        if (ValidateFile(s))
        {
            rv = s;
        }
    }
    catch (Exception)
    {
        // Anything could have happened here but 
        // we don't want to stop the user
        // from using the application.
        rv = null;
    }
    return rv;
}

Naturally, this could be more robust and have better error handling, but for our purposes, we either have an update or we don't, so we'll do a really simple error trap. If the user can't see the version file, the Launcher shouldn't try to give them an update.

Now that we can retrieve both our local version and the current version stored on the web server, we can write integration tests to show how we'll be using this information to download an update. Two more tests!

[TestMethod()]
public void CompareVersions_Match_Test()
{
    string url = "http://localhost/AutoUpdate/app.txt";
    // Create our server-side version file.
    string folderPath = "C:\\VersionsTest";
    string fileName = "app.txt";
    string expected = "1.0.3.121";
    string path = CreateLocalVersionFile(folderPath, fileName, expected);

    // Create our local version file.
    fileName = "app.version";
    path = CreateTestFile(folderPath, fileName, expected);

    string localVersion = Versions.LocalVersion(path);
    string remoteVersion = Versions.RemoteVersion(url);

    Assert.AreEqual(localVersion, remoteVersion, 
                    "Versions should match. No update!");
}

[TestMethod()]
public void CompareVersions_NoMatch_Test()
{
    string url = "http://localhost/AutoUpdate/app.txt";
    // Create our server-side version file.
    string folderPath = "C:\\VersionsTest";
    string fileName = "app.txt";
    string expected = "1.0.3.121";
    string path = CreateLocalVersionFile(folderPath, fileName, expected);

    // Create our local version file.
    fileName = "app.version";
    expected = "1.0.4.1";
    path = CreateLocalVersionFile(folderPath, fileName, expected);

    string localVersion = Versions.LocalVersion(path);
    string remoteVersion = Versions.RemoteVersion(url);

    Assert.AreNotEqual(localVersion, remoteVersion, 
      "Versions should not match. We need an update!");
}

Excellent work. We've been working a lot with different folders, but we don't have a testable and repeatable way to create and test for them. We'll write a routine to check, clear, and create our target download folders now.

[TestMethod()]
public void CreateTargetLocationTest()
{
    string downloadToPath = "C:\\AutoDownloadTest\\Downloads";
    string version = "6.2.3.0";
    string expected = downloadToPath + "\\" + version;

    // Delete the folder if it exists, and assert that it isn't there.
    if (new System.IO.DirectoryInfo(expected).Exists)
        new System.IO.DirectoryInfo(expected).Delete();
    Assert.IsFalse(new System.IO.DirectoryInfo(expected).Exists, 
                   "Before we start, it shouldn't exist.");

    string actual;
    actual = Versions.CreateTargetLocation(downloadToPath, version);
    Assert.AreEqual(expected, actual);
    Assert.IsTrue(new System.IO.DirectoryInfo(expected).Exists, 
                  "After we are done, it should exist.");
}

The routine to make the test pass is pretty simple:

public static string CreateTargetLocation(string downloadToPath, string version)
{
    if (!downloadToPath.EndsWith("\\")) // Give a trailing \ if there isn't one
        downloadToPath += "\\";

    string filePath = downloadToPath + version;

    System.IO.DirectoryInfo newFolder = new System.IO.DirectoryInfo(filePath);
    if (!newFolder.Exists)
        newFolder.Create();
    return filePath;
}

Run the test. Pass!

We now know how to get version numbers and compare them, and where to put the files we download. Our next step is to actually do something with this information.

Downloading the new update

Once we realize that we are out of sync, we need to download our update from the server. The actual download is a zip file containing the executable application, essentially the bin folder of your compiled assembly. Let's create an "update" package. For our purposes, we'll use something small and easy, since creating an application to actually be updated is out of the scope for this document.

The zip file is quite cleverly named the same as its version. Let's create a text file called update.txt and put some text in it.

This is the first version.

Zip it! Create a zip file called 1.0.0.0.zip containing the text file.

Now edit update.txt and change it to:

This is the second version.

Create a new zip file called 2.0.0.0.zip containing the newly updated text file. Put both of those zip files into the virtual web site folder we created earlier, C:\VersionsTest.

Our next step is to indicate that 1.0.0.0 is the current version so that our update client will download it. Add a text file to your VersionsTest folder called updateVersion.txt. Edit the file to read 1.0.0.0.

The first thing our AutoUpdater needs is see what version we have and what version we need. We already have both calls, so we'll start with a simple form that will display both the local and the remote versions. In our WPF project, we have a MainWindow form. Add the following definitions to the Grid on the window:

<Grid.ColumnDefinitions>
    <ColumnDefinition></ColumnDefinition>
    <ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
    <RowDefinition></RowDefinition>
    <RowDefinition></RowDefinition>
</Grid.RowDefinitions>

And add the text blocks for version information:

<TextBlock Grid.Row="0" Grid.Column="0">Local version:</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0">Latest version:</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="1" Name="LocalVersion" />
<TextBlock Grid.Row="1" Grid.Column="1" Name="RemoteVersion" />

Add a reference to the ApplicationUpdate library project, so that we can use the logic we've written, and a reference to DotNetZip, so we can decompress the file we download.

Now add a method to the form:

private void CompareVersions()
{
    string downloadToPath = "C:\\VersionsTest\\Downloads";
    string localVersion = 
      ApplicationUpdate.Versions.LocalVersion(downloadToPath + "version.txt");
    string remoteURL = "http://localhost/AutoUpdate/";
    string remoteVersion = 
      ApplicationUpdate.Versions.RemoteVersion(remoteURL + "updateVersion.txt");
    string remoteFile = remoteURL + remoteVersion + ".zip";

    LocalVersion.Text = localVersion;
    RemoteVersion.Text = remoteVersion;
}

And call it from the MainWindow() constructor right after InitializeComponent();.

Run the application, and we'll see that our local version is blank and our remote version says 1.0.0.0. Ideally, our variables above would be populated from a configurable source rather than hard-coded. A natural extension of our work here would be to add these to app.config or a settings file. But for now, let's do what our method says we should do and actually compare the versions.

if (localVersion != remoteVersion)
{
    BeginDownload(remoteFile, downloadToPath, remoteVersion, "update.txt");
}

We have to write the BeginDownload procedure, but we know we'll need the URL of the file to download, where to save it, and what file to run when our download is complete.

private void BeginDownload(string remoteURL, string downloadToPath, 
                           string version, string executeTarget)
{
    string filePath = ApplicationUpdate.Versions.CreateTargetLocation(
                                        downloadToPath, version);

    Uri remoteURI = new Uri(remoteURL);
    System.Net.WebClient downloader = new System.Net.WebClient();

    downloader.DownloadFileCompleted += 
      new System.ComponentModel.AsyncCompletedEventHandler(
                                downloader_DownloadFileCompleted);

    downloader.DownloadFileAsync(remoteURI, filePath + ".zip",
        new string[] { version, downloadToPath, executeTarget });
}

First, we need to create the target folders. Then we set up a WebClient to do the HTTP download. We'll be downloading asynchronously so that we can report progress and not tie up the system while we wait. These are all first-party objects and calls, so we don't need to write unit tests for methods that come with .NET. We need an event procedure for this, so we'll create DownloadFileCompleted. We'll store the data we need when the download is complete in the UserState argument on the DownloadFileAsync call.

void downloader_DownloadFileCompleted(object sender, 
                System.ComponentModel.AsyncCompletedEventArgs e)
{
    string[] us = (string[])e.UserState;
    string currentVersion = us[0];
    string downloadToPath = us[1];
    string executeTarget = us[2];

    if (!downloadToPath.EndsWith("\\"))
    // Give a trailing \ if there isn't one
        downloadToPath += "\\";

    // Download folder + zip file
    string zipName = downloadToPath + currentVersion + ".zip";
    // Download folder\version\ + executable
    string exePath = downloadToPath + currentVersion + "\\" + executeTarget;

    if (new System.IO.FileInfo(zipName).Exists)
    {
        using (Ionic.Zip.ZipFile zip = new Ionic.Zip.ZipFile(zipName))
        {
            zip.ExtractAll(downloadToPath + currentVersion,
                Ionic.Zip.ExtractExistingFileAction.OverwriteSilently);
        }
        if (new System.IO.FileInfo(exePath).Exists)
        {
            ApplicationUpdate.Versions.CreateLocalVersionFile(
                      downloadToPath, "version.txt", currentVersion);
            System.Diagnostics.Process proc = 
                      System.Diagnostics.Process.Start(exePath);

        }
        else
        {
            MessageBox.Show("Problem with download. File does not exist.");
        }
    }
    else
    {
        MessageBox.Show("Problem with download. File does not exist.");
    }
}

That's a lot of code all at once, so here's the breakdown:

We download the zip file into our download folder, then unzip it into download\version\, make sure the file we want is present, update the local version file, and finally run the file we specify. Since we are using a Windows Process, the file itself doesn't need to be an executable - anything with an associated application will do. If the file isn't there, clearly we have had a problem. We'll worry about those in a later installment.

Run the application, and see our first version text file open in Notepad (assuming you are still associating .txt files with Notepad!).

Close Notepad and the launcher window. Now change your updateVersion.txt created earlier from 1.0.0.0 to 2.0.0.0. Run the app again, and you'll see the second version text file. Congratulations! You're automatically updating your client software!

Twice the fun is too much

It is important that only one instance of the application is running at a time. After all, we'd have conflicts if we attempted to download a version that is already being downloaded. The attached solution uses Arik Poznanski's excellent blog post (http://blogs.microsoft.co.il/blogs/arik/archive/2010/05/28/wpf-single-instance-application.aspx) to ensure that only a single instance is running.

Next steps

A progress bar for monitoring the download of a large update is certainly nice for users.

A change-based update installer would be nice, too. For the apps I have used this for, I compress the entire bin folder and download it. Having delta-based update sets would make these packages much smaller.

Cleaning up older downloaded versions would save disk space for your users. Keep the version the user had before the latest version starts to download in case the download fails, but clear the older ones.

I'll be addressing each of these in a future installment.

Conclusion

We needed an easy way to allow our remote users to update to the latest version of our software, while still allowing them to run disconnected. Using a Test Driven Development approach, we wrote a WPF application that checks the version of our local copy against the version from a web server, and then downloads, unzips, and executes the new version if one is available.

We can post new updates by zipping our application and dropping the zip file in the web folder, then updating the web folder's version text file to point to the version we just created. Then our clients will recognize the new version the next time they run the application while connected to the internet.

We unit tested all of our own logic, and used a third-party library to process the zip files.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here