Click here to Skip to main content
15,867,771 members
Articles / Desktop Programming / WPF

Silently updatable single instance WPF ClickOnce application

Rate me:
Please Sign up or sign in to vote.
4.95/5 (28 votes)
21 Apr 2013CPOL4 min read 61.5K   1.3K   84   23
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.

C#
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:   

C#
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.

C#
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: 

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

The Restart method is as follows: 

C#
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

C#
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: 

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead Bloomberg LP
United States United States
10+ years of software design and development.

Using C/C++, C#, JavaScript, Java, Python, AngularJS, React, React Native, .NET, WinAPI, WPF, PureMVC, MVVM, MVC, Unix/Linux, AWS, Azure, Heroku, Git, grafana, InfluxDB, TeamCity, Jenkins, Gulp, MS SQL, PostgreSQL, MySQL, Snowflake SQL, MongoDB

http://ileonenko.wordpress.com

Comments and Discussions

 
SuggestionSuggested Improvement Pin
Graeme_Grant19-Aug-15 21:41
mvaGraeme_Grant19-Aug-15 21:41 
QuestionUsing this code with Windows Form ClickOnce application Pin
d.barile12-Aug-15 5:04
professionald.barile12-Aug-15 5:04 
AnswerRe: Using this code with Windows Form ClickOnce application Pin
Graeme_Grant1-Oct-17 16:57
mvaGraeme_Grant1-Oct-17 16:57 
Questionappref-ms hardcoded content Pin
Abramson uri21-Apr-13 1:17
Abramson uri21-Apr-13 1:17 
AnswerRe: appref-ms hardcoded content Pin
Ivan Leonenko21-Apr-13 4:50
Ivan Leonenko21-Apr-13 4:50 
QuestionCode not thread safe Pin
Abramson uri17-Apr-13 6:25
Abramson uri17-Apr-13 6:25 
AnswerRe: Code not thread safe Pin
Ivan Leonenko17-Apr-13 6:47
Ivan Leonenko17-Apr-13 6:47 
QuestionGot my 5 for a straight-to-the-point and very accurate article about a difficult subject. Thanks! Pin
Nigel Shaw12-Feb-13 7:01
Nigel Shaw12-Feb-13 7:01 
GeneralRe: Got my 5 for a straight-to-the-point and very accurate article about a difficult subject. Thanks! Pin
Ivan Leonenko12-Feb-13 20:52
Ivan Leonenko12-Feb-13 20:52 
GeneralWell Done Pin
db7uk16-Dec-12 4:27
db7uk16-Dec-12 4:27 
GeneralMy vote of 5 Pin
ridoy12-Dec-12 18:02
professionalridoy12-Dec-12 18:02 
GeneralMy vote of 5 Pin
Ahmed Ibrahim Assaf10-Dec-12 4:35
professionalAhmed Ibrahim Assaf10-Dec-12 4:35 
GeneralMy vote of 5 Pin
DrABELL6-Dec-12 16:34
DrABELL6-Dec-12 16:34 
GeneralRe: My vote of 5 Pin
Ivan Leonenko6-Dec-12 21:30
Ivan Leonenko6-Dec-12 21:30 
Thank you
QuestionNice job. Pin
alinux0826-Nov-12 10:09
alinux0826-Nov-12 10:09 
GeneralMy vote of 5 Pin
LWFDN20-Nov-12 9:03
LWFDN20-Nov-12 9:03 
AnswerRe: My vote of 5 Pin
Ivan Leonenko20-Nov-12 23:04
Ivan Leonenko20-Nov-12 23:04 
GeneralRe: My vote of 5 Pin
LWFDN21-Nov-12 4:49
LWFDN21-Nov-12 4:49 
GeneralRe: My vote of 5 Pin
Ivan Leonenko21-Nov-12 5:01
Ivan Leonenko21-Nov-12 5:01 
GeneralRe: My vote of 5 Pin
LWFDN21-Nov-12 7:41
LWFDN21-Nov-12 7:41 
AnswerRe: My vote of 5 Pin
Ivan Leonenko21-Nov-12 1:01
Ivan Leonenko21-Nov-12 1:01 
GeneralMy vote of 5 Pin
Dharmeshb Bhalodia6-Nov-12 15:44
professionalDharmeshb Bhalodia6-Nov-12 15:44 
QuestionКонтакт Pin
Violet Tape6-Nov-12 7:16
Violet Tape6-Nov-12 7:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.