Tuesday, November 27, 2018

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

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 live MVC web server.
If you download the solution and follow this article, you will:
  • Configured a ClickOnce installation that will work with all major Web Browsers
  • Created a ClickOnce Manifest Signing Certificate automagically
  • Published to a Web Application
  • Set up a local Custom Domain for you Web Application
  • Downloaded ClickOnce Installer, run, and installed the application(s)
  • Updated the published files
  • Watched the Silent Updater automatically download and update whilst 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 myself.
Microsoft, Apple, 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 ensure 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 address 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 shut down
  • 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
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();
    }
}

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
  • Shows the restart button when the download is completed
  • Restarts the application when the clicks the restart button
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.
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
    }
}
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.
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();
    }
}
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.
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();
    }
}

WPF (Windows Presentation Foundation)

First we need to hook up the SilentUpdater class. This is the code-behind example. A 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 to 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
  • Shows the restart button when the download is completed
  • Restarts the application when the clicks the restart button
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
}
One thing that should standout is that the WinForm and WPF versions are almost identical.
And here is the XAML for the UI:
<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.
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);
    }
}
And here are the extensions for the WPF framework Application class:
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();
}
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.
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();
    }
}
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"
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 that you publish:
Next, we need to set our Publish profile and settings. First is to set up the Publish Properties defaults:
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 will run the process automatically for you.
Next we need to set the update settings. Here we don't want the ClickOnce installer 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".
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 a install page script and set the deployment file extension:
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. ;)

Setting the Desktop Application Assembly Version

Publish version is different to the Assembly and File versions. The Publish version is use 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:

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 publish process to the path in my web application project.

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

Step 4 - Finish - Review Settings

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 th refresh button
  2. Expand the folders so that you can see the new installation files

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

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:
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:
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:
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:
<!-- 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:
    <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.
Now we can revisit the Publish Wizard steps above and the finish screen should now be:
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 resarting the server
  2. Install the application
  3. Run the application (don't stop it)
  4. Update the version number and make a noticeable change (eg: application background color)
  5. Compile, publish, start server
  6. Wait up to 60 seconds whilst watching the application's StausBar.
  7. Once silent update is complete, click the restart button, 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

The Installation

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:

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:
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 does 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 compares 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>section in the .application file. 
Here is the correction to our "Properties > Publish > Updates > Update location (if different than publish location)" section:

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 to your web site.

Normal State

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

Updating State

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

Updated State

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

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
WPF Application

Hosting on a Web Service (IIS)

When hosting on a live web site, 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.
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 }
    );
}

FileController.CS/VB

The FileController ensures that the returned files requested are returned with the correct mimetype headers.
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";
    }
}

Summary

Hopefully, this articles leaves you with more hair (than I) and less frustrations by guiding your through the holes in the Microsoft documentation; filling in the blanks left by Ivan's original article; and avoid common mistakes that myself 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:
source : https://www.codeproject.com/Articles/1208414/%2FArticles%2F1208414%2FSilent-ClickOnce-Installer-for-WPF-Winforms-in-Csh