Justin Toth's Blog

Justin is a web developer living in Maryland

Manually programming automatic updates into your .NET WPF app

clock February 6, 2011 01:25 by author Justin Toth

I have a WPF app that I created a few weeks ago. For starters, I created an installer using Click Once Deployment. COD's best feature is that it handles updates of your app automatically. You can publish a new version of your app to a web server, and then when users open the app, it'll see that there's a new version and download/install it. However, COD seems to be a half-baked solution and has a number of shortcomings. One example is that there is no good way to add your app to startup on windows boot.

The natural option is to switch over to using a .NET setup project, which generates a .MSI installer for your app. The setup project is a solid solution and allows all of the basic features such as adding your app to startup, start menu, desktop, etc... However, one glaring thing missing is the ability to handle automatic updates, so in this regard it falls short of COD. There are valid reasons for why it doesn't have this ability, which I won't go into in this post, but we still need a way to handle automatic updates in a good way.

The recommended approach is to upload the latest setup (.MSI) file to a web server and then on application startup, you can follow these steps:

 

  1. Check if a new version of the application is available at the web server url.
  2. If a newer version is available, download it.
  3. Generate a .cmd file that will install the new version over the old one.
  4. Run the .cmd file.
  5. Close the existing application.
Here are more detailed instructions and some code so that others don't have to reinvent the wheel.

First, on a web server somewhere, copy the latest .MSI generated by the setup project to that location. Also create an xml file called Version.xml at that location:


<?xml version="1.0" encoding="UTF-8" ?>
<root>
  <versions>
    <version>
      <name>Housters Crawler 1.06</name>
      <number>6</number>
      <url>http://tothsolutions.com/apps/housterscrawler/HoustersCrawler.msi</url>
      <date>2/5/2011 9:00 PM</date>
    </version>
  </versions>
</root>

Next, create a new class in your WPF project called VersionHelper.cs. Also make sure to add a "Version" app key in your app.config appSettings section, which you will increment with each new version and should match the version/number in the xml file on the server.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Net;
using System.Diagnostics;
using System.Windows;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Management;
using System.Threading;

namespace Housters.Tray.Installer
{
    public class VersionHelper
    {
        private string MSIFilePath = Path.Combine(Environment.CurrentDirectory, "HoustersCrawler.msi");
        private string CmdFilePath = Path.Combine(Environment.CurrentDirectory, "Install.cmd");
        private string MsiUrl = String.Empty;

        public bool CheckForNewVersion()
        {
            MsiUrl = GetNewVersionUrl();
            return MsiUrl.Length > 0;
        }

        public void DownloadNewVersion()
        {
            DownloadNewVersion(MsiUrl);
            CreateCmdFile();
            RunCmdFile();
            ExitApplication();
        }

        private string GetNewVersionUrl()
        {
            var currentVersion = Convert.ToInt32(ConfigurationManager.AppSettings["Version"]);
            //get xml from url.
            var url = ConfigurationManager.AppSettings["VersionUrl"].ToString();
            var builder = new StringBuilder();
            using (var stringWriter = new StringWriter(builder))
            {
                using (var xmlReader = new XmlTextReader(url))
                {
                    var doc = XDocument.Load(xmlReader);
                    //get versions.
                    var versions = from v in doc.Descendants("version")
                                   select new
                                   {
                                       Name = v.Element("name").Value,
                                       Number = Convert.ToInt32(v.Element("number").Value),
                                       URL = v.Element("url").Value,
                                       Date = Convert.ToDateTime(v.Element("date").Value)
                                   };
                    var version = versions.ToList()[0];
                    //check if latest version newer than current version.
                    if (version.Number > currentVersion)
                    {
                        return version.URL;
                    }
                }
            }
            return String.Empty;
        }

        private void DownloadNewVersion(string url)
        {
            //delete existing msi.
            if (File.Exists(MSIFilePath))
            {
                File.Delete(MSIFilePath);
            }
            //download new msi.
            using (var client = new WebClient())
            {
                client.DownloadFile(url, MSIFilePath);
            }
        }

        private void CreateCmdFile()
        {
            //check if file exists.
            if (File.Exists(CmdFilePath))
                File.Delete(CmdFilePath);
            //create new file.
            var fi = new FileInfo(CmdFilePath);
            var fileStream = fi.Create();
            fileStream.Close();
            //write commands to file.
            using (TextWriter writer = new StreamWriter(CmdFilePath))
            {
                writer.WriteLine(@"msiexec /i HoustersCrawler.msi /quiet");
            }
        }

        private void RunCmdFile()
        {//run command file to reinstall app.
            var p = new Process();
            p.StartInfo = new ProcessStartInfo("cmd.exe", "/c Install.cmd");
            p.StartInfo.CreateNoWindow = true;
            p.Start();
            //p.WaitForExit();
        }

        private void ExitApplication()
        {//exit the app.
            Application.Current.Shutdown();
        }
    }
}

Lastly, call the code in your WPF app on start. I commented out the lines where I also show in the system tray that a new version is being downloaded so the user knows.

//check for new version.
                var versionHelper = new VersionHelper();
                if (versionHelper.CheckForNewVersion())
                {
                    //NotifyIcon.BalloonTipText = "A new version is being downloaded, the crawler will restart once download is complete...";
                    //NotifyIcon.ShowBalloonTip(5000);
                    versionHelper.DownloadNewVersion();
                }

We're just about done. There's one catch with what we have done above. If you try to install the downloaded .MSI file over the existing app, it will launch the repair/remove wizard instead of overwriting it. Left click on the setup project in solution explorer and then go to the properties pane. Make sure that RemovePreviousVersion is true and also increment the Version. When it asks if you want to change the Product Code, click Yes to generate a new Guid. Lastly, change the Version in your WPF project's Properties/AssemblyInfo.cs.

Each time you release a new version on the server, you should do the following things:

  1. Increment the AssemblyInfo version in your WPF app.
  2. Increment the setup project version via the properties window.
  3. Increment the "Version" app key in your WPF app.
  4. Build the setup project to generate the new .MSI.
  5. Copy the new .MSI to the web server.
  6. Update the version/number in the xml file on the web server.
Now whenever the WPF app is launched, it will check for a new version. If it exists then it will download the new .MSI, generate a .cmd file, run the .cmd file to install the new .MSI over the old one, and exit the app. When the new .MSI is installed then it will launch the app again, assuming that you have set up a custom rule in your setup project for launching the app after install.

You can take it from here, but one tip is that you may not want to only check for a new version on app start. You could easily have some sort of timer that checks every so often for a new version and runs this process async.

 



Clean up your JavaScript Code with Dojo

clock December 19, 2009 19:39 by author Justin Toth

You may have heard of Dojo, the javascript toolkit found at http://www.dojotoolkit.org/. The difference between it and its competitors such as jQuery and Ext JS is that it's much more than a toolkit, it's a full-fledged javascript framework. It provides things such as object-oriented programming (who would've thought you'd ever be able to have class inheritance in javascript?!), template-based widgets (you can dynamically create widgets in javascript that have html templates rather than having to dynamically create the tr's and td's with DOM manipulation), and basic utility functionality.

I've been using the more complex features of dojo, such as OO and template-based widgets, for some years but I never bothered with the basic utility functionality until today. It's pretty cool what dojo lets you do and it makes me sad that I was writing manual javascript code for years to do the same things. Here are a few of the cooler things that dojo lets you do:

1. Finding and looping through DOM elements

dojo.forEach(
                dojo.query("#myDiv img"),
                function(element) {
                    dojo.attr(element, { src: "_images/test.png" });
                }
            );

You can see the use of 3 dojo functions here: dojo.forEach, dojo.query, and dojo.attr. This will loop through all img elements that are children of the element with id "myDiv" and set the src of each image. Dojo.query is very powerful and can let you easily select the element(s) that you want from the page or from a specific parent node.

2. Creating DOM elements

var imgArrow = dojo.create("img", { src: "_images/arrow.png", title: "my arrow!" }, divContainer);

This code creates an image, sets the src and title of it, and appends it to a div, all in 1 line of code!

3. Clearing DOM elements

 dojo.empty(divContainer);

Dojo.empty clears all child elements of the element inputted, no more grabbing all child elements, looping through them, and doing childNode.parentNode.removeChild(childNode).

I've barely scratched the surface of what you can do with dojo but you can go a long way with just these 3 concepts...



Introduction

clock May 3, 2009 19:30 by author Justin Toth

This is my first post and I'm excited to start blogging!! It seems like every day I run into some sort of issue and once I figure out how to solve it, it makes me wish I had a blog so I could post the solution so that others won't have to spend as much time on it as I did.

This blog will be related to .NET development, and more specifically front-end web development. I used to consider myself an ASP.NET developer but I feel like we've come upon a crossroads. Straight ahead is ASP.NET, to the left is pure javascript development, and to the right is Silverlight. You look further down the ASP.NET road and see that there's another branch up beyond this one, ASP.NET going to the left and ASP.NET MVC going to the right. Which road to take??



About the author

Justin

Justin is a senior developer who has been working with .NET since 2003. His main focus is building highly-interactive web applications using ASP.NET MVC and Dojo or jQuery. Visit his company's site at http://tothsolutions.com.

Page List

Sign in