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 = 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.