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

Silently updatable single instance WPF ClickOnce application

0.00/5 (No votes)
21 Apr 2013 3  
Silently updatable single instance WPF ClickOnce applicaiton.

Introduction    

ClickOnce provides an API to easily control and customize simple update flows. I needed a mandatory update functionality for my application with minimal user interaction. With this code you can do everything without a user at all or notifying a user and providing, e.g., a restart button. An update of this application runs in the background and provides an event and property when it is completed. The application is single instance and has a restart functionality, which in ClickOnce delivery may not be so easy to implement. 

Using the code  

There's not much code. All the functionality discussed is in the SilentUpdater and SingleInstanceApplication classes. 

Update with SilentUpdater 

This implementation of the ClickOnce update periodically checks for an update and if there's any (required or not), it raises an event, also it has a property signaling if an update's available (UpdateAvailabe). This functionality is in the SilentUpdater class. Also it implements INotifyPropertyChanged, so it provides a PropertyChanged for the property UpdateAvailabe

Why silent? Well, using SilentUpdater, you can update and restart an application without showing to the user all these standard ClickOnce dialogs. When an update is available you can silently restart the application (SingleInstanceApplication has a Restart method) or provide a restarting button/notification for the user. On manual user restart of the application it will also be updated. So this is kind of a mandatory update for the application.

public class SilentUpdater : INotifyPropertyChanged
{
    private readonly ApplicationDeployment applicationDeployment;
    private readonly Timer timer = new Timer(60000);
    private bool processing;

    public event EventHandler<UpdateProgressChangedEventArgs> ProgressChanged;
    public event EventHandler<EventArgs> Completed;
    public event PropertyChangedEventHandler PropertyChanged;
    private bool updateAvailable;
    public bool UpdateAvailable
    {
        get { return updateAvailable; }
        private set { updateAvailable = value; OnPropertyChanged("UpdateAvailable"); }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    private void OnCompleted()
    {
        var handler = Completed;
        if (handler != null) handler(this, null);
    }

    private void OnProgressChanged(UpdateProgressChangedEventArgs e)
    {
        var handler = ProgressChanged;
        if (handler != null) handler(this, e);
    }

    public SilentUpdater()
    {
        if (!ApplicationDeployment.IsNetworkDeployed)
            return;
        applicationDeployment = ApplicationDeployment.CurrentDeployment;
        applicationDeployment.UpdateCompleted += UpdateCompleted;
        applicationDeployment.UpdateProgressChanged += UpdateProgressChanged;
        timer.Elapsed += (sender, args) =>
                                {
                                    if (processing)
                                        return;
                                    processing = true;
                                    try
                                    {
                                        if (applicationDeployment.CheckForUpdate(false))
                                            applicationDeployment.UpdateAsync();
                                        else
                                            processing = false;
                                    }
                                    catch(Exception ex)
                                    {
                                        Debug.Write("Check for update failed. " + ex.Message);
                                        processing = false;
                                    }
                                };
        timer.Start();
    }

    void UpdateProgressChanged(object sender, DeploymentProgressChangedEventArgs e)
    {
        OnProgressChanged(new UpdateProgressChangedEventArgs(e));
    }

    void UpdateCompleted(object sender, AsyncCompletedEventArgs e)
    {
        processing = false;
        if (e.Cancelled || e.Error != null)
        {
            Debug.WriteLine("Could not install the latest version of the application.");
            return;
        }
        UpdateAvailable = true;
        OnCompleted();
    }
} 

The previous version of SilentUpdater uses CheckForUpdateAsync, but  it may lead to an update dialog window appearing if your code checked for updates but didn't apply it and then restarted. Now SilentUpdater calls applicationDeployment.CheckForUpdate(persistUpdateCheckResult:false), which will not persist the updates available state and will not show the update dialog window.  

MSDN says: 

If CheckForUpdate discovers that an update is available, and the user chooses not to install it, ClickOnce will prompt the user that an update is available the next time the application is run. There is no way to disable this prompting. (If the application is a required update, ClickOnce will install it without prompting.)

Also, as long as I use System.Timers.Timer, the Elapsed event handler is launched on the other thread, so the CheckForUpdate method can be used without the risk of freezes.  

Often you can see code like this:   

if (!ApplicationDeployment.IsNetworkDeployed)
    return; 

This means you cannot rely on the fact that CurrentDeployment will be initialized. However, if your application uses only ClickOnce delivery and this is not going to be changed, this checking is redundant. In this case you could get an exception, but this would mean that something absolutely wrong is happening with your application delivery. Microsoft documentation says:  

The CurrentDeployment static property is valid only from within an application that was deployed using ClickOnce. Attempts to call this property from non-ClickOnce applications will throw an exception. If you are developing an application that may or may not be deployed using ClickOnce, use the IsNetworkDeployed property to test whether the current program is a ClickOnce application.

However sometimes it may cause debugging to fail, so I'm using it in my code.  

Single instance and restart in SingleInstanceApplication 

The application is single instance. For this purpose a Mutex is used.

bool createdNew;
instanceMutex = new Mutex(true, @"Local\" + Assembly.GetExecutingAssembly().GetType().GUID, out createdNew);
if (!createdNew)
{
    instanceMutex = null;
    Current.Shutdown();
    return;
} 

The logic is simple - if the mutex of this application for the current user is already created, stop execution. With "Local\" prefix, the mutex for the current user is created, use "Global\" to make the scope of the mutex the current machine.

You should not forget to release and close the mutex on exit or restart: 

private void ReleaseMutex() 
{ 
    if (instanceMutex == null) 
        return;
    instanceMutex.ReleaseMutex();
    instanceMutex.Close();
    instanceMutex = null;
} 

The Restart method is as follows: 

ReleaseMutex(); 
proc.Start();
Current.Shutdown(); 

First the mutex should be released, a new instance of the application starts, and the current instance shuts down.

Restarting updated ClickOnce application may be a problem, because if you restart an executable, the old version will be launched. Sometimes people reference System.Windows.Forms.dll and call

System.Windows.Forms.Application.Restart(); 

but it is too much to achieve what's needed. You can just launch the appref-ms file. 

You can search for it on the Desktop/Start menu or generate the temp appref-ms file.  In the code below you can find both options. In commented section there's a call to CreateClickOnceShortcut which will generate temp appref-ms file. First not commented line in Restart calls  GetShortcutPath wich returns path to appref-ms file generated on installation: 

        public void Restart()
        {
            //var shortcutFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".appref-ms");
            //CreateClickOnceShortcut(tmpFile);

            var shortcutFile = GetShortcutPath();
            var proc = new Process { StartInfo = { FileName = shortcutFile, UseShellExecute = true } };
            
            ReleaseMutex();
            proc.Start();
            Current.Shutdown();
        }

        public static string GetShortcutPath()
        {
            return String.Format(@"{0}\{1}\{2}.appref-ms", Environment.GetFolderPath(Environment.SpecialFolder.Programs), GetPublisher(), GetDeploymentInfo().Name.Replace(".application", ""));
        }

        public static string GetPublisher()
        {
            XDocument xDocument;
            using (var memoryStream = new MemoryStream(AppDomain.CurrentDomain.ActivationContext.DeploymentManifestBytes))
            using (var xmlTextReader = new XmlTextReader(memoryStream))
                xDocument = XDocument.Load(xmlTextReader);

            if (xDocument.Root == null)
                return null;
            
            var description = xDocument.Root.Elements().First(e => e.Name.LocalName == "description");
            var publisher = description.Attributes().First(a => a.Name.LocalName == "publisher");
            return publisher.Value;
        }

        private static ApplicationId GetDeploymentInfo()
        {
            var appSecurityInfo = new System.Security.Policy.ApplicationSecurityInfo(AppDomain.CurrentDomain.ActivationContext);
            return appSecurityInfo.DeploymentId;
        }

        private static void CreateClickOnceShortcut(string location)
        {
            var updateLocation = System.Deployment.Application.ApplicationDeployment.CurrentDeployment.UpdateLocation;
            var deploymentInfo = GetDeploymentInfo();
            using (var shortcutFile = new StreamWriter(location, false, Encoding.Unicode))
            {
                shortcutFile.Write(String.Format(@"{0}#{1}, Culture=neutral, PublicKeyToken=",
                                    updateLocation.ToString().Replace(" ", "%20"),
                                    deploymentInfo.Name.Replace(" ", "%20")));
                foreach (var b in deploymentInfo.PublicKeyToken)
                    shortcutFile.Write("{0:x2}", b);
                shortcutFile.Write(String.Format(", processorArchitecture={0}", deploymentInfo.ProcessorArchitecture));
                shortcutFile.Close();
            }
        } <span style="font-size: 9pt;"> </span>

How to use sample application  

  • Change tmpFileContent (use appref-ms to get the needed content)   Not relevant as long as I removed hardcoded variable 
  • Publish it to some location (make sure that tmpFileContent matches the location you'll set). 
  • Install application from this location. 
  • Publish again to the same location. 
  • Wait for a minute and observe that UI is updated and restart button appears.
  • Press Restart button -> a new application is launched. 
 Sample application will show the updated UI when the update is applied and a restart is needed:

 

History 

  • 21.04.2013 - Hard coded content for appref-ms file removed, now you can dynamically generate this link or use default one generating a path to it. 
  • 21.11.2012 - Update. In the previous version if update checking is performed but the actual update was not finished for some reason, on the next application start, a default dialog appears. Now this state is not persisted, so dialog windows are not shown.  
  • 06.11.2012 - Download link added.   
  • 06.11.2012 - Initial version.

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