Winform & WPF 的 C# & VB 的静默 ClickOnce 安装程序





5.00/5 (30投票s)
本文全面介绍了 Microsoft ClickOnce 安装程序,并提供了一个基于 WinForm/WPF C#/VB 的静默更新程序框架,涵盖了如何实现、故障排除、本地测试以及发布。
引言
本文全面介绍了 Microsoft ClickOnce 安装程序,并在 Ivan Leonenko 之前发表的一篇文章的基础上进行了改进,提供了一个基于 WinForm/WPF C#/VB 的静默更新程序框架。本文涵盖了如何实现、故障排除、本地测试以及发布到实时 MVC Web 服务器。
如果您下载解决方案并按照本文操作,您将:
- 配置一个适用于所有主流网络浏览器的 ClickOnce 安装
- 自动创建 ClickOnce 清单签名证书
- 发布到 Web 应用程序
- 为您的 Web 应用程序设置本地自定义域
- 下载 ClickOnce 安装程序,运行并安装应用程序
- 更新发布的文件
- 在应用程序运行时观察静默更新程序自动下载和更新
目录
- 概述
- 必备组件
- 静默更新器核心
- 实现
- 为 ClickOnce 测试准备桌面应用程序
- 配置安装程序
- Visual Studio 和本地自定义域托管
- 安装和测试静默更新
- 在 Web 服务 (IIS) 上托管
- 摘要
- 鸣谢及其他相关链接
- 历史
概述
我研究了多种安装应用程序的方法,以及如何让用户及时获取我发布的最新版本应用程序。应用程序存在多个版本碎片化,这对我这样的小企业来说是一个巨大的难题。
Microsoft、Apple 和 Google 的应用商店都有机制来自动化更新用户设备上安装的应用程序。我需要一个简单且自动化的系统,确保用户始终保持最新,并且推送更改快速透明。ClickOnce 看起来是,并且事实证明是,解决方案。
ClickOnce 是一种部署技术,可让您创建可自动更新的基于 Windows 的应用程序,这些应用程序可以通过最少的用户交互进行安装和运行。您可以以三种不同的方式发布 ClickOnce 应用程序:从网页、从网络文件共享或从 CD-ROM 等媒体。... Microsoft Docs[^]
我不喜欢在运行应用程序之前进行检查的更新方式。感觉有点业余。于是快速 Google 搜索[^] 找到了 Ivan Leonenko 的 可静默更新的单实例 WPF ClickOnce 应用程序[^] 一文。
Ivan 的文章是静默 ClickOnce 更新程序的一个很好的实现,但是有点粗糙,存在一些小问题,并且似乎不再受支持。下面的文章解决了这个问题,并提供了以下内容:
- 用于 C# 和 VB 的 WinForm 和 WPF 应用程序的预构建应用程序框架,随时可用
- 清理了代码并更改为单实例类
- WinForm 和 WPF 示例框架都包含优雅的未处理应用程序异常关闭
- 添加了一个示例 MVC Web 服务器主机
- 添加了有关如何进行本地 IIS/IIS Express 主机故障排除和测试的说明
- 添加了用于在实时网站上进行 IIS 托管的 MVC ClickOnce 文件支持
- 添加了 ClickOnce 用户安装故障排除帮助
- 所有示例都包含 C# 和 VB 版本
先决条件
本文的项目是基于以下考虑构建的:
- C#6 最低要求(在 属性 > 生成 > 高级 > 通用 > 语言版本 > C#6 中设置)
- 使用 VS2017 构建(VS2015 也可以加载、构建和运行)
- 首次加载代码时,您需要恢复 Nuget 包。
- 需要按照文章查看静默更新的实际效果
静默更新器核心
实际完成所有工作的代码非常简单
- 单实例类(新)
- 每 60 秒检查一次更新
- 启动带有反馈的后台/异步更新
- 静默处理下载问题 + 每 60 秒重试
- 更新准备就绪时通知
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();
}
}
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
实现
实现 ClickOnce 静默更新支持有两个部分:
- 启动服务、未处理的应用程序异常以及重启到新版本
- 用户反馈和交互
WinForm 和 WPF 应用程序的实现略有不同。将分别介绍每个应用程序。
WinForm
首先,我们需要连接 SilentUpdater
类。以下代码将
- 获取对
SilentUpdater
类实例的引用 - 监听
SilentUpdater
类的事件 - 下载更新时更新 UI
- 下载完成后显示重启按钮
- 点击重启按钮时重启应用程序
最后,C# 和 VB WinForm 应用程序的启动方式略有不同。因此,在 VB 版本中,为了将引导代码与表单代码分开,我们需要在主表单初始化时手动调用启动/引导代码。
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
}
}
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
以上代码还将支持通知当前已安装的应用程序版本。
作为 WinForm 框架一部分的 Application
类简化了所需的代码。然而,由于 Application
类是密封的,我们不能编写扩展来用我们自己的方法调用来扩展它。因此,我们需要一个 AppProcessHelper
类来启用
- 单应用程序实例管理
- 应用程序的条件重启
- 已安装版本号检索
同时运行多个应用程序副本,并且它们都尝试自行更新,这不是一个好主意。本文的要求是只运行应用程序的一个实例。因此,本文不会涵盖如何处理具有单一实例负责静默更新的多个运行实例。
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();
}
}
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
我将重启分为两个步骤,并提供了阻止重启的选项。我这样做有两个原因:
- 给予应用程序机会,让用户选择保存任何未保存的工作,如果意外按下则中止,并允许应用程序在最终确定关闭过程之前进行清理。
- 如果发生任何未处理的异常,(可选)阻止重新启动并结束可能无休止的异常循环。
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();
}
}
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)
首先,我们需要连接 SilentUpdater
类。这是代码隐藏示例。下载中也包含了 MVVM 版本。我将代码放在一个名为 StatusBarView
的独立 UserControl
中。这将使代码与主窗口中的其余代码分离。
以下代码将:
- 获取对
SilentUpdater
类实例的引用 - 监听
SilentUpdater
类的事件 - 下载更新时更新 UI
- 下载完成后显示重启按钮
- 点击重启按钮时重启应用程序
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
}
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
一个应该突出的地方是 WinForm 和 WPF 版本几乎相同。
这是 UI 的 XAML
<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>
以上代码还将支持通知当前已安装的应用程序版本。
WPF 框架的一部分的 Application
类不支持重启,但该类不是 sealed
,因此我们可以编写扩展以我们自己的方法调用来扩展它。因此,我们需要 WinForm AppProcessHelper
类的一个略有不同的版本,以实现:
- 单应用程序实例管理
- 支持重启应用程序(Ivan 的文章中有一个很好的实现,我们将使用它)
- 应用程序的条件重启
- 已安装版本号检索
同样,同时运行多个应用程序副本,并且它们都尝试自行更新,这不是一个好主意。本文的要求是只运行应用程序的一个实例。因此,本文不会涵盖如何处理具有单一实例负责静默更新的多个运行实例。
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);
}
}
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
这是 WPF 框架 Application
类的扩展
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();
}
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
同样,出于相同的原因,我将重启分为两个步骤,并提供了阻止重启的选项。
- 给予应用程序机会,让用户选择保存任何未保存的工作,如果意外按下则中止,并允许应用程序在最终确定关闭过程之前进行清理。
- 如果发生任何未处理的异常,(可选)阻止重新启动并结束可能无休止的异常循环。
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();
}
}
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
您可以运行应用程序并测试单实例支持。但是,要测试 ClickOnce 安装,您需要首先发布应用程序,托管安装程序,安装,运行,然后发布并托管更新版本。这将在以下部分中介绍。
为 ClickOnce 测试准备桌面应用程序
测试任何 ClickOnce 更新支持都需要在实时服务器 (IIS) 或本地主机 (IIS / IIS Express) 上安装和运行。下一节将涵盖:
- 创建一个基于 Web 的 ClickOnce 安装程序
- 在本地和实时 MVC 服务器上托管 ClickOnce 安装程序
- 如何在本地机器上运行 Web 测试安装以及所需的设置
- 如何避免 Chrome 和 Firefox 浏览器出现“部署和应用程序安全区域不匹配”的问题
- 如何测试静默更新程序
配置安装程序
您应该始终签署 ClickOnce 清单,以减少任何黑客攻击的可能性。您可以购买并使用自己的(发布应用程序真正需要),或者让 VS 为您生成一个(仅适用于测试)。即使只测试应用程序,这也是一个好习惯。为此,请转到 属性 > 签名 > 勾选“签署 ClickOnce 清单”。
注意:上面我们只勾选了“签署 ClickOnce 清单”框。测试时,证书会自动为您生成。这是第一次发布后证书的示例
接下来,我们需要设置发布配置文件和设置。首先是设置 发布 属性的默认值。
“发布文件夹位置”指向已发布文件的物理位置。“安装文件夹”是 ClickOnce 安装程序将查找并从中下载文件的 Web 服务器上的位置。我突出显示了“安装文件夹”,以显示稍后在运行“发布向导”时可能出现问题的位置。
“安装模式和设置”设置为“可离线使用”,以便应用程序在未连接互联网时也能运行。
接下来,我们需要设置安装程序的先决条件。在这里,我们将设置 .NET Framework 版本。安装程序将检查用户计算机上是否安装了正确的框架版本。如果未安装,它将自动为您运行该过程。
接下来,我们需要设置更新设置。在这里,我们不希望 ClickOnce 安装程序在应用程序运行之前检查更新。这会让人感觉业余,并会减慢我们应用程序启动时的加载速度。相反,我们希望静默更新程序在应用程序启动后进行工作。因此,我们取消选中“应用程序应检查更新”。
注意:我已突出显示“更新位置(如果与发布位置不同)”部分。文档没有提及此可选设置在某些情况下如何影响安装程序。下面有一个部分将更详细地讨论未填写此字段的后果。
最后,我们需要设置“选项”。首先是部署设置。我们希望自动发布安装页面脚本并设置部署文件扩展名。
接下来,为了安全起见,我们不希望通过 URL 激活清单,但我们确实希望使用清单信息来获取用户信任。最后,我更喜欢创建桌面快捷方式以便于访问,比让他们在“开始”菜单中找到我们的应用程序更容易。;)
设置桌面应用程序程序集版本
发布版本不同于程序集和文件版本。发布版本由用户本地机器上的 ClickOnce 安装程序用于识别版本和更新。程序集版本将显示给用户。程序集版本在 属性 > 应用程序选项卡中设置。
将桌面应用程序发布到 Web 应用程序
设置发布默认值后,我们可以使用发布向导来
- 检查默认设置
- 自动生成测试签名证书
- 构建应用程序
- 创建安装并复制所有相关文件到 Web 应用程序
- 自动增加 ClickOnce 用于识别更新的发布版本
步骤 1 - 发布位置
您可以直接发布到您的实时 Web 服务器,但是我更喜欢在“上线”之前进行阶段性测试。所以我将发布过程指向我的 Web 应用程序项目中的路径。
步骤 2 - ClickOnce 安装程序下载位置
这将是 ClickOnce 安装程序查找安装文件和后续更新文件的路径/URL。
注意:我已突出显示 https:///...
路径。这将由向导更改,我们可以在向导的最后一步看到会发生什么。
步骤 3 - ClickOnce 操作模式
我们希望应用程序在本地安装,并且在未连接互联网时能够离线运行。
步骤 4 - 完成 - 审查设置
在发布向导的第 2 步中,我们指定测试的安装路径为 https://
,但是发布向导将其更改为 http://[local_network_name]
。发布向导为何这样做尚不清楚。
步骤 5 - 发布到 Web 服务器
一旦您点击 发布向导 中的 完成 按钮,发布过程将创建测试 ClickOnce 清单签名证书,构建应用程序(如果需要),然后创建安装文件,并将它们复制到 Web 应用程序中,准备好包含。
完成运行 Web 应用程序和安装应用程序的完整测试后,进一步发布只需单击“立即发布”按钮即可。发布向导中使用的所有设置都将由“立即发布”使用。
将已发布文件包含到 Web 应用程序中
要包含已发布的安装文件,请在解决方案资源管理器中
- 转到 Web 应用程序,确保隐藏文件可见,然后单击“刷新”按钮。
- 展开文件夹,以便您可以看到新的安装文件。
- 选择要包含的文件和文件夹,右键单击,然后选择“包含在项目中”
本地主机安装失败 (IIS / IIS Express)
当我尝试进行本地 Web 服务器 ClickOnce 更新测试时,我遇到了一个问题,即 Visual Studio ClickOnce 发布器做了一件奇怪的事情。https://
变成了 http://[网络计算机名]
。因此,如果您通过 https://
下载并运行 ClickOnce Setup.exe 应用程序,您会看到类似以下内容
这是 install.log 文件
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'.
如果我们在 Web 浏览器中输入 http://macbookpro:60492/Installer/WpfCBApp/WpfCBAppVB.application
,我们就可以看到它为什么失败了。
解决方案是将您的开发计算机配置为 Web 服务器。
如何配置开发计算机进行离线主机测试
配置您的开发机器以进行 Web 托管的 ClickOnce 更新测试。
- 修改 applicationhost.config 文件以支持自定义域
- 更新
Hosts
文件 - 以管理员模式运行 VS(访问 hosts 文件)
使用自定义域配置 IIS Express
对于 VS2015 和 VS2017,applicationhost.config 文件位于“解决方案”文件夹中的 .vs\config 文件夹。在该文件夹中,您会找到 applicationhost.config 文件。
在网站属性 > Web 选项卡中,使用以下配置
在 hosts 文件中(位于 C:\Windows\System32\drivers\etc)包含以下内容:
127.0.0.1 silentupdater.net
127.0.0.1 www.silentupdater.net
并在 applicationhost.config 文件中添加以下内容
<!-- 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>
对于 VS2010 和 VS2013,过程略有不同。
- 右键单击您的 Web 应用程序项目 > 属性 > Web,然后配置“服务器”部分,如下所示:
- 从下拉列表中选择“IIS Express”
- 项目 URL:
https://
- 覆盖应用程序根 URL:
http://www.silentupdater.net
- 点击“创建虚拟目录”按钮(如果您在此处遇到错误,您可能需要禁用 IIS 5/6/7/8,将 IIS 的“默认站点”更改为除端口
:80
之外的任何值,确保 Skype 等应用程序没有使用端口 80)。
- 可选:将“起始 URL”设置为
http://www.silentupdater.net
- 打开 %USERPROFILE%\My Documents\IISExpress\config\applicationhost.config (Windows XP, Vista, and 7),并编辑
<sites>
配置块中的站点定义,使其类似于以下内容:<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>
- 如果运行 MVC:确保“
applicationPool
”设置为“Integrated
”选项之一(例如“Clr2IntegratedAppPool
”)。 - 打开您的 hosts 文件并添加行
127.0.0.1 www.silentupdater.net
。 - 启动你的应用程序!
注意:请记住以管理员身份运行您的 Visual Studio 2015 实例!否则,UAC 将阻止 VS 和 IIS Express 查看对 hosts
文件所做的更改。
以管理员模式运行 Visual Studio
有几种以管理员模式运行的方法。每个人都有自己喜欢的方式。其中一种方法是:
- 转到 Visual Studio IDE 文件夹,其中包含 devenv.exe 文件。对于 VS2017,默认位于 C:\Program Files (x86)\Microsoft Visual Studio\2017\[版本]\Common7\IDE。
- 按住 Shift 键并右键单击 devenv.exe 文件
- 点击 以管理员身份运行
- 打开解决方案,将 Web 服务器设置为“设置为启动项目”
- 运行 Web 服务器
Visual Studio 和本地自定义域托管
在进行任何测试之前,我们需要更新发布配置文件以反映新的自定义域 www.silentupdater.net
。
配置发布下载
我们需要设置 ClickOnce 安装程序将查找更新的位置。路径需要从 https://
更改为我们的自定义域 www.silentupdater.net
。
现在我们可以重新访问上面的 发布向导 步骤,完成屏幕现在应该如下所示
一旦发布向导过程完成,安装文件和文件夹包含在 Web 服务器的项目中,我们现在可以运行并进行 ClickOnce 安装。
安装和测试静默更新
安装、重新发布、重新托管、运行、更新和重启的步骤。
- 将应用程序发布到您的 MVC 服务器
- 在更新和重新启动服务器之前,请确保已包含已发布文件。
- 安装应用程序
- 运行应用程序(不要停止它)
- 更新版本号并进行显着更改(例如:应用程序背景颜色)
- 编译、发布并启动服务器
- 在观察应用程序的
StatusBar
时等待最多 60 秒。 - 静默更新完成后,单击“重启”按钮,然后查看更改和更新后的版本号。
使用 Microsoft Edge 或 Internet Explorer 进行 ClickOnce 安装
以下是从安装页面到运行的步骤。
下载 Setup.exe 安装程序
安装过程
现在应用程序已准备好运行。由于我们使用的是测试证书,第一次运行时,我们将看到以下屏幕:
使用 Google Chrome 或 Mozilla Firefox 进行 ClickOnce 安装
使用 Chrome 和 Firefox 下载和安装应该与 Edge 和 Internet Explorer 相同。但是,在使用 Chrome 或 Firefox 从安装页面下载安装程序文件并运行安装程序后,您可能会遇到 ClickOnce 安装失败的情况
细节可能看起来像这样
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.
安装失败:部署和应用程序安全区域不匹配
关于此故障的文档非常有限。Microsoft 的 ClickOnce 部署故障排除[^] 页面没有任何合适的解决方案。
结果是 Chrome 和 Firefox 都会检查可选的“属性 > 发布 > 更新 > 更新位置(如果与发布位置不同)”部分,并将其与“发布 > 安装文件夹 URL”进行比较。如果这两个位置不匹配,安装就会失败,并显示“部署和应用程序安全区域不匹配”。
此设置位于 .application 文件中 <deployment>
部分的 <deploymentProvider codebase=... />
子部分中。
这是对我们的“属性 > 发布 > 更新 > 更新位置(如果与发布位置不同)”部分的修正
运行和测试静默更新
在测试静默更新时,务必在点击“立即发布”按钮之前更新程序集版本。这使得更容易查看您正在测试哪个版本。
在进行测试时,将安装文件包含到您的 Web 应用程序中也是一个好习惯。这样,当发布发布应用程序版本时,您就不会忘记在将其推送到您的网站之前执行此步骤。
正常状态
当应用程序运行且没有更新时,StatusBar
将只报告当前程序集版本。
WinForm 应用程序
WPF 应用程序
更新状态
当应用程序更新开始时,StatusBar
将报告下载状态。
WinForm 应用程序
WPF 应用程序
已更新状态
当应用程序更新完成后,StatusBar
将显示完成消息和重启按钮。
WinForm 应用程序
WPF 应用程序
重启后的新版本
最后,在单击重启按钮或应用程序关闭并重新启动后,StatusBar
将反映更新后的程序集版本。
WinForm 应用程序
WPF 应用程序
在 Web 服务 (IIS) 上托管
在实时网站上托管时,我们需要在 MVC 服务器上启用对安装文件的支持。我为 Azure Web 应用程序使用了以下内容:
RouteConfig.CS/VB
确保我们接受安装文件的请求并将请求路由到 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 }
);
}
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
FileController
确保返回所请求的文件时带有正确的 MIME 类型头。
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";
}
}
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
总结
希望这篇文章能让您少掉头发(比我少),少些挫折,因为它引导您填补了微软文档中的空白;弥补了伊万原始文章中遗漏的部分;并避免了我和其他人多年来遇到的常见错误。
鸣谢及其他相关链接
这篇文章包含了大量零散的信息,这些信息是经过一段时间的研究和收集而来的。下面是使之成为可能的各种人物和资源的链接。
- ClickOnce 安全和部署 - Microsoft Docs[^]
- ClickOnce 部署故障排除 - Microsoft Docs[^]
- 原始静默更新作者和文章:可静默更新的单实例 WPF ClickOnce 应用程序[^]
- ClickOnce 应用程序错误:部署和应用程序安全区域不匹配[^]
- 配置 Visual Studio 以包含更新位置[^]
- 使用 IIS Express 的自定义域[^]
- 如何将本地 IP 映射到主机名[^]
- ClickOnce 部署中的服务器和客户端配置问题[^]
- Azure 上的 MVC ClickOnce 应用程序[^]
- 如何默认以管理员身份运行 Visual Studio[^]
历史
- 2017年10月1日 - v1.0 - 首次发布
- 2023年1月10日 - v1.1 - 添加了 .Net Framework 4.8 的下载