目录
WPF(Windows Presentation Foundation)
使用Microsoft Edge或Internet Explorer进行ClickOnce安装
使用Google Chrome或Mozilla Firefox进行ClickOnce安装
介绍
本文全面介绍了Microsoft ClickOnce安装程序,其中包含一个基本的WinForm/WPF C#/VB静默Updater框架。本文介绍如何在本地实现、故障排除和测试,以及如何发布到实时MVC Web服务器。
如果下载解决方案并按照本文进行操作,您将:
- 配置适用于所有主要Web浏览器的ClickOnce安装
- 自动创建ClickOnce清单签名证书
- 发布到Web应用程序
- 为Web应用程序设置本地自定义域
- 下载ClickOnce安装程序,运行并安装应用程序
- 更新已发布的文件
- 观看静默更新程序在应用程序运行时自动下载和更新
概述
我已经了解了许多安装应用程序的方法,以及如何让用户使用我发布的应用程序的最新版本。对于像我这样的小型企业来说,使用多个版本的应用程序进行碎片化是一个令人头疼的问题。
Microsoft、Apple和Google商店应用程序都具有自动更新用户设备上安装的应用程序的机制。我需要一个简单的自动化系统,以确保用户始终保持最新状态,并且快速透明地推送更改。ClickOnce看起来像,并被证明是解决方案:
ClickOnce是一种部署技术,使您能够创建基于Windows的自我更新应用程序,这些应用程序可以通过最少的用户交互进行安装和运行。可以通过三种不同的方式发布ClickOnce应用程序:从网页、从网络文件共享或从媒体(如 CD-ROM)。...Microsoft文档[^]
我不喜欢更新在运行应用程序之前与检查的工作方式。感觉有点业余。因此,快速的Google搜索[^]找到了Ivan Leonenko的静默可更新的单实例WPF ClickOnce应用程序文章。
Ivan的文章是Silent ClickOnce更新程序的一个很好的实现,但是有点粗糙,有轻微的问题,并且似乎不再受支持。以下文章解决了这个问题:
- C#和VB中用于WinForm和WPF应用程序的预构建应用程序框架可供使用
- 清理了代码并更改为单实例类
- WinForm和WPF示例框架都包括优雅的未经处理的应用程序异常关闭
- 添加了示例MVC Web服务器主机
- 添加了有关如何执行本地化IIS/IIS Express主机故障排除和测试的说明
- 添加了对实时网站上托管IIS的MVC 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清单”框。测试时,将自动生成证书。下面是首次发布后的证书示例:
接下来,我们需要设置我们的发布配置文件和设置。首先是设置发布属性默认值:
“发布文件夹位置”指向已发布文件所在的物理位置。“安装文件夹”是Web服务器上ClickOnce安装程序从中下载文件的位置。我突出显示了“安装文件夹”,以显示可能存在问题的位置,我们将在稍后运行“发布向导”时看到这些问题。
“安装模式和设置”设置为“离线可用”,以便应用程序可以在未连接到互联网时运行。
接下来,我们需要设置安装程序先决条件。在这里,我们将设置.NET Framework版本。安装程序将检查用户的计算机上是否安装了正确的框架版本。如果没有,它将自动为您运行该过程。
接下来,我们需要设置更新设置。在这里,我们不希望ClickOnce安装程序在应用程序运行之前运行并检查更新。这会让人感觉很业余,并在启动时减慢我们的应用程序的加载速度。相反,我们希望静默更新程序在应用程序启动后完成工作。因此,我们取消选中“应用程序应检查更新”。
注意:我突出显示了“更新位置(如果与发布位置不同)”部分。文档未提及此可选设置在某些情况下将如何影响安装程序。我在下面有一个部分,将在下面更详细地讨论不填写此字段的后果。
最后,我们需要设置“选项”。首先,部署设置。我们希望自动发布安装页面脚本并设置部署文件扩展名:
接下来,为了安全起见,我们不希望通过URL激活清单,但我们确实希望将清单信息用于用户信任。最后,我更喜欢创建一个易于访问的桌面快捷方式,这比让他们在“开始”菜单中找到我们的应用程序更容易。;)
设置桌面应用程序程序集版本
发布版本不同于程序集和文件版本。用户本地计算机上的ClickOnce安装程序使用发布版本来标识版本和更新。程序集版本将显示给用户。程序集版本在“应用程序属性>”选项卡上设置:
将桌面应用程序发布到Web应用程序
设置发布默认值后,我们可以使用发布向导执行以下操作:
- 检查默认设置
- 自动生成测试签名证书
- 构建应用程序
- 创建安装并将所有相关文件复制到Web应用程序
- 自动递增ClickOnce用于标识更新的发布版本
步骤 1——发布位置
您可以直接发布到您的实时Web服务器,但是,我更喜欢在“上线”之前进行暂存和测试。因此,我将发布过程指向Web应用程序项目中的路径。
步骤 2——ClickOnce安装程序下载位置
这将是ClickOnce安装程序将查找要安装的文件以及稍后更新的路径/URL。
注意:我已经突出显示了http://localhost/...路径。这将由向导更改,我们可以看到向导的最后一步会发生什么。
步骤 3——单击一次操作模式
我们希望应用程序在本地安装,并且在未连接到互联网时能够脱机运行。
步骤 4——完成——查看设置
在发布向导的步骤2 ,我们指定了用于测试的安装路径http://localhost,但发布向导将其更改为http://[local_network_name]。发布向导执行此操作的原因尚不清楚。
步骤 5——发布到We服务器
单击“发布向导”中的“完成”按钮后,发布过程将创建“测试ClickOnce清单签名证书”,生成应用程序(如果需要),然后创建安装文件,并将其复制到准备包含的Web应用程序。
通过运行Web应用程序并安装应用程序完成完整测试后,只需单击“立即发布”按钮即可进一步发布。“立即发布”将使用发布向导中使用的所有设置。
将已发布的文件包含在Web应用程序中
若要包括已发布的安装文件,请在“解决方案资源管理器”中:
1、转到Web应用程序,确保隐藏文件可见,然后单击“刷新”按钮。
2、展开文件夹,以便可以看到新的安装文件:
3、选择要包含的文件和文件夹,单击鼠标右键,然后选择“包含在项目中”:
本地主机安装失败(IIS/IIS Express)
我在尝试进行本地Web服务器ClickOnce更新测试时遇到的一个问题是Visual Studio ClickOnce Publisher做了一件奇怪的事情。http://localhost更改为http://[network computer name]。因此,如果您通过http://localhost下载并运行ClickOnce Setup.exe应用程序,您将看到如下所示的内容:
以下是安装.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'.
如果我们将http://macbookpro:60492/Installer/WpfCBApp/WpfCBAppVB.application放入网络浏览器,我们可以看到它失败的原因:
解决方案是将开发计算机配置为Web服务器。
如何为脱机主机测试配置开发计算机
配置开发计算机以进行Web托管ClickOnce更新测试。
- 修改自定义域的applicationhost.config文件
- 正在更新Hosts文件
- 在管理员模式下运行VS(访问主机文件)
使用自定义域配置IIS Express
对于VS2015和VS2017,applicationhost.config文件位于.vs\config文件夹的“solution”文件夹中。在该文件夹中,您将找到 applicationhost.config 文件。
在网站的“属性> Web”选项卡中,使用以下配置:
主机文件中有以下内容(位于 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,过程略有不同。
1、右键单击Web 应用程序项目>“Web”属性>,然后按如下所示配置“服务器”部分:
- 从下拉列表中选择“IIS Express”
- 项目网址:http://localhost
- 覆盖应用程序根URL:http://www.silentupdater.net
- 单击“创建虚拟目录”按钮(如果在此处收到错误,则可能需要禁用IIS 5/6/7/8,将IIS的“默认站点”更改为:80端口以外的任何内容,确保Skype等应用程序不使用端口80。
2、可选:将“起始 URL”设置为http://www.silentupdater.net
3、打开%USERPROFILE%\My Documents\IISExpress\config\applicationhost.config(Windows XP、Vista和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>
4、如果运行 MVC:确保将“applicationPool”设置为“Integrated”选项之一(如“Clr2IntegratedAppPool”)。
5、打开hosts文件并添加行127.0.0.1 www.silentupdater.net。
6、开始您的应用程序!
注意:请记住以管理员身份运行Visual Studio 2015实例!否则,UAC将阻止VS&IIS Express查看对hosts文件所做的更改。
在管理员模式下运行Visual Studio
有几种在管理员模式下运行的方法。每个人都有自己喜欢的方式。一种方法是:
- 转到devenv.exe文件所在的Visual Studio IDE文件夹。对于VS2017,它默认位于C:\Program Files(x86)\Microsoft Visual Studio\2017\[version]\Common7\IDE
- 按住 Shift 键并右键单击devenv.exe文件
- 单击以管理员身份运行
- 打开解决方案,将Web服务器设置为“设置为启动项目"
- 运行网络服务器
Visual Studio和本地自定义域托管
在进行任何测试之前,我们需要更新发布配置文件以反映新的自定义域www.silentupdater.net。
配置发布下载
我们需要设置ClickOnce安装程序查找更新的位置。路径需要从http://localhost更改为我们的自定义域www.silentupdater.net。
现在我们可以重新访问上面的发布向导步骤,完成屏幕现在应该是:
一旦发布向导过程完成,安装文件和文件夹包含在Web服务器的项目中,我们现在可以运行并执行ClickOnce安装。
安装和测试静默更新
安装、重新发布、重新托管、运行、更新和重新启动的步骤。
- 将应用程序发布到MVC服务器
- 确保在更新和重新启动服务器之前包含已发布的文件
- 安装应用程序
- 运行应用程序(不要停止它)
- 更新版本号并进行明显的更改(例如:应用程序背景颜色)
- 编译、发布和启动服务器
- 等待60秒,在观看应用程序的StatusBar。
- 静默更新完成后,单击“重新启动”按钮,然后查找更改和更新的版本号。
使用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
总结
希望这篇文章通过引导您完成Microsoft文档中的漏洞,让您(比我)有更多的头发和更少的挫败感;填补Ivan原文留下的空白;并避免我和其他人随着时间的推移遇到的常见错误。
引用列表和其他相关链接
本文包含大量在一段时间内研究和收集的碎片化信息。以下是使这成为可能的各种人员和资源的链接:
- ClickOnce安全性和部署——Microsoft文档
- ClickOnce部署疑难解答——Microsoft文档
- 原始静默更新作者和文章:静默可更新的单实例WPF ClickOnce应用程序
- ClickOnce应用程序错误:部署和应用程序没有匹配的安全区域
- 将Visual Studio配置为包含更新位置
- 将自定义域与IIS Express配合使用
- 如何将本地IP映射到主机名
- ClickOnce部署中的服务器和客户端配置问题
- Azure上的MVC ClickOnce应用程序
- 默认情况下,如何以管理员身份运行Visual Studio
https://www.codeproject.com/Articles/1208414/Silent-ClickOnce-Installer-for-WPF-Winforms-in-Csh