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

目录

介绍

概述

先决条件

静默更新程序核心

实现

WinForm

WPF(Windows Presentation Foundation)

准备桌面应用程序以进行ClickOnce测试

配置安装程序

设置桌面应用程序程序集版本

将桌面应用程序发布到Web应用程序

步骤 1——发布位置

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

步骤 3——单击一次操作模式

步骤 4——完成——查看设置​编辑

步骤 5——发布到We服务器

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

本地主机安装失败(IIS/IIS Express)

如何为脱机主机测试配置开发计算机

在管理员模式下运行Visual Studio

Visual Studio和本地自定义域托管

配置发布下载

安装和测试静默更新

使用Microsoft Edge或Internet Explorer进行ClickOnce安装

使用Google Chrome或Mozilla Firefox进行ClickOnce安装

安装失败:部署和应用程序没有匹配的安全区域

运行和测试静默更新

正常状态

正在更新状态

更新状态

重启后的新版本

在Web服务(IIS)上托管

RouteConfig.CS/VB

FileController.CS/VB

总结

引用列表和其他相关链接


介绍

本文全面介绍了Microsoft ClickOnce安装程序,其中包含一个基本的WinForm/WPF C#/VB静默Updater框架。本文介绍如何在本地实现、故障排除和测试,以及如何发布到实时MVC Web服务器。

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

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

概述

我已经了解了许多安装应用程序的方法,以及如何让用户使用我发布的应用程序的最新版本。对于像我这样的小型企业来说,使用多个版本的应用程序进行碎片化是一个令人头疼的问题。

MicrosoftAppleGoogle商店应用程序都具有自动更新用户设备上安装的应用程序的机制。我需要一个简单的自动化系统,以确保用户始终保持最新状态,并且快速透明地推送更改。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静默更新的支持有两个部分:

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

WinFormWPF应用程序的实现略有不同。每个都将单独承保。

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

WPFWindows 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

应该突出的一件事是WinFormWPF版本几乎相同。

下面是UIXAML代码:

<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清单框。测试时,将自动生成证书。下面是首次发布后的证书示例:

接下来,我们需要设置我们的发布配置文件和设置。首先是设置发布属性默认值:

发布文件夹位置指向已发布文件所在的物理位置。安装文件夹Web服务器上ClickOnce安装程序从中下载文件的位置。我突出显示了安装文件夹,以显示可能存在问题的位置,我们将在稍后运行发布向导时看到这些问题。

安装模式和设置设置为离线可用,以便应用程序可以在未连接到互联网时运行。

接下来,我们需要设置安装程序先决条件。在这里,我们将设置.NET Framework版本。安装程序将检查用户的计算机上是否安装了正确的框架版本。如果没有,它将自动为您运行该过程。

接下来,我们需要设置更新设置。在这里,我们不希望ClickOnce安装程序在应用程序运行之前运行并检查更新。这会让人感觉很业余,并在启动时减慢我们的应用程序的加载速度。相反,我们希望静默更新程序在应用程序启动后完成工作。因此,我们取消选中应用程序应检查更新

注意:我突出显示了更新位置(如果与发布位置不同)部分。文档未提及此可选设置在某些情况下将如何影响安装程序。我在下面有一个部分,将在下面更详细地讨论不填写此字段的后果。

最后,我们需要设置选项。首先,部署设置。我们希望自动发布安装页面脚本并设置部署文件扩展名:

接下来,为了安全起见,我们不希望通过URL激活清单,但我们确实希望将清单信息用于用户信任。最后,我更喜欢创建一个易于访问的桌面快捷方式,这比让他们在开始菜单中找到我们的应用程序更容易。;)

设置桌面应用程序程序集版本

发布版本不同于程序集和文件版本。用户本地计算机上的ClickOnce安装程序使用发布版本来标识版本和更新。程序集版本将显示给用户。程序集版本应用程序属性>”选项卡上设置:

将桌面应用程序发布到Web应用程序

设置发布默认值后,我们可以使用发布向导执行以下操作:

  1. 检查默认设置
  2. 自动生成测试签名证书
  3. 构建应用程序
  4. 创建安装并将所有相关文件复制到Web应用程序
  5. 自动递增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更新测试。

  1. 修改自定义域的applicationhost.config文件
  2. 正在更新Hosts文件
  3. 在管理员模式下运行VS(访问主机文件)

使用自定义域配置IIS Express

对于VS2015VS2017applicationhost.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>

对于VS2010VS2013,过程略有不同。

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

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

  1. 转到devenv.exe文件所在的Visual Studio IDE文件夹。对于VS2017,它默认位于C:\Program Filesx86\Microsoft Visual Studio\2017\[version]\Common7\IDE
  2. 按住 Shift 键并右键单击devenv.exe文件
  3. 单击以管理员身份运行
  4. 打开解决方案,将Web服务器设置为“设置为启动项目"
  5. 运行网络服务器

Visual Studio和本地自定义域托管

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

配置发布下载

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

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

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

安装和测试静默更新

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

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

使用Microsoft EdgeInternet Explorer进行ClickOnce安装

以下是从安装页面到运行的步骤。

下载Setup.exe安装程序

安装

现在,应用程序已准备好运行。第一次运行时,由于我们使用测试证书,我们将看到以下屏幕:

使用Google ChromeMozilla Firefox进行ClickOnce安装

使用ChromeFirefox下载和安装应该与EdgeInternet Explorer相同。但是,在使用ChromeFirefox安装页面下载安装程序文件并运行安装程序后,您可能会遇到以下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部署疑难解答页面没有任何合适的解决方案。

事实证明,ChromeFirefox都会检查可选的属性>发布>更新>更新位置(如果与发布位置不同)部分,并将其与发布>安装文件夹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原文留下的空白;并避免我和其他人随着时间的推移遇到的常见错误。

引用列表和其他相关链接

本文包含大量在一段时间内研究和收集的碎片化信息。以下是使这成为可能的各种人员和资源的链接:

https://www.codeproject.com/Articles/1208414/Silent-ClickOnce-Installer-for-WPF-Winforms-in-Csh

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值