65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (30投票s)

2017年10月1日

CPOL

18分钟阅读

viewsIcon

77358

downloadIcon

2758

本文全面介绍了 Microsoft ClickOnce 安装程序,并提供了一个基于 WinForm/WPF C#/VB 的静默更新程序框架,涵盖了如何实现、故障排除、本地测试以及发布。

引言

本文全面介绍了 Microsoft ClickOnce 安装程序,并在 Ivan Leonenko 之前发表的一篇文章的基础上进行了改进,提供了一个基于 WinForm/WPF C#/VB 的静默更新程序框架。本文涵盖了如何实现、故障排除、本地测试以及发布到实时 MVC Web 服务器。

如果您下载解决方案并按照本文操作,您将:

  • 配置一个适用于所有主流网络浏览器的 ClickOnce 安装
  • 自动创建 ClickOnce 清单签名证书
  • 发布到 Web 应用程序
  • 为您的 Web 应用程序设置本地自定义域
  • 下载 ClickOnce 安装程序,运行并安装应用程序
  • 更新发布的文件
  • 在应用程序运行时观察静默更新程序自动下载和更新

目录

概述

我研究了多种安装应用程序的方法,以及如何让用户及时获取我发布的最新版本应用程序。应用程序存在多个版本碎片化,这对我这样的小企业来说是一个巨大的难题。

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 静默更新支持有两个部分:

  1. 启动服务、未处理的应用程序异常以及重启到新版本
  2. 用户反馈和交互

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

我将重启分为两个步骤,并提供了阻止重启的选项。我这样做有两个原因:

  1. 给予应用程序机会,让用户选择保存任何未保存的工作,如果意外按下则中止,并允许应用程序在最终确定关闭过程之前进行清理。
  2. 如果发生任何未处理的异常,(可选)阻止重新启动并结束可能无休止的异常循环。
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

同样,出于相同的原因,我将重启分为两个步骤,并提供了阻止重启的选项。

  1. 给予应用程序机会,让用户选择保存任何未保存的工作,如果意外按下则中止,并允许应用程序在最终确定关闭过程之前进行清理。
  2. 如果发生任何未处理的异常,(可选)阻止重新启动并结束可能无休止的异常循环。
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 应用程序

设置发布默认值后,我们可以使用发布向导来

  1. 检查默认设置
  2. 自动生成测试签名证书
  3. 构建应用程序
  4. 创建安装并复制所有相关文件到 Web 应用程序
  5. 自动增加 ClickOnce 用于识别更新的发布版本

步骤 1 - 发布位置

您可以直接发布到您的实时 Web 服务器,但是我更喜欢在“上线”之前进行阶段性测试。所以我将发布过程指向我的 Web 应用程序项目中的路径。

步骤 2 - ClickOnce 安装程序下载位置

这将是 ClickOnce 安装程序查找安装文件和后续更新文件的路径/URL。

注意:我已突出显示 https:///... 路径。这将由向导更改,我们可以在向导的最后一步看到会发生什么。

步骤 3 - ClickOnce 操作模式

我们希望应用程序在本地安装,并且在未连接互联网时能够离线运行。

步骤 4 - 完成 - 审查设置

在发布向导的第 2 步中,我们指定测试的安装路径为 https://,但是发布向导将其更改为 http://[local_network_name]。发布向导为何这样做尚不清楚。

步骤 5 - 发布到 Web 服务器

一旦您点击 发布向导 中的 完成 按钮,发布过程将创建测试 ClickOnce 清单签名证书,构建应用程序(如果需要),然后创建安装文件,并将它们复制到 Web 应用程序中,准备好包含。

完成运行 Web 应用程序和安装应用程序的完整测试后,进一步发布只需单击“立即发布”按钮即可。发布向导中使用的所有设置都将由“立即发布”使用。

将已发布文件包含到 Web 应用程序中

要包含已发布的安装文件,请在解决方案资源管理器中

  1. 转到 Web 应用程序,确保隐藏文件可见,然后单击“刷新”按钮。
  2. 展开文件夹,以便您可以看到新的安装文件。

  3. 选择要包含的文件和文件夹,右键单击,然后选择“包含在项目中

本地主机安装失败 (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 更新测试。

  1. 修改 applicationhost.config 文件以支持自定义域
  2. 更新 Hosts 文件
  3. 以管理员模式运行 VS(访问 hosts 文件)

使用自定义域配置 IIS Express

对于 VS2015 和 VS2017applicationhost.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,过程略有不同。

  1. 右键单击您的 Web 应用程序项目 > 属性 > Web,然后配置“服务器”部分,如下所示:
    • 从下拉列表中选择“IIS Express
    • 项目 URL: https://
    • 覆盖应用程序根 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, 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>
  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

有几种以管理员模式运行的方法。每个人都有自己喜欢的方式。其中一种方法是:

  1. 转到 Visual Studio IDE 文件夹,其中包含 devenv.exe 文件。对于 VS2017,默认位于 C:\Program Files (x86)\Microsoft Visual Studio\2017\[版本]\Common7\IDE。
  2. 按住 Shift 键并右键单击 devenv.exe 文件
  3. 点击 以管理员身份运行
  4. 打开解决方案,将 Web 服务器设置为“设置为启动项目
  5. 运行 Web 服务器

Visual Studio 和本地自定义域托管

在进行任何测试之前,我们需要更新发布配置文件以反映新的自定义域 www.silentupdater.net

配置发布下载

我们需要设置 ClickOnce 安装程序将查找更新的位置。路径需要从 https:// 更改为我们的自定义域 www.silentupdater.net

现在我们可以重新访问上面的 发布向导 步骤,完成屏幕现在应该如下所示

一旦发布向导过程完成,安装文件和文件夹包含在 Web 服务器的项目中,我们现在可以运行并进行 ClickOnce 安装。

安装和测试静默更新

安装、重新发布、重新托管、运行、更新和重启的步骤。

  1. 将应用程序发布到您的 MVC 服务器
    • 在更新和重新启动服务器之前,请确保已包含已发布文件。
  2. 安装应用程序
  3. 运行应用程序(不要停止它)
  4. 更新版本号并进行显着更改(例如:应用程序背景颜色)
  5. 编译、发布并启动服务器
  6. 在观察应用程序的 StatusBar 时等待最多 60 秒。
  7. 静默更新完成后,单击“重启”按钮,然后查看更改和更新后的版本号。

使用 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

总结

希望这篇文章能让您少掉头发(比我少),少些挫折,因为它引导您填补了微软文档中的空白;弥补了伊万原始文章中遗漏的部分;并避免了我和其他人多年来遇到的常见错误。

鸣谢及其他相关链接

这篇文章包含了大量零散的信息,这些信息是经过一段时间的研究和收集而来的。下面是使之成为可能的各种人物和资源的链接。

历史

  • 2017年10月1日 - v1.0 - 首次发布
  • 2023年1月10日 - v1.1 - 添加了 .Net Framework 4.8 的下载
© . All rights reserved.