public static double[] GetWorkHours(DateTime start, DateTime end, TimeSpan workStartTime, TimeSpan workEndTime)
{
IList<double> result = new List<double>();
if (start > end)
{
throw new ArgumentException("Start date is later than end date");
}
DateTime currentDate = start.Date;
while (currentDate <= end.Date)
{
if (currentDate.DayOfWeek != DayOfWeek.Saturday && currentDate.DayOfWeek != DayOfWeek.Sunday)
{
TimeSpan realStartTime = GetStartTimeForDay(start, end, workStartTime, currentDate);
TimeSpan realEndTime = GetEndTimeForDay(start, end, workEndTime, currentDate);
if (realStartTime >= workEndTime || realEndTime <= workStartTime)
{
result.Add(0);
}
else
{
result.Add(GetHoursInTimePeriod(realStartTime, realEndTime));
}
}
currentDate = currentDate.AddDays(1);
}
return result.ToArray();
}
private static TimeSpan GetStartTimeForDay(DateTime start, DateTime end, TimeSpan workStartTime, DateTime day)
{
return day == start.Date ? (start.TimeOfDay < workStartTime ? workStartTime : start.TimeOfDay) : workStartTime;
}
private static TimeSpan GetEndTimeForDay(DateTime start, DateTime end, TimeSpan workEndTime, DateTime day)
{
return day == end.Date ? (end.TimeOfDay < workEndTime ? end.TimeOfDay : workEndTime) : workEndTime;
}
private static double GetHoursInTimePeriod(TimeSpan start, TimeSpan end)
{
return (end - start).TotalHours;
}
Source : https://stackoverflow.com/questions/47301975/c-sharp-get-working-hours-within-a-time-span
Friday, December 28, 2018
Wednesday, December 19, 2018
ASP.NET MVC Url Route supporting (dot)
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
</modules>
</system.webServer>
source : https://stackoverflow.com/questions/9273987/asp-net-mvc-url-route-supporting-dot
<modules runAllManagedModulesForAllRequests="true">
</modules>
</system.webServer>
source : https://stackoverflow.com/questions/9273987/asp-net-mvc-url-route-supporting-dot
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
- Prerequisites
- The Silent Updater Core
- Implementations
- Preparing the Desktop Application for ClickOnce Testing
- Configuring the Installer
- Setting the Desktop Application Assembly Version
- Publishing the Desktop Application to the Web Application
- Including Published Files into the Web Application
- LocalHost Installation fails (IIS / IIS Express)
- How to configure Dev Computer for Offline Host Testing
- Running Visual Studio in Administrator Mode
- Visual Studio and Local Custom Domain Hosting
- Installing and Testing Silent Updating
- Hosting on a Web Service (IIS)
- Summary
- Credits and Other Related Links
- History
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
Hide Shrink Copy Code
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:
- Starting the service, unhandled application exceptions, and rebooting into the new version.
- 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.
Hide Shrink Copy Code
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.
Hide Shrink Copy Code
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:
- 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.
- If any unhandled exceptions occur, to (optionally) prevent restarting and end in a possible endless exception cycle.
Hide Shrink Copy Code
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
Hide Shrink Copy Code
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:
Hide Shrink Copy Code
<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.
Hide Shrink Copy Code
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:
Hide Copy Code
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:
- 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.
- If any unhandled exceptions occur, to (optionally) prevent restarting and end in a possible endless exception cycle.
Hide Shrink Copy Code
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:
- Check the default settings
- Automatically generate the Testing Signing Certificate
- Build the application
- Create the installation and copy all relevant files to the web application
- 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:
- Go to the web application, make sure that hidden files are visible, and click th refresh button
- Expand the folders so that you can see the new installation files
- 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:
Hide Shrink Copy Code
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.
- Modifying the
applicationhost.config
file for Custom Domains - Updating
Hosts
file - 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
):
Hide Copy Code
127.0.0.1 silentupdater.net
127.0.0.1 www.silentupdater.net
And the following in the
applicationhost.config
file:
Hide Copy Code
<!-- 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.
- 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.
- Optionally: Set the "Start URL" to http://
www.silentupdater.net
- 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:Hide Copy Code<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>
- If running MVC: make sure the "applicationPool" is set to one of the "Integrated" options (like "Clr2IntegratedAppPool").
- Open your hosts file and add the line
127.0.0.1 www.silentupdater.net
. - 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:
- Go to the Visual Studio
IDE
folder where thedevenv.exe
file is located. For VS2017, it is located by default inC:\Program Files (x86)\Microsoft Visual Studio\2017\[version]\Common7\IDE
- Hold the
Shift
key and RightClick on thedevenv.exe
file - Click on
Run as administrator
- Open the solution, set the web server as "Set as Startup Project"
- 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.
- Publish the application to your MVC Server
- Make sure that you included published files before updating and resarting the server
- Install the application
- Run the application (don't stop it)
- Update the version number and make a noticeable change (eg: application background color)
- Compile, publish, start server
- Wait up to 60 seconds whilst watching the application's StausBar.
- 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:
Hide Copy Code
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
.
Hide Copy Code
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.
Hide Copy Code
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:
- ClickOnce Security and Deployment - Microsoft Docs[^]
- Troubleshooting ClickOnce Deployments - Microsoft Docs[^]
- Original Silent Update author & article: Silently updatable single instance WPF ClickOnce application[^]
- ClickOnce Application Error: Deployment and application do not have matching security zones[^]
- Configure Visual Studio to include update location[^]
- Using Custom Domains With IIS Express[^]
- How to map a local ip to a hostname[^]
- Server and Client Configuration Issues in ClickOnce Deployments[^]
- MVC ClickOnce applications on Azure[^]
- How to Run Visual Studio as Administrator by default[^]
source : https://www.codeproject.com/Articles/1208414/%2FArticles%2F1208414%2FSilent-ClickOnce-Installer-for-WPF-Winforms-in-Csh
Subscribe to:
Posts (Atom)