Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop

Silent ClickOnce Installer for Winform & WPF in C# & VB

5.00/5 (31 votes)
10 Jan 2023CPOL18 min read 74.3K   2.2K  
This article comprehensively covers the Microsoft ClickOnce Installer with a bare bones WinForm/WPF C#/VB Silent Updater framework plus covers how to implement, troubleshoot, test locally, to release.

Introduction

This article comprehensively covers the Microsoft ClickOnce Installer and improves on a previous article, published by Ivan Leonenko, with a bare-bones WinForm/WPF C#/VB Silent Updater framework. This article covers how to implement, troubleshoot and test locally, plus release to a live MVC web server.

If you download the solution and follow this article, you will:

  • configure a ClickOnce installation that will work with all major Web Browsers
  • create a ClickOnce Manifest Signing Certificate automagically
  • publish to a Web Application
  • set up a local Custom Domain for your Web Application
  • download ClickOnce Installer, run, and installed the application(s)
  • update the published files
  • watch the Silent Updater automatically download and update whilst the application is running

Contents

Overview

I have looked at a number of methods of installing applications and how to keep users up-to-date with the latest version of the application that I release. Having fragmentation with multiple versions of an application out in the wild presented a major headache for a small business like mine.

Microsoft, Apple, and Google store apps all have a mechanism to automate the update of applications installed on user devices. I needed a simple and automated system that ensures users were always up to date and pushing changes would be quick and transparent. ClickOnce looked like, and proved to be, the solution:

ClickOnce is a deployment technology that enables you to create self-updating Windows-based applications that can be installed and run with minimal user interaction. You can publish a ClickOnce application in three different ways: from a Web page, from a network file share, or from media such as a CD-ROM. ... Microsoft Docs[^]

I didn't like how the update worked with the check before running the application. It felt a bit amateurish. So a quick Google Search[^] found Ivan Leonenko's Silently updatable single instance WPF ClickOnce application[^] article.

Ivan's article is a good implementation of a Silent ClickOnce updater however was a bit rough, had slight problems, and appears to be no longer supported. The following article addresses this plus:

  • Pre-built application frameworks for WinForm and WPF applications in C# and VB ready for use
  • Cleaned up the code and changed to a Single Instance class
  • Both WinForm and WPF sample frameworks include graceful unhandled application exception shutdown
  • Added a sample MVC web server host
  • Added instructions on how to do localized IIS/IIS Express host troubleshooting and testing
  • Added MVC ClickOnce file support for IIS hosting on a live website
  • Added ClickOnce user installation troubleshooting help
  • Included both C# and VB versions for all samples

Prerequisites

The projects for this article were built with the following in mind:

  • C#6 minimum (Set in Properties > Build > Advanced > General > Language Version > C#6)
  • Built using VS2017 (VS2015 will also load, build, and run)
  • When you load the code the first time, you will need to restore Nuget Packages
  • Will need to follow the article to see the Silent Update in action

The Silent Updater Core

The actual code that does all the work is quite simple:

  • Single Instance class (new)
  • Checks for an update every 60 seconds
  • Starts a background/asynchronous update with feedback
  • Silent handling of download issues + retry every 60 seconds
  • Notify when an update is ready
C#
public sealed class SilentUpdater : INotifyPropertyChanged
{
    private static volatile SilentUpdater instance;
    public static SilentUpdater Instance
    { 
        get { return instance ?? (instance = new SilentUpdater()); }
    }

    private bool updateAvailable;
    public bool UpdateAvailable
    {
        get { return updateAvailable; }
        internal set
        {
            updateAvailable = value;
            RaisePropertyChanged(nameof(UpdateAvailable));
        }
    }

    private Timer Timer { get; }
    private ApplicationDeployment ApplicationDeployment { get; }
    private bool Processing { get; set; }

    public event EventHandler<UpdateProgressChangedEventArgs> ProgressChanged;
    public event EventHandler<EventArgs> Completed;
    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    private SilentUpdater()
    {
        if (!ApplicationDeployment.IsNetworkDeployed) return;
        ApplicationDeployment = ApplicationDeployment.CurrentDeployment;

        // progress
        ApplicationDeployment.UpdateProgressChanged += (s, e) =>
            ProgressChanged?.Invoke(this, new UpdateProgressChangedEventArgs(e));

        // completed
        ApplicationDeployment.UpdateCompleted += (s, e) =>
        {
            Processing = false;
            if (e.Cancelled || e.Error != null)
                return;

            UpdateAvailable = true;
            Completed?.Invoke(sender: this, e: null);
        };

        // checking
        Timer = new Timer(60000);
        Timer.Elapsed += (s, e) =>
        {
            if (Processing) return;
            Processing = true;
            try
            {
                if (ApplicationDeployment.CheckForUpdate(false))
                    ApplicationDeployment.UpdateAsync();
                else
                    Processing = false;
            }
            catch (Exception)
            {
                Processing = false;
            }
        };

        Timer.Start();
    }
}
VB
Public NotInheritable Class SilentUpdater : Implements INotifyPropertyChanged

    Private Shared mInstance As SilentUpdater
    Public Shared ReadOnly Property Instance() As SilentUpdater
        Get
            Return If(mInstance, (Factory(mInstance, New SilentUpdater())))
        End Get
    End Property

    Private mUpdateAvailable As Boolean
    Public Property UpdateAvailable() As Boolean
        Get
            Return mUpdateAvailable
        End Get
        Friend Set
            mUpdateAvailable = Value
            RaisePropertyChanged(NameOf(UpdateAvailable))
        End Set
    End Property

    Private ReadOnly Property Timer() As Timer
    Private ReadOnly Property ApplicationDeployment() As ApplicationDeployment

    Private Property Processing() As Boolean

    Public Event ProgressChanged As EventHandler(Of UpdateProgressChangedEventArgs)
    Public Event Completed As EventHandler(Of EventArgs)
    Public Event PropertyChanged As PropertyChangedEventHandler _
        Implements INotifyPropertyChanged.PropertyChanged

    Public Sub RaisePropertyChanged(propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    Private Sub New()

        If Not ApplicationDeployment.IsNetworkDeployed Then Return

        ApplicationDeployment = ApplicationDeployment.CurrentDeployment

        ' progress
        AddHandler ApplicationDeployment.UpdateProgressChanged,
        Sub(s, e)
            RaiseEvent ProgressChanged(Me, New UpdateProgressChangedEventArgs(e))
        End Sub

        ' completed
        AddHandler ApplicationDeployment.UpdateCompleted,
        Sub(s, e)
            Processing = False
            If e.Cancelled OrElse e.[Error] IsNot Nothing Then
                Return
            End If

            UpdateAvailable = True
            RaiseEvent Completed(Me, Nothing)

        End Sub

        ' checking
        Timer = New Timer(60000)

        AddHandler Timer.Elapsed,
        Sub(s, e)
            If Processing Then Return
            Processing = True

            Try
                If ApplicationDeployment.CheckForUpdate(False) Then
                    ApplicationDeployment.UpdateAsync()
                Else
                    Processing = False
                End If
            Catch generatedExceptionName As Exception
                Processing = False
            End Try

        End Sub

        Timer.Start()

    End Sub

    Private Shared Function Factory(Of T)(ByRef target As T, value As T) As T
        target = value
        Return value
    End Function

End Class

Implementation

There are two parts to implementing support for ClickOnce Silent Updating:

  1. Starting the service, unhandled application exceptions, and rebooting into the new version
  2. User feedback and interaction

Implementation for WinForm and WPF applications is slightly different. Each will be covered individually.

WinForm

First, we need to hook up the SilentUpdater class. The following code will:

  • obtain a reference to the SilentUpdater class instance
  • listen to the events of the SilentUpdater class
  • update the UI when an update is being downloaded
  • show the restart button when the download is completed
  • restart the application when the restart button is clicked

Lastly, C# and VB WinForm applications start a little differently. So in the VB version, to keep the bootstrap code separate to the form code, we need to manually call the start-up/bootstrap code as the main form initializes.

C#
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        UpdateService = SilentUpdater.Instance;
        UpdateService.ProgressChanged += SilentUpdaterOnProgressChanged;
        UpdateService.Completed += UpdateService_Completed;

        Version = AppProcessHelper.Version();
    }

    #region Update Service

    private SilentUpdater UpdateService { get; }

    public string UpdaterText { set { sbMessage.Text = value; } }

    private void RestartClicked(object sender, EventArgs e)
    {
	    // restart app
		AppProcessHelper.BeginReStart();
	}

    private bool updateNotified;

    private void SilentUpdaterOnProgressChanged
                 (object sender, UpdateProgressChangedEventArgs e)
        => UpdaterText = e.StatusString;

    private void UpdateService_Completed(object sender, EventArgs e)
    {
        if (updateNotified) return;
        updateNotified = true;

        NotifyUser();
    }

    private void NotifyUser()
    {
        // Notify on UI thread...
        if (InvokeRequired)
            Invoke((MethodInvoker)(NotifyUser));
        else
        {
            // silently notify the user...
            sbButRestart.Visible = true;
            UpdaterText = "A new version was installed!";
        }

        #endregion
    }
}
VB
Public Class Form1

    Sub New()

        ' Hookup Single instance and unhandled exception handling
        Bootstrap()

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.

        UpdateService = SilentUpdater.Instance
        AddHandler UpdateService.ProgressChanged, AddressOf SilentUpdaterOnProgressChanged
        AddHandler UpdateService.Completed, AddressOf UpdateService_Completed

        Version = AppProcessHelper.Version()

    End Sub

#Region "Update Service"
     Private ReadOnly Property UpdateService As SilentUpdater

    Public WriteOnly Property UpdaterText() As String
        Set
            sbMessage.Text = Value
        End Set
    End Property

    Public WriteOnly Property Version() As String
        Set
            sbVersion.Text = Value
        End Set
    End Property

    Private Sub RestartClicked(sender As Object, e As EventArgs) Handles sbButRestart.Click
        AppProcessHelper.BeginReStart()
    End Sub

    Private updateNotified As Boolean

    Private Sub SilentUpdaterOnProgressChanged(sender As Object, _
                                               e As UpdateProgressChangedEventArgs)
        UpdaterText = e.StatusString
    End Sub

    Private Sub UpdateService_Completed(sender As Object, e As EventArgs)
        If updateNotified Then
            Return
        End If
        updateNotified = True

        NotifyUser()
    End Sub

    Private Sub NotifyUser()

        ' Notify on UI thread...
        If InvokeRequired Then
            Invoke(DirectCast(AddressOf NotifyUser, MethodInvoker))
        Else
            ' silently notify the user...
            sbButRestart.Visible = True

            ' Uncomment if app needs to be more disruptive
            'MessageBox.Show(this, "A new version is now available.",
            '                "NEW VERSION",
            '                MessageBoxButtons.OK,
            '                MessageBoxIcon.Information);
            UpdaterText = "A new version was installed!"
        End If

    End Sub

#End Region

End Class

The above code will also support notifying the currently installed application version.

The Application class that is part of the WinForm framework simplifies the code required. However, as the Application class is sealed, we can't write extensions to extend it with our own method calls. So, we need an AppProcessHelper class to enable:

  • Single application instance management
  • The conditional restarting of the application
  • Installed version number retrieval

It is not a good idea to have multiple copies of the application running at the same time all trying to update themselves. The requirement of this article is to only have one instance of the application running. So I won't be covering in this article how to handle multiple running instances with single instance responsibility for silent updating.

C#
public static class AppProcessHelper
{
    private static Mutex instanceMutex;
    public static bool SetSingleInstance()
    {
        bool createdNew;
        instanceMutex = new Mutex(
            true, 
            @"Local\" + Process.GetCurrentProcess().MainModule.ModuleName,
            out createdNew);
        return createdNew;
    }

    public static bool ReleaseSingleInstance()
    {
        if (instanceMutex == null) return false;

        instanceMutex.Close();
        instanceMutex = null;

        return true;
    }

    private static bool isRestartDisabled;
    private static bool canRestart;

    public static void BeginReStart()
    {
        // Note that we can restart
        canRestart = true;

        // Start the shutdown process
        Application.Exit();
    }

    public static void PreventRestart(bool state = true)
    {
        isRestartDisabled = state;
        if (state) canRestart = false;
    }

    public static void RestartIfRequired(int exitCode = 0)
    {
        // make sure to release the instance
        ReleaseSingleInstance();

        if (canRestart)
            //app is restarting...
            Application.Restart();
        else
            // app is stopping...
            Environment.Exit(exitCode);
    }

    public static string Version()
    {
        return Assembly.GetEntryAssembly().GetName().Version.ToString();
    }
}
VB
Public Module AppProcessHelper

    Private instanceMutex As Mutex

    Public Function SetSingleInstance() As Boolean
        Dim createdNew As Boolean
        instanceMutex = New Mutex(True, _
                                  String.Format("Local\{0}", Process.GetCurrentProcess() _
                                                                    .MainModule.ModuleName), _
                                  createdNew)
        Return createdNew
    End Function

    Public Function ReleaseSingleInstance() As Boolean
        If instanceMutex Is Nothing Then
            Return False
        End If

        instanceMutex.Close()
        instanceMutex = Nothing

        Return True
    End Function

    Private isRestartDisabled As Boolean
    Private canRestart As Boolean

    Public Sub BeginReStart()
        ' Note that we can restart
        canRestart = True

        ' Start the shutdown process
        Application.[Exit]()
    End Sub

    Public Sub PreventRestart(Optional state As Boolean = True)
        isRestartDisabled = state
        If state Then
            canRestart = False
        End If
    End Sub

    Public Sub RestartIfRequired(Optional exitCode As Integer = 0)
        ' make sure to release the instance
        ReleaseSingleInstance()

        If canRestart Then
            'app is restarting...
            Application.Restart()
        Else
            ' app is stopping...
            Environment.[Exit](exitCode)
        End If
    End Sub

    Public Function Version() As String
        Return Assembly.GetEntryAssembly().GetName().Version.ToString()
    End Function

End Module

I have separated the Restart into two steps with the option to prevent restarting. I have done this for two reasons:

  1. To give the application the opportunity to let the user choose to save any unsaved work, abort if pressed by accident, and allow the application to clean up before finalizing the shutdown process.
  2. If any unhandled exceptions occur, to (optionally) prevent restarting and end in a possible endless exception cycle.
C#
internal static class Program
{
    [STAThread]
    private static void Main()
    {
        // check if this is already running...
        if (!AppProcessHelper.SetSingleInstance())
        {
            MessageBox.Show("Application is already running!",
                            "ALREADY ACTIVE",
                            MessageBoxButtons.OK,
                            MessageBoxIcon.Exclamation);
            Environment.Exit(-1);
        }

        Application.ApplicationExit += ApplicationExit;
        Application.ThreadException += Application_ThreadException;
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        Application.Run(new Form1());
    }

    private static void CurrentDomain_UnhandledException(object sender,
                                                         UnhandledExceptionEventArgs e)
        => ShowExceptionDetails(e.ExceptionObject as Exception);

    private static void Application_ThreadException(object sender,
                                                    ThreadExceptionEventArgs e)
        => ShowExceptionDetails(e.Exception);

    private static void ShowExceptionDetails(Exception Ex)
    {
        // Do logging of exception details

        // Let the user know that something serious happened...
        MessageBox.Show(Ex.Message,
                        Ex.TargetSite.ToString(),
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Error);

        // better not try and restart as we might end up in an endless exception loop....
        AppProcessHelper.PreventRestart();

        // ask the app to shutdown...
        Application.Exit();
    }

    private static void ApplicationExit(object sender, EventArgs e)
    {
        // last change for cleanup code here!

        // only restart if user requested, not an unhandled app exception...
        AppProcessHelper.RestartIfRequired();
    }
}
VB
Partial Class Form1

    Sub Bootstrap()

        ' check if this is already running...
        If Not AppProcessHelper.SetSingleInstance() Then
            MessageBox.Show("Application is already running!", _
                            "ALREADY ACTIVE", _
                            MessageBoxButtons.OK, _
                            MessageBoxIcon.Exclamation)
            Environment.[Exit](-1)
        End If

        AddHandler Application.ApplicationExit, AddressOf ApplicationExit
        AddHandler Application.ThreadException, AddressOf Application_ThreadException
        AddHandler AppDomain.CurrentDomain.UnhandledException, _
                   AddressOf CurrentDomain_UnhandledException

    End Sub

    Private Sub CurrentDomain_UnhandledException_
            (sender As Object, e As UnhandledExceptionEventArgs)
        ShowExceptionDetails(TryCast(e.ExceptionObject, Exception))
    End Sub

    Private Sub Application_ThreadException(sender As Object, e As ThreadExceptionEventArgs)
        ShowExceptionDetails(e.Exception)
    End Sub

    Private Sub ShowExceptionDetails(Ex As Exception)
        ' Do logging of exception details

        ' Let the user know that something serious happened...
        MessageBox.Show(Ex.Message, _
                        Ex.TargetSite.ToString(), _
                        MessageBoxButtons.OK, _
                        MessageBoxIcon.[Error])

        ' better not try and restart as we might end up in an endless exception loop....
        AppProcessHelper.PreventRestart()

        ' ask the app to shutdown...
        Application.[Exit]()
    End Sub

    Private Sub ApplicationExit(sender As Object, e As EventArgs)
        ' last change for cleanup code here!

        ' only restart if user requested, not an unhandled app exception...
        AppProcessHelper.RestartIfRequired()
    End Sub

End Class

WPF (Windows Presentation Foundation)

First, we need to hook up the SilentUpdater class. This is the code-behind example. An MVVM version is also included in the download. I have put the code in a separate UserControl called StatusBarView. This will keep the code separate from the rest of the code in the main window.

The following code will:

  • obtain a reference to the SilentUpdater class instance
  • listen to the events of the SilentUpdater class
  • update the UI when an update is being downloaded
  • show the restart button when the download is completed
  • restart the application when the restart button is clicked
C#
public partial class StatusBarView : UserControl, INotifyPropertyChanged
{
    public StatusBarView()
    {
        InitializeComponent();
        DataContext = this;

        // only use the service if the app is running...
        if (!this.IsInDesignMode())
        {
            UpdateService = SilentUpdater.Instance;
            UpdateService.ProgressChanged += SilentUpdaterOnProgressChanged;
        }
    }

    #region Update Service

    public SilentUpdater UpdateService { get; }

    private string updaterText;
    public string UpdaterText
    {
        get { return updaterText; }
        set { Set(ref updaterText, value); }
    }

    public string Version { get { return Application.Current.Version(); } }

    // Only works once installed...
    private void RestartClicked(object sender, RoutedEventArgs e)
        => Application.Current.BeginReStart();

    private bool updateNotified;

    private void SilentUpdaterOnProgressChanged(object sender,
                                                UpdateProgressChangedEventArgs e)
        => UpdaterText = e.StatusString;

    #endregion

    #region INotifyPropertyChanged

    public void Set<TValue>(ref TValue field,
                            TValue newValue,
                            [CallerMemberName] string propertyName = "")
    {
        if (EqualityComparer<TValue>.Default.Equals(field, default(TValue))
            || !field.Equals(newValue))
        {
            field = newValue;
            PropertyChanged?.Invoke(this,
                                    new PropertyChangedEventArgs(propertyName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion
}
VB
Public Class StatusBarView : Implements INotifyPropertyChanged

    Public Sub New()
        InitializeComponent()
        DataContext = Me

        ' only use the service if the app is running...
        If Not IsInDesignMode() Then
            UpdateService = SilentUpdater.Instance

            ' Uncomment if app needs to be more disruptive
            ' AddHandler UpdateService.Completed, AddressOf UpdateServiceCompleted
            AddHandler UpdateService.ProgressChanged,
                AddressOf SilentUpdaterOnProgressChanged
        End If
    End Sub

#Region "Update Service"
     Public ReadOnly Property UpdateService() As SilentUpdater

    Private mUpdaterText As String
    Public Property UpdaterText As String
        Get
            Return mUpdaterText
        End Get
        Set
            [Set](mUpdaterText, Value)
        End Set
    End Property

    Public ReadOnly Property Version As String
        Get
            Return Application.Current.Version()
        End Get
    End Property

    ' Only works once installed...
    Private Sub RestartClicked(sender As Object, e As RoutedEventArgs)
        Application.Current.BeginReStart()
    End Sub

    Private updateNotified As Boolean

    Private Sub SilentUpdaterOnProgressChanged(sender As Object,
                                               e As UpdateProgressChangedEventArgs)
        UpdaterText = e.StatusString
    End Sub

    Private Sub UpdateServiceCompleted(sender As Object, e As EventArgs)
        If updateNotified Then
            Return
        End If
        updateNotified = True

        NotifyUser()
    End Sub

    Private Sub NotifyUser()
        ' Notify on UI thread...
        Dispatcher.Invoke(Sub()
                              MessageBox.Show("A new version is now available.",
                                              "NEW VERSION",
                                              MessageBoxButton.OK,
                                              MessageBoxImage.Information)
                          End Sub)
    End Sub

#End Region

#Region "INotifyPropertyChanged"
     Public Sub [Set](Of TValue)(ByRef field As TValue, _
                                newValue As TValue, _
                                <CallerMemberName> Optional propertyName As String = "")
        If EqualityComparer(Of TValue).Default.Equals(field, Nothing) _
            OrElse Not field.Equals(newValue) Then
            field = newValue
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
        End If
    End Sub

    Public Event PropertyChanged As PropertyChangedEventHandler _
        Implements INotifyPropertyChanged.PropertyChanged

#End Region

End Class

One thing that should stand out is that the WinForm and WPF versions are almost identical.

And here is the XAML for the UI:

XML
<UserControl
    x:Class="WpfCBApp.Views.StatusBarView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:Wpf.Core.Converters;assembly=Wpf.Core"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    mc:Ignorable="d" d:DesignHeight="30" d:DesignWidth="400">

    <Grid Background="DarkGray">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>
        <Grid.Resources>
            <c:VisibilityConverter x:Key="VisibilityConverter"/>
            <c:NotVisibilityConverter x:Key="NotVisibilityConverter"/>
            <Style TargetType="TextBlock">
                <Setter Property="Foreground" Value="White"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>
            <Style TargetType="Button">
                <Setter Property="Foreground" Value="White"/>
                <Setter Property="Background" Value="Green"/>
                <Setter Property="BorderThickness" Value="0"/>
                <Setter Property="Margin" Value="4 1 1 1"/>
                <Setter Property="Padding" Value="10 0"/>
                <Setter Property="VerticalAlignment" Value="Stretch"/>
            </Style>
        </Grid.Resources>

        <TextBlock Margin="4 0">
            <Run FontWeight="SemiBold">Version: </Run>
            <Run Text="{Binding Version, Mode=OneTime}"/>
        </TextBlock>

        <TextBlock Text="{Binding UpdaterText}" Grid.Column="2" 
                   Margin="4 0" HorizontalAlignment="Right"
                   Visibility="{Binding UpdateService.UpdateAvailable,
                                Converter={StaticResource NotVisibilityConverter}}"/>
        <TextBlock Text="A new version was installed!" Grid.Column="2"
                   Margin="4 0" HorizontalAlignment="Right"
                   Visibility="{Binding UpdateService.UpdateAvailable,
                                Converter={StaticResource VisibilityConverter}}"/>
        <Button Content="Click to Restart" Grid.Column="3"
                Visibility="{Binding UpdateService.UpdateAvailable,
                             Converter={StaticResource VisibilityConverter}}"
                Click="RestartClicked"/>

    </Grid>

</UserControl>

The above code will also support notifying the currently installed application version.

The Application class that is part of the WPF framework does not have support for Restarting however the class is not sealed, so we can write extensions to extend it with our own method calls. So we need a slightly different version of the WinForm AppProcessHelper class to enable:

  • single application instance management
  • support for restarting the application (Ivan's article has a good implementation that we will use)
  • the conditional restarting of the application
  • installed version number retrieval

Again, it is not a good idea to have multiple copies of the application running at the same time all trying to update themselves. The requirement of this article is to only have one instance of the application running. So I won't be covering in this article how to handle multiple running instances with single instance responsibility for silent updating.

C#
internal static class AppProcessHelper
{
    private static Process process;
    public static Process GetProcess
    {
        get
        {
            return process ?? (process = new Process
            {
                StartInfo =
                {
                    FileName = GetShortcutPath(), UseShellExecute = true
                }
            });
        }
    }

    public static string GetShortcutPath()
        => $@"{Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.Programs),
                GetPublisher(),
                GetDeploymentInfo().Name.Replace(".application", ""))}.appref-ms";

    private static ActivationContext ActivationContext
        => AppDomain.CurrentDomain.ActivationContext;

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

        if (xDocument.Root == null)
            return null;

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

    public static ApplicationId GetDeploymentInfo()
        => (new ApplicationSecurityInfo(ActivationContext)).DeploymentId;

    private static Mutex instanceMutex;
    public static bool SetSingleInstance()
    {
        bool createdNew;
        instanceMutex = new Mutex(true,
                                  @"Local\" + Assembly.GetExecutingAssembly().GetType().GUID, 
                                  out createdNew);
        return createdNew;
    }

    public static bool ReleaseSingleInstance()
    {
        if (instanceMutex == null) return false;

        instanceMutex.Close();
        instanceMutex = null;

        return true;
    }

    private static bool isRestartDisabled;
    private static bool canRestart;

    public static void BeginReStart()
    {
        // make sure we have the process before we start shutting down
        var proc = GetProcess;

        // Note that we can restart only if not
        canRestart = !isRestartDisabled;

        // Start the shutdown process
        Application.Current.Shutdown();
    }

    public static void PreventRestart(bool state = true)
    {
        isRestartDisabled = state;
        if (state) canRestart = false;
    }

    public static void RestartIfRequired(int exitCode = 0)
    {
        // make sure to release the instance
        ReleaseSingleInstance();

        if (canRestart && process != null)
            //app is restarting...
            process.Start();
        else
            // app is stopping...
            Application.Current.Shutdown(exitCode);
    }
}
VB
Public Module AppProcessHelper

    Private process As Process
    Public ReadOnly Property GetProcess() As Process
        Get
            If process Is Nothing Then
                process = New Process() With
                {
                    .StartInfo = New ProcessStartInfo With
                    {
                        .FileName = GetShortcutPath(),
                        .UseShellExecute = True
                    }
                }
            End If

            Return process
        End Get
    End Property

    Public Function GetShortcutPath() As String
        Return String.Format("{0}.appref-ms", _
                                Path.Combine( _
                                    Environment.GetFolderPath( _
                                    Environment.SpecialFolder.Programs), _
                                GetPublisher(), _
                                GetDeploymentInfo().Name.Replace(".application", "")))
    End Function

    Private ReadOnly Property ActivationContext() As ActivationContext
        Get
            Return AppDomain.CurrentDomain.ActivationContext
        End Get
    End Property

    Public Function GetPublisher() As String

        Dim xDocument As XDocument
        Using memoryStream = New MemoryStream(ActivationContext.DeploymentManifestBytes)
            Using xmlTextReader = New XmlTextReader(memoryStream)
                xDocument = XDocument.Load(xmlTextReader)
            End Using
        End Using

        If xDocument.Root Is Nothing Then
            Return Nothing
        End If

        Return xDocument.Root _
                        .Elements().First(Function(e) e.Name.LocalName = "description") _
                        .Attributes().First(Function(a) a.Name.LocalName = "publisher") _
                        .Value

    End Function

    Public Function GetDeploymentInfo() As ApplicationId
        Return (New ApplicationSecurityInfo(ActivationContext)).DeploymentId
    End Function

    Private instanceMutex As Mutex
    Public Function SetSingleInstance() As Boolean
        Dim createdNew As Boolean
        instanceMutex = New Mutex(True, _
                                  String.Format("Local\{0}", _
                                                Assembly.GetExecutingAssembly() _
                                                        .GetType().GUID), _
                                  createdNew)
        Return createdNew
    End Function

    Public Function ReleaseSingleInstance() As Boolean
        If instanceMutex Is Nothing Then
            Return False
        End If

        instanceMutex.Close()
        instanceMutex = Nothing

        Return True
    End Function

    Private isRestartDisabled As Boolean
    Private canRestart As Boolean

    Public Sub BeginReStart()
        ' make sure we have the process before we start shutting down
        Dim proc = GetProcess

        ' Note that we can restart only if not
        canRestart = Not isRestartDisabled

        ' Start the shutdown process
        Application.Current.Shutdown()
    End Sub

    Public Sub PreventRestart(Optional state As Boolean = True)
        isRestartDisabled = state
        If state Then
            canRestart = False
        End If
    End Sub

    Public Sub RestartIfRequired(Optional exitCode As Integer = 0)

        ' make sure to release the instance
        ReleaseSingleInstance()

        If canRestart AndAlso process IsNot Nothing Then
            'app is restarting...
            process.Start()
        Else
            ' app is stopping...
            Application.Current.Shutdown(exitCode)
        End If
    End Sub

End Module

And here are the extensions for the WPF framework Application class:

C#
public static class ApplicationExtension
{
    public static bool SetSingleInstance(this Application app)
        => AppProcessHelper.SetSingleInstance();

    public static bool ReleaseSingleInstance(this Application app)
        => AppProcessHelper.ReleaseSingleInstance();

    public static void BeginReStart(this Application app)
        => AppProcessHelper.BeginReStart();

    public static void PreventRestart(this Application app, bool state = true)
        => AppProcessHelper.PreventRestart(state);

    public static void RestartIfRequired(this Application app)
        => AppProcessHelper.RestartIfRequired();

    public static string Version(this Application app)
        => Assembly.GetEntryAssembly().GetName().Version.ToString();
}
VB
Public Module ApplicationExtension

    <Extension>
    Public Function SetSingleInstance(app As Application) As Boolean
        Return AppProcessHelper.SetSingleInstance()
    End Function

    <Extension>
    Public Function ReleaseSingleInstance(app As Application) As Boolean
        Return AppProcessHelper.ReleaseSingleInstance()
    End Function

    <Extension>
    Public Sub BeginReStart(app As Application)
        AppProcessHelper.BeginReStart()
    End Sub

    <Extension>
    Public Sub PreventRestart(app As Application, Optional state As Boolean = True)
        AppProcessHelper.PreventRestart(state)
    End Sub

    <Extension>
    Public Sub RestartIfRequired(app As Application)
        AppProcessHelper.RestartIfRequired()
    End Sub

    <Extension>
    Public Function Version(app As Application) As String
        Return Assembly.GetEntryAssembly().GetName().Version.ToString()
    End Function

End Module

Again, for the same reasons, I have separated the restart into two steps with the option to prevent restarting:

  1. To give the application the opportunity to let the user choose to save any unsaved work, abort if pressed by accident, and allow the application to clean up before finalizing the shutdown process.
  2. If any unhandled exceptions occur, to (optionally) prevent restarting and end in a possible endless exception cycle.
C#
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        // check if this is already running...
        if (!Current.SetSingleInstance())
        {
            MessageBox.Show("Application is already running!",
                            "ALREADY ACTIVE",
                            MessageBoxButton.OK,
                            MessageBoxImage.Exclamation);
            Current.Shutdown(-1);
        }

        // setup global exception handling  
        Current.DispatcherUnhandledException +=
            new DispatcherUnhandledExceptionEventHandler(AppDispatcherUnhandledException);

        Dispatcher.UnhandledException +=
            new DispatcherUnhandledExceptionEventHandler(DispatcherOnUnhandledException);

        AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException;

        // start the app
        base.OnStartup(e);
    }

    private void AppDispatcherUnhandledException(object sender,
                                                 DispatcherUnhandledExceptionEventArgs e)
        => ForwardUnhandledException(e);

    private void DispatcherOnUnhandledException(object sender, 
                                                DispatcherUnhandledExceptionEventArgs e)
        => ForwardUnhandledException(e);

    private void ForwardUnhandledException(DispatcherUnhandledExceptionEventArgs e)
    {
        // forward the exception to AppDomain.CurrentDomain.UnhandledException ...
        Current.Dispatcher.Invoke(DispatcherPriority.Normal,
            new Action<Exception>((exc) =>
            {
                throw new Exception("Exception from another Thread", exc); 
            }),
            e.Exception);
    }

    private void CurrentDomainOnUnhandledException
                 (object sender, UnhandledExceptionEventArgs e)
    {
        // Do logging of exception details

        // Let the user know that something serious happened...
        var ex = e.ExceptionObject as Exception;
        MessageBox.Show(ex.Message,
                        ex.TargetSite.ToString(),
                        MessageBoxButton.OK,
                        MessageBoxImage.Error);

        // better not try and restart as we might end up in an endless exception loop....
        Current.PreventRestart();

        // ask the app to shutdown...
        Current.Shutdown();
    }

    protected override void OnExit(ExitEventArgs e)
    {
        // last change for cleanup code here!

        // clear to exit app
        base.OnExit(e);

        // only restart if user requested, not an unhandled app exception...
        Current.RestartIfRequired();
    }
}
VB
Class Application

    Protected Overrides Sub OnStartup(e As StartupEventArgs)

        ' check if this is already running...
        If Not Current.SetSingleInstance() Then
            MessageBox.Show("Application is already running!",
                            "ALREADY ACTIVE",
                            MessageBoxButton.OK,
                            MessageBoxImage.Exclamation)
            Current.Shutdown(-1)
        End If

        ' setup global exception handling  
        AddHandler Current.DispatcherUnhandledException,
            New DispatcherUnhandledExceptionEventHandler_
                (AddressOf AppDispatcherUnhandledException)

        AddHandler Dispatcher.UnhandledException,
            New DispatcherUnhandledExceptionEventHandler_
                (AddressOf DispatcherOnUnhandledException)

        AddHandler AppDomain.CurrentDomain.UnhandledException, _
                   AddressOf CurrentDomainOnUnhandledException

        ' start the app
        MyBase.OnStartup(e)

    End Sub

    Private Sub AppDispatcherUnhandledException(sender As Object, _
                                                e As DispatcherUnhandledExceptionEventArgs)
        ForwardUnhandledException(e)
    End Sub

    Private Sub DispatcherOnUnhandledException(sender As Object, _
                                               e As DispatcherUnhandledExceptionEventArgs)
        ForwardUnhandledException(e)
    End Sub

    Private Sub ForwardUnhandledException(e As DispatcherUnhandledExceptionEventArgs)
        ' forward the exception to AppDomain.CurrentDomain.UnhandledException ...
        Current.Dispatcher.Invoke(DispatcherPriority.Normal,
            New Action(Of Exception)(Sub(exc)
                                         Throw New Exception_
                                         ("Exception from another Thread", exc)
                                     End Sub), e.Exception)
    End Sub

    Private Sub CurrentDomainOnUnhandledException(sender As Object, _
                                                  e As UnhandledExceptionEventArgs)
        ' Do logging of exception details

        ' Let the user know that something serious happened...
        Dim ex = TryCast(e.ExceptionObject, Exception)
        MessageBox.Show(ex.Message,
                        ex.TargetSite.ToString(),
                        MessageBoxButton.OK,
                        MessageBoxImage.[Error])

        ' better not try and restart as we might end up in an endless exception loop....
        Current.PreventRestart()

        ' ask the app to shutdown...
        Current.Shutdown()
    End Sub

    Protected Overrides Sub OnExit(e As ExitEventArgs)
        ' last change for cleanup code here!

        ' clear to exit app
        MyBase.OnExit(e)

        ' only restart if user requested, not an unhandled app exception...
        Current.RestartIfRequired()
    End Sub

End Class

You can run the application and test the Single Instance support. However, to test the ClickOnce installation, you need to first publish the application, host the installer, install, run, then publish and host an updated version. This will be covered in the following sections.

Preparing the Desktop Application for ClickOnce Testing

Testing any ClickOnce update support requires installing and running either on a live server (IIS) or localhost (IIS / IIS Express). This next section will cover:

  • Creating a ClickOnce web-based installer
  • Hosting the ClickOnce installer on a local and live MVC server
  • How to run a test web install on a local machine and the setup required
  • How to avoid "Deployment and application do not have matching security zones" for Chrome and Firefox web browsers
  • How to test the Silent Updater

Configuring the Installer

You should always sign the ClickOnce manifests to reduce the chance of any hacking. You can either buy and use your own (really needed for released applications) or you can let VS generate one for you (good for testing only). It is a good practice to maintain even when only testing applications. To do that, go to Properties > Signing > check "Sign the ClickOnce manifest".

Image 1

Note: Above, we have only checked the "Sign the ClickOnce Manifests" box. When testing, the Certificate will be automatically generated for you. Here is an example of a Certificate after the first time you publish:

Image 2

Next, we need to set our Publish profile and settings. First is to set up the Publish Properties defaults:

Image 3

The "Publish Folder Location" points to the physical location where the published files will go. The "Installation Folder" is the location on the web server where the ClickOnce installer will look to download the files from. I have highlighted the "Installation Folder" to show where there can be a problem that we will see later when we run the "Publish Wizard".

The "Install Mode and Settings" is set to "available offline" so that the application can run when not connected to the internet.

Next, we need to set up the installer prerequisites. Here, we will set the .NET Framework version. The installer will check that the correct framework version is installed on the user's computer. If not, it will run the process automatically for you.

Image 4

Next, we need to set the update settings. Here, we don't want the ClickOnce installer to run and check for updates before the application runs. This will feel amateurish and slow the loading of our application on start-up. Instead, we want the Silent Updater to do the work after the application starts. So we uncheck "The application should check for updates".

Image 5

Note: I have highlighted the "Update location (if different than publish location)" section. The documentation does not mention how this optional setting will affect the installer in some circumstances. I have a section below that will discuss the ramifications of not completing this field in more detail below.

Last, we need to set the "Options". First, Deployment settings. We want to automatically publish an install page script and set the deployment file extension:

Image 6

Next, for security, we don't want the manifest to be activated via URL, we do however want to use the manifest information for user trust. Lastly, I prefer to create a desktop shortcut for easy access, easier than having them find our application in the Start Menu. ;)

Image 7

Setting the Desktop Application Assembly Version

Publish version is different from the Assembly and File versions. The Publish version is used by the ClickOnce installer on the user's local machine to identify versions and updates. The Assembly version will be displayed to the user. The Assembly version is set on the Properties > Application tab:

Image 8

Publishing the Desktop Application to the Web Application

Once the publishing defaults are set, we can use the Publish Wizard to:

  1. Check the default settings
  2. Automatically generate the Testing Signing Certificate
  3. Build the application
  4. Create the installation and copy all relevant files to the web application
  5. Auto increment the Publish Version used by ClickOnce to identify updates

Step 1 - Publish Location

You can publish directly to your live web server, however, I prefer to stage and test before I "go live". So I point the publishing process to the path in my web application project.

Image 9

Step 2 - ClickOnce Installer Download Location

This will be the path/url that the ClickOnce installer will look for files for installation and later for updates.

Image 10

Note: I have highlighted the http://localhost/... path. This will be changed by the wizard and we can see what happens in the final step of the Wizard.

Step 3 - ClickOnce Operation Mode

We want the application to be installed locally and be able to run offline when not connected to the internet.

Image 11

Step 4 - Finish - Review Settings

Image 12

In Step 2 of the Publish Wizard, we specified that the install path for testing will be http://localhost however the Publish Wizard changed it to http://[local_network_name]. Why the Publish Wizard does this is unclear.

Step 5 - Publishing to Web Server

Once you click the Finish button in the Publish Wizard, the publishing process will create the Testing ClickOnce Manifests Signing Certificate, build the application (if required), then create the installation files, and copy them to the web application ready for inclusion.

After you complete full testing by running the web application and installing the application, further publishing is a simple click of the "Publish Now" button. All the settings used in the Publish Wizard will be used by "Publish Now".

Including Published Files into the Web Application

To include the published installation files, in Solution Explorer:

  1. Go to the web application, make sure that hidden files are visible, and click the Refresh button.
  2. Expand the folders so that you can see the new installation files:

    Image 13

  3. Select the files and folders for inclusion, right-click, and select "Include In Project":

    Image 14

LocalHost Installation Fails (IIS / IIS Express)

One problem I encountered when trying to do local web server ClickOnce update testing is that the Visual Studio ClickOnce Publisher does a strange thing. http://localhost gets changed to http://[network computer name]. So if you download and run the ClickOnce Setup.exe application via http://localhost, you will see something like this:

Image 15

Here is the install.log file:

The following properties have been set:
Property: [AdminUser] = true {boolean}
Property: [InstallMode] = HomeSite {string}
Property: [NTProductType] = 1 {int}
Property: [ProcessorArchitecture] = AMD64 {string}
Property: [VersionNT] = 10.0.0 {version}
Running checks for package 'Microsoft .NET Framework 4.5.2 (x86 and x64)', phase BuildList
Reading value 'Release' of registry key 
              'HKLM\Software\Microsoft\NET Framework Setup\NDP\v4\Full'
Read integer value 460798
Setting value '460798 {int}' for property 'DotNet45Full_Release'
Reading value 'v4' of registry key 
              'HKLM\SOFTWARE\Microsoft\NET Framework Setup\OS Integration'
Read integer value 1
Setting value '1 {int}' for property 'DotNet45Full_OSIntegrated'
The following properties have been set for package 
              'Microsoft .NET Framework 4.5.2 (x86 and x64)':
Property: [DotNet45Full_OSIntegrated] = 1 {int}
Property: [DotNet45Full_Release] = 460798 {int}
Running checks for command 'DotNetFX452\NDP452-KB2901907-x86-x64-AllOS-ENU.exe'
Result of running operator 'ValueEqualTo' on property 'InstallMode' and value 'HomeSite': true
Result of checks for command 'DotNetFX452\NDP452-KB2901907-x86-x64-AllOS-ENU.exe' is 'Bypass'
Running checks for command 'DotNetFX452\NDP452-KB2901907-x86-x64-AllOS-ENU.exe'
Result of running operator 'ValueEqualTo' on property 'InstallMode' and value 'HomeSite': true
Result of checks for command 'DotNetFX452\NDP452-KB2901907-x86-x64-AllOS-ENU.exe' is 'Bypass'
Running checks for command 'DotNetFX452\NDP452-KB2901954-Web.exe'
Result of running operator 'ValueNotEqualTo' on 
                  property 'InstallMode' and value 'HomeSite': false
Result of running operator 'ValueGreaterThanEqualTo' on 
                  property 'DotNet45Full_Release' and value '379893': true
Result of checks for command 'DotNetFX452\NDP452-KB2901954-Web.exe' is 'Bypass'
Running checks for command 'DotNetFX452\NDP452-KB2901954-Web.exe'
Result of running operator 'ValueNotEqualTo' on property 'InstallMode' and 
                  value 'HomeSite': false
Result of running operator 'ValueGreaterThanEqualTo' on 
                  property 'DotNet45Full_Release' and value '379893': true
Result of checks for command 'DotNetFX452\NDP452-KB2901954-Web.exe' is 'Bypass'
'Microsoft .NET Framework 4.5.2 (x86 and x64)' RunCheck result: No Install Needed
Launching Application.
URLDownloadToCacheFile failed with HRESULT '-2146697208'
Error: An error occurred trying to download 
       'http://macbookpro:60492/Installer/WpfCBApp/WpfCBAppVB.application'.

If we put http://macbookpro:60492/Installer/WpfCBApp/WpfCBAppVB.application in a web browser, we can see why it failed:

Image 16

The solution is to configure your dev computer as a web server.

How to Configure Dev Computer for Offline Host Testing

To configure your dev machine for web-hosting ClickOnce update testing.

  1. Modifying the applicationhost.config file for Custom Domains
  2. Updating Hosts file
  3. Running VS in Administrator mode (to access hosts file)

Configuring IIS Express with Custom Domains

For VS2015 & VS2017, the applicationhost.config file is located in the "solution" folder in the .vs\config folder. Within that folder, you will find the applicationhost.config file.

In the Website's Properties > Web tab, use the following configuration:

Image 17

With the following in the hosts file (located in C:\Windows\System32\drivers\etc):

127.0.0.1    silentupdater.net
127.0.0.1    www.silentupdater.net

And the following in the applicationhost.config file:

XML
<!-- C# server -->
<site name="SampleMvcServer" id="2">
    <application path="/" applicationPool="Clr4IntegratedAppPool">
        <virtualDirectory path="/" physicalPath="[path_to_server_project_folder]" />
    </application>
    <bindings>
        <binding protocol="http" bindingInformation="*:63690:" />
        <binding protocol="http" bindingInformation="*:63690:localhost" /> 
    </bindings>
</site>
<!-- VB server -->
<site name="SampleMvcServerVB" id="4">
    <application path="/" applicationPool="Clr4IntegratedAppPool">
        <virtualDirectory path="/" physicalPath="[path_to_server_project_folder]" />
    </application>
    <bindings>
        <binding protocol="http" bindingInformation="*:60492:" />
        <binding protocol="http" bindingInformation="*:60492:localhost" />
    </bindings>
</site>

For VS2010 & VS2013, the process is a little different.

  1. Right-click your Web Application Project > Properties > Web, then configure the "Servers" section as follows:
    • Select "IIS Express" from the drop-down
    • Project URL: http://localhost
    • Override application root URL: http://www.silentupdater.net
    • Click the "Create Virtual Directory" button (if you get an error here, you may need to disable IIS 5/6/7/8, change IIS's "Default Site" to anything but port :80, make sure that applications like Skype, etc. are not using port 80.
  2. Optionally: Set the "Start URL" to http://www.silentupdater.net
  3. Open %USERPROFILE%\My Documents\IISExpress\config\applicationhost.config (Windows XP, Vista, and 7) and edit the site definition in the <sites> config block to be along the lines of the following:
    XML
    <site name="SilentUpdater" id="997005936">
        <application path="/" applicationPool="Clr2IntegratedAppPool">
            <virtualDirectory
                path="/"
                physicalPath="C:\path\to\application\root" />
        </application>
        <bindings>
            <binding
                protocol="http"
                bindingInformation=":80:www.silentupdater.net" />
        </bindings>
        <applicationDefaults applicationPool="Clr2IntegratedAppPool" />
    </site>
  4. If running MVC: make sure the "applicationPool" is set to one of the "Integrated" options (like "Clr2IntegratedAppPool").
  5. Open your hosts file and add the line 127.0.0.1 www.silentupdater.net.
  6. Start your application!

NOTE: Remember to run your instance of Visual Studio 2015 as an administrator! Otherwise, the UAC will block VS & IIS Express from seeing the changes made to the hosts file.

Running Visual Studio in Administrator Mode

There are several methods of running in administrator mode. Everyone has their favourite way. One method is to:

  1. Go to the Visual Studio IDE folder where the devenv.exe file is located. For VS2017, it is located by default in C:\Program Files (x86)\Microsoft Visual Studio\2017\[version]\Common7\IDE
  2. Hold the Shift key and RightClick on the devenv.exe file
  3. Click on Run as administrator
  4. Open the solution, set the web server as "Set as Startup Project"
  5. Run the web server

Visual Studio and Local Custom Domain Hosting

Before we can do any testing, we need to update the publish profile to reflect the new custom domain www.silentupdater.net.

Configuring the Publish Download

We need to set the location that the ClickOnce Installer will look for updates. The path needs to be changed from http://localhost to our custom domain www.silentupdater.net.

Image 18

Now we can revisit the Publish Wizard steps above and the finish screen should now be:

Image 19

Once the Publish Wizard process is completed, the install files and folders included in the Web server's project, we can now run and do the ClickOnce install.

Installing and Testing Silent Updating

Steps to install, re-publish, re-host, run, update, and restart.

  1. Publish the application to your MVC Server
    • Make sure that you included published files before updating and restarting the server
  2. Install the application
  3. Run the application (don't stop it)
  4. Update the version number and make a noticeable change (e.g.: application background color)
  5. Compile, publish, and start the server
  6. Wait up to 60 seconds whilst watching the application's StatusBar.
  7. Once the silent update is complete, click the Restart button, and look for the changes and the updated version number.

ClickOnce Installation with Microsoft Edge or Internet Explorer

Below are the steps from the install page, to running.

Download Setup.exe Installer

Image 20

The Installation

Image 21

Image 22

Image 23

Image 24

Now the application is ready to run. The first time we run, as we are using a test certificate, we will see the following screen:

Image 25

ClickOnce Installation with Google Chrome or Mozilla Firefox

Downloading and installing using Chrome and Firefox should work the same as Edge and Internet Explorer. However, after downloading the installer file from the Install page using Chrome or Firefox, and running the installer, you might encounter this ClickOnce Install Failure:

Image 26

And the details may look like this:

PLATFORM VERSION INFO
    Windows             : 10.0.15063.0 (Win32NT)
    Common Language Runtime     : 4.0.30319.42000
    System.Deployment.dll         : 4.7.2046.0 built by: NET47REL1
    clr.dll             : 4.7.2110.0 built by: NET47REL1LAST
    dfdll.dll             : 4.7.2046.0 built by: NET47REL1
    dfshim.dll             : 10.0.15063.0 (WinBuild.160101.0800)

SOURCES
    Deployment url            : file:///C:/Users/[username]/Downloads/WinFormAppVB.application

IDENTITIES
    Deployment Identity        : WinFormAppVB.application, Version=1.0.0.0, 
    Culture=neutral, PublicKeyToken=e6b9c5f6a79417a1, processorArchitecture=msil

APPLICATION SUMMARY
    * Installable application.

ERROR SUMMARY
    Below is a summary of the errors, details of these errors are listed later in the log.
    * Activation of C:\Users\[username]\Downloads\WinFormAppVB.application resulted in exception. 
      Following failure messages were detected:
        + Deployment and application do not have matching security zones.

Install Failure: Deployment and application do not have matching security zones

Documentation on this failure is very limited. Microsoft's Troubleshooting ClickOnce Deployments[^] pages do not have any suitable solutions.

It turns out that both Chrome and Firefox check for the optional "Properties > Publish > Updates > Update location (if different than publish location)" section and compare it with the "Publish > Installation Folder URL". If the two locations don't match, the installation fails with "Deployment and application do not have matching security zones".

This setting is found in the <deploymentProvider codebase=... /> subsection to the <deployment> the section in the .application file.

Here is the correction to our "Properties > Publish > Updates > Update location (if different than publish location)" section:

Image 27

Running and Testing Silent Updating

When testing the silent updating, make sure to update the Assembly Version before pressing the "Publish Now" button. This makes it easier to see which version you are testing.

It is also a good practice, when doing testing, to include the install files into your web application before running. This way, when it is time to publish the release application version, you won't forget this step before pushing it to your website.

Normal State

When the application is running and there are no updates, the StatusBar will only report the current Assembly version.

WinForm Application

Image 28

WPF Application

Image 29

Updating State

When the application update starts, the StatusBar will report the downloading status.

WinForm Application

Image 30

WPF Application

Image 31

Updated State

When the application update has been completed, the StatusBar will show the completed message and a restart button.

WinForm Application

Image 32

WPF Application

Image 33

New Version After Restarting

Lastly, after the restart button is clicked, or the application is closed and restarted, the StatusBar will reflect the updated Assembly version.

WinForm Application

Image 34

WPF Application

Image 35

Hosting on a Web Service (IIS)

When hosting on a live website, we need to enable support for the install files on our MVC server. I have used the following for an Azure Web application:

RouteConfig.CS/VB

Makes sure that we accept requests for the install files and route the request to the FileController.

C#
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "ClickOnceWpfcbInstaller",
        "installer/wpfcbapp/{*fileName}",
        new { controller = "File", action = "GetFile", fileName = UrlParameter.Optional },
        new[] { "SampleMvcServer.Web.Controllers" }
    );

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}
VB
Public Sub RegisterRoutes(ByVal routes As RouteCollection)

    routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

    routes.MapRoute(
        "ClickOnceWpfInstaller",
        "installer/wpfapp/{*fileName}",
        New With {.controller = "File", .action = "GetFile", 
                  .fileName = UrlParameter.[Optional]},
        New String() {"SampleMvcServer.Web.Controllers"})

    routes.MapRoute(
        name:="Default",
        url:="{controller}/{action}/{id}",
        defaults:=New With 
        {.controller = "Home", .action = "Index", .id = UrlParameter.Optional}
    )

End Sub

FileController.CS/VB

The FileController ensures that the returned files requested are returned with the correct mime-type headers.

C#
public class FileController : Controller
{
    // GET: File
    public FilePathResult GetFile(string fileName)
    {
        var dir = Server.MapPath("/installer/wpfcbapp");
        var path = Path.Combine(dir, fileName);
        return File(path, GetMimeType(Path.GetExtension(fileName)));
    }

    private string GetMimeType(string extension)
    {
        if (extension == ".application" || extension == ".manifest")
            return "application/x-ms-application";
        else if (extension == ".deploy")
            return "application/octet-stream";
        else
            return "application/x-msdownload";
    }
}
VB
Public Class FileController : Inherits Controller

    ' GET: File
    Public Function GetFile(fileName As String) As FilePathResult

        Dim dir = Server.MapPath("/installer/wpfcbapp")
        Dim path = IO.Path.Combine(dir, fileName)
        Return File(path, GetMimeType(IO.Path.GetExtension(fileName)))

    End Function

    Private Function GetMimeType(extension As String) As String

        If extension = ".application" OrElse extension = ".manifest" Then
            Return "application/x-ms-application"
        ElseIf extension = ".deploy" Then
            Return "application/octet-stream"
        Else
            Return "application/x-msdownload"
        End If

    End Function

End Class

Summary

Hopefully, this article leaves you with more hair (than me) and less frustration by guiding you through the holes in the Microsoft documentation; filling in the blanks left by Ivan's original article; and avoiding common mistakes that I and others have encountered over time.

Credits and Other Related Links

This article contains a lot of fragmented information that was researched and collected over a period of time. Below are the links to the various people and resources that made this possible:

History

  • 1st October, 2017 - v1.0 - Initial release
  • 10th January, 2023 - v1.1 - Added download for .Net Framework 4.8

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)