• Wix 安装部署教程(九) --用WPF做安装界面


          经常安装PC端的应用,特别是重装系统之后,大致分为两类。一类像QQ,搜狗输入法这样的。分三步走的:第一个页面可以自定义安装路径和软件许可。第二个页面显示安装进度条,第三个页面推荐其他应用。先不管人家怎么实现的,我们先回顾一下。

         QQ:

               

          再一个就是分六步或七步走的,如QQ影音:欢迎界面,用户许可,安装组件,安装目录,安装进度,安装完成,有七步的,一般会多一些软件推荐。当然还有其他的,比如是基于ClickOnce打包的,就一个界面,一个进度条。没有安装目录选择,这一般不是商业软件。先说第二种,上一步下一步的有很多第三方的打包工具,比较有名的有InstallShield,setupfactory等,看似有了图形化的操作界面,做简单的还可以,但是要是有一些自定义的部分就比较麻烦,因为你没地方改,比如你要在一个安装包中去静默的触发另一个安装包悄悄的安装,而且这些还是商业化的。开源的WIX,基于XML文件配置的方法来打包应用。Wix的基本打包(6,7个步骤),可以出门左拐可以看我之前的教程。接下来要说的,是基于WIX的Bootstrapper工程用WPF接入安装界面。

        WIX是基于BootstrapperCore.dll提供UI扩展的,你可以使用WPF,也可以使用Winform作为安装界面。而这里先不得不先说其中的两个对象。一个是Engine,它提供了最根本的安装方法,如Detect,Plan,Apply和Quit等,另外一个就是引导对象BootstrapperApplication,他提供了安装相关事件,设置窗体对象等。这为我们自定义界面的时候控制安装包提供了基础。

        一、工程准备

       1)新建一个Bootstrapper工程。

        

       新建完成之后会有一个Bundle.wxs文件,里面包含你要安装的msi文件和依赖组件比如.net4.0.  

    <?xml version="1.0" encoding="UTF-8"?>
    <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension"  xmlns:netfx="http://schemas.microsoft.com/wix/NetFxExtension"  xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
      <Bundle Name="WIXTest" Version="1.1.1.0" Manufacturer="Delta" UpgradeCode="{51C1EB78-C0D2-4C30-803C-8D7993CB38A5}"    Compressed="yes"    >
    
        <BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense"   /> 
    
         
        <WixVariable Id="WixMbaPrereqLicenseUrl" Value=""/>
        <WixVariable Id="WixMbaPrereqPackageId" Value=""/>
       
    
        <Chain DisableRollback='yes'>
          <PackageGroupRef Id="Netfx4Full"  />
              <MsiPackage Id="DIAView" SourceFile="D:TestWixinDebugzh-cnTestWix.msi" Compressed="yes"  DisplayInternalUI="yes" >
                 </MsiPackage>
        </Chain>
      </Bundle>
    
      <Fragment>
        <util:RegistrySearchRef Id="NETFRAMEWORK40"/>
        <PackageGroup Id="Netfx4Full">
          <ExePackage
              Id="Netfx4FullExe"
              Cache="no"
              Compressed="yes"
              PerMachine="yes"
              Permanent="yes"
              Vital="yes"
              SourceFile="$(var.Dia)dotNetFx40_Full_x86_x64.exe"
              InstallCommand="/q /norestart "
              DetectCondition="NETFRAMEWORK40"
              DownloadUrl="http://go.microsoft.com/fwlink/?LinkId=164193"/>
        </PackageGroup>
      </Fragment>
    </Wix>
    View Code

      最终的EXE还是通过这个工程来生成的。MsiPackage的ID 后面可以用来检测当前电脑是否安装了这个软件。

    二、创建WPF界面程序。

      1).创建一个C# Library工程CustomBA,引用BootstrapperCore.dll 及 WPF相关dll

      BootstrapperCore在你安装的wix安装目录中就能找到。比如我使用的wix3.8,路径就是C:Program Files (x86)WiX Toolset v3.8SDKBootstrapperCore.dll

      

      另外一个就是Prism 组件,这个可以通过Nuget安装。

     

    但这个有点嫌多,其实工程只使用到了属性更改通知和命令。可以使用wix源码中的两个对象。如此就不必引用上面的Prism

    RelayCommand

     public class RelayCommand : ICommand
        {
            private readonly Action<object> execute;
            private readonly Predicate<object> canExecute;
    
            public RelayCommand(Action<object> execute)
                : this(execute, null)
            {
            }
    
            public RelayCommand(Action<object> execute, Predicate<object> canExecute)
            {
                this.execute = execute;
                this.canExecute = canExecute;
            }
    
            public event EventHandler CanExecuteChanged
            {
                add { CommandManager.RequerySuggested += value; }
                remove { CommandManager.RequerySuggested -= value; }
            }
    
            [DebuggerStepThrough]
            public bool CanExecute(object parameter)
            {
                return this.canExecute == null ? true : this.canExecute(parameter);
            }
    
            public void Execute(object parameter)
            {
                this.execute(parameter);
            }
        }
    View Code

    PropertyNotifyBase

      public abstract class PropertyNotifyBase : INotifyPropertyChanged
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="PropertyNotifyBase"/> class.
            /// </summary>
            protected PropertyNotifyBase()
            {
            }
    
            /// <summary>
            /// Raised when a property on this object has a new value.
            /// </summary>
            public event PropertyChangedEventHandler PropertyChanged;
    
            /// <summary>
            /// Warns the developer if this object does not have a public property with the
            /// specified name. This method does not exist in a Release build.
            /// </summary>
            /// <param name="propertyName">Property name to verify.</param>
            [Conditional("DEBUG")]
            [DebuggerStepThrough]
            public void VerifyPropertyName(string propertyName)
            {
                // Verify that the property name matches a real, public, instance property
                // on this object.
                if (null == TypeDescriptor.GetProperties(this)[propertyName])
                {
                    Debug.Fail(String.Concat("Invalid property name: ", propertyName));
                }
            }
    
            /// <summary>
            /// Raises this object's PropertyChanged event.
            /// </summary>
            /// <param name="propertyName">The property that has a new value.</param>
            protected virtual void OnPropertyChanged(string propertyName)
            {
                this.VerifyPropertyName(propertyName);
    
                PropertyChangedEventHandler handler = this.PropertyChanged;
                if (null != handler)
                {
                    PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
                    handler(this, e);
                }
            }
        }
    View Code

    使用方法差别不大。 

     2)可以建立如下的文件目录,配合WPF的MVVM。

     

    3)增加配置文件,以让Burn使用新的程序集。必须命名为BootstrapperCore.config ,也可以从C:Program Files (x86)WiX Toolset v3.8SDK 目录下拷贝一个版本。

    <?xml version="1.0" encoding="utf-8" ?>
    <!--
      <copyright file="BootstrapperCore.config" company="Outercurve Foundation">
        Copyright (c) 2004, Outercurve Foundation.
        This software is released under Microsoft Reciprocal License (MS-RL).
        The license and further copyright text can be found in the file
        LICENSE.TXT at the root directory of the distribution.
      </copyright>
    -->
    <configuration>
        <configSections>
            <sectionGroup name="wix.bootstrapper" type="Microsoft.Tools.WindowsInstallerXml.Bootstrapper.BootstrapperSectionGroup, BootstrapperCore">
                <section name="host" type="Microsoft.Tools.WindowsInstallerXml.Bootstrapper.HostSection, BootstrapperCore" />
            </sectionGroup>
        </configSections>
        <startup useLegacyV2RuntimeActivationPolicy="true">
            <supportedRuntime version="v4.0" />
            <supportedRuntime version="v2.0.50727" />
        </startup>
        <wix.bootstrapper>
            <!-- Example only. Use only if the startup/supportedRuntime above cannot discern supported frameworks. -->
            <!--
            <supportedFramework version="v4Client" />
            <supportedFramework version="v3.5" />
            <supportedFramework version="v3.0" />
            -->
            <!-- Example only. Replace the host/@assemblyName attribute with assembly that implements BootstrapperApplication. --> 
            <host assemblyName="AssemblyWithClassThatInheritsFromBootstrapperApplication" />
        </wix.bootstrapper>
    </configuration>
    View Code

     修改为:

    <?xml version="1.0" encoding="utf-8" ?>
     <configuration>
        <configSections>
            <sectionGroup name="wix.bootstrapper" type="Microsoft.Tools.WindowsInstallerXml.Bootstrapper.BootstrapperSectionGroup, BootstrapperCore">
                <section name="host" type="Microsoft.Tools.WindowsInstallerXml.Bootstrapper.HostSection, BootstrapperCore" />
            </sectionGroup>
        </configSections>
        <startup useLegacyV2RuntimeActivationPolicy="true">
            <supportedRuntime version="v4.0" />
        </startup>
        <wix.bootstrapper>
          <host assemblyName="CustomBA">
            <supportedFramework version="v4Full" />
            <supportedFramework version="v4Client" />
          </host>
        </wix.bootstrapper>
    </configuration>

    CustomBA.dll,BootstrapperCore.config 和 Microsoft.Practices. Prism.dll(如果使用了) 都会拷贝到bootstrapper项目中去。

     4)修改Assemblyinfo

     增加:

    [assembly: BootstrapperApplication(typeof(CustomBootstrapperApplication))]
    [assembly: AssemblyTitle("CustomBA")]
    CustomBootstrapperApplication 是我们接下来要创建的一个对象,Bundle会调用它的Run方法,这是整个安装包的起点。

     5)创建WPF对象。

        根据MVVM模式,我们分别创建Model,ViewModel和View 以及一些辅助的对象。

        1.Model-->BootstrapperApplicationModel

      public class BootstrapperApplicationModel
        {
            private IntPtr hwnd;
            public BootstrapperApplicationModel( BootstrapperApplication bootstrapperApplication)
            {
                BootstrapperApplication = bootstrapperApplication;
               
                hwnd = IntPtr.Zero;
            }
    
            public BootstrapperApplication BootstrapperApplication
            {
                get;
                private set;
            }
            
            public int FinalResult { get; set; }
            
            public void SetWindowHandle(Window view)
            {
                hwnd = new WindowInteropHelper(view).Handle;
            }
            
            public void PlanAction(LaunchAction action)
            {
                BootstrapperApplication.Engine.Plan(action);
            }
            
            public void ApplyAction()
            {
                BootstrapperApplication.Engine.Apply(hwnd);
            }
            
            public void LogMessage(string message)
            {
                BootstrapperApplication.Engine.Log(LogLevel.Standard, message);
            }
    
            public void SetBurnVariable(string variableName, string value)
            {
                BootstrapperApplication.Engine.StringVariables[variableName] = value;
            }
        }

     从代码看是包装了Engine的方法,设置窗口,PlanAction,ApplyAction,LogMessage以及设置变量等。

    PlanAction是做一个任务准备,例如安装,卸载,修复或者更改。而ApplyAction就是执行这个任务。当UI上的按钮点击时,我们调用PlanAction方法,传递我们要执行的任务。Plan完成之后,再会触发ApplyAction。最后一点就是FinalResult 属性。我们用来存储引导程序结束后Burn引擎返回的状态码 。

      2.ViewModel-->InstallViewModel

     这是一个重要的交互对象。InstallState 安装状态,UpdateState 更新状态,Command用来处理用户在安装界面的操作,在构造函数里进行初始化。然后就是事件订阅。

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Input;
    using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
    
    namespace CustomBA.ViewModels
    {
        public class InstallViewModel : PropertyNotifyBase
        {
            public enum InstallState
            {
                Initializing,
                Present,
                NotPresent,
                Applying,
                Cancelled,
                Applied,
                Failed,
            }
    
            public enum UpdateState
            {
                Unknown,
                Initializing,
                Checking,
                Current,
                Available,
                Failed,
            }
    
            /// <summary>
            /// 记录状态
            /// </summary>
            private InstallState state;
            public UpdateState updatestate;
    
            /// <summary>
            /// 需要显示在WPFWindow
            /// </summary>
            private string message;
            private BootstrapperApplicationModel model;
            private string _packageId = string.Empty;
            private bool canceled;
            private Dictionary<string, int> executingPackageOrderIndex;
            private string username;
            private int progress;
            private int cacheProgress;
            private int executeProgress;
            private Version _version = new Version("2.0.0.0");
            private bool _installEnabled;
            private int progressPhases=1;
            private bool isUnstalling=false;
            #region Command
            /// <summary>
            /// 执行安装命令
            /// </summary>
            public ICommand InstallCommand { get; private set; }
            public ICommand UninstallCommand { get; private set; }
            public ICommand CancelCommand { get; private set; }
            public ICommand LaunchNewsCommand { get; private set; }
            private ICommand repairCommand;
    
            public ICommand RepairCommand
            {
                get
                {
                    return this.repairCommand ?? (this.repairCommand = new RelayCommand(param =>
                        model.PlanAction(LaunchAction.Repair) 
                     , param => State == InstallState.Present));
                }
            }
    
            #endregion 
    
            #region 属性
            public string Message
            {
                get
                {
                    return message;
                }
                set
                {
                    if (message != value)
                    {
                        message = value;
                        OnPropertyChanged("Message");
                    }
                }
            }
    
            public InstallState State
            {
                get
                {
                    return state;
                }
                set
                {
                    if (state != value)
                    {
                        state = value;
                        Message = "Status: " + state;
                        OnPropertyChanged("State");
                        Refresh();
                    }
                }
            }
    
            public string PackageId
            {
                get { return _packageId; }
                set
                {
                    if (_packageId != value)
                    {
                        _packageId = "packid:" + value;
                        OnPropertyChanged("PackageId");
                    }
                }
            }
            public bool Canceled
            {
                get
                {
                    return this.canceled;
                }
    
                set
                {
                    if (this.canceled != value)
                    {
                        this.canceled = value;
                        OnPropertyChanged("Canceled");
                    }
                }
            }
            public Version Version
            {
                get
                {
                    return _version;
                }
            }
    
            public string Username
            {
                get
                {
                    return this.username;
                }
                set
                {
                    this.username = value;
                    this.model.SetBurnVariable("Username", this.username);
                }
            }
    
         
    
            public int Progress
            {
                get
                {
                    if (isUnstalling)
                    {
                        return progress*2;
                    }
                    return this.progress;
                }
                set
                {
                    this.progress = value;
                    OnPropertyChanged("Progress");
                    OnPropertyChanged("Persent");
                }
            }
    
            private string _info;
    
            public string Info
            {
                get
                {
                    if(string.IsNullOrEmpty(_info))
                    _info= InstallEnabled ? "安装中..." : "进行中...";
                    return _info;
                }
                set
                {
                    _info = value;
                    OnPropertyChanged("Info");
                }
            }
    
            public string Persent
            {
                get { return Progress + "%"; }
            }
    
            public bool InstallEnabled
            {
                get { return State == InstallState.NotPresent; }
            }
    
            public bool UninstallEnabled
            {
                get { return UninstallCommand.CanExecute(this); }
            }
    
            public bool CancelEnabled
            {
                get { return  State == InstallState.Applying; }
            }
    
            public bool ExitEnabled
            {
                get { return this.State != InstallState.Applying; }
            }
    
            public bool ProgressEnabled
            {
                get { return this.State == InstallState.Applying; }
            }
    
            /// <summary>
            /// 先不管
            /// </summary>
            public bool IsUpToDate
            {
                get { return true; }
            }
            public bool RepairEnabled
            {
                get { return this.RepairCommand.CanExecute(this); }
            }
    
            public bool CompleteEnabled
            {
                get { return State == InstallState.Applied; }
            }
    
            public int  Phases
            {
                get
                {
                    return progressPhases;
                }
            }
    
            private string _installText = "Uninstall";
            public string InstallText
            {
                get
                {
                    return _installText;
                }
                set
                {
                    _installText = value;
                    OnPropertyChanged("InstallText");
                }
            }
    
            public string RepairText
            {
                get { return _repairText; }
                set { _repairText = value; }
            }
    
            private bool _lableback=true;
            private string _repairText = "Repair";
    
            public bool LabelBack
            {
                get
                {
                    return _lableback;
                }
                set
                {
                    _lableback = value;
                    OnPropertyChanged("LabelBack");
                }
            }
    
            #endregion 
    
            #region 构造函数
            /// <summary>
            /// 构造函数
            /// </summary>
            /// <param name="_model"></param>
            public InstallViewModel(BootstrapperApplicationModel _model)
            {
                model = _model;
                executingPackageOrderIndex = new Dictionary<string, int>();
                
                State = InstallState.Initializing;
                //处理由bootstrapper触发的事件
                WireUpEventHandlers();
                //初始化命令 第一个参数是命令要触发的方法,第二个匿名函数是命令执行的条件
                InstallCommand = new RelayCommand(param => model.PlanAction(LaunchAction.Install), param => State == InstallState.NotPresent);
    
                UninstallCommand = new RelayCommand(param =>
                {
                    model.PlanAction(LaunchAction.Uninstall);
                    isUnstalling = true;
                }, param => State == InstallState.Present);
    
                CancelCommand = new RelayCommand(param =>
                {
                    model.LogMessage("Cancelling...");
                    if (State == InstallState.Applying)
                    {
                        State = InstallState.Cancelled;
                    }
                    else
                    {
                        CustomBootstrapperApplication.Dispatcher.InvokeShutdown();
                    }
                }, param => State != InstallState.Cancelled);
    
    
                model.BootstrapperApplication.DetectComplete += DetectComplete;
    
    
    
    
                //进度条相关事件绑定
                //this.model.BootstrapperApplication.CacheAcquireProgress +=
                //(sender, args) =>
                //{
                //    this.cacheProgress = args.OverallPercentage;
                //    this.Progress = (this.cacheProgress + this.executeProgress) / 2;
                //};
                //this.model.BootstrapperApplication.ExecuteProgress +=
                //(sender, args) =>
                //{
                //    this.executeProgress = args.OverallPercentage;
                //    this.Progress = (this.cacheProgress + this.executeProgress) / 2;
                //};
                model.BootstrapperApplication.CacheAcquireProgress += CacheAcquireProgress;
                model.BootstrapperApplication.ExecuteProgress += ApplyExecuteProgress;
                model.BootstrapperApplication.ExecuteMsiMessage += ExecuteMsiMessage;
                model.BootstrapperApplication.PlanBegin += PlanBegin;
                model.BootstrapperApplication.PlanPackageComplete += PlanPackageComplete;
                model.BootstrapperApplication.Progress += ApplyProgress;
                model.BootstrapperApplication.CacheComplete += CacheComplete;
            }
    
            #endregion
    
            private void DetectComplete(object sender, DetectCompleteEventArgs e)
            {
                if (LaunchAction.Uninstall == CustomBootstrapperApplication.Model.Command.Action)
                {
                    CustomBootstrapperApplication.Model.Engine.Log(LogLevel.Verbose, "Invoking automatic plan for uninstall");
                    CustomBootstrapperApplication.Plan(LaunchAction.Uninstall);
                }
                else if (Hresult.Succeeded(e.Status))
                {
                    if (CustomBootstrapperApplication.Model.Engine.EvaluateCondition("NETFRAMEWORK35_SP_LEVEL < 1"))
                    {
                        string message = "WiX Toolset requires the .NET Framework 3.5.1 Windows feature to be enabled.";
                        CustomBootstrapperApplication.Model.Engine.Log(LogLevel.Verbose, message);
    
                        if (Display.Full == CustomBootstrapperApplication.Model.Command.Display)
                        {
                            CustomBootstrapperApplication.Dispatcher.Invoke((Action)delegate()
                            {
                                MessageBox.Show(message, "DIAView", MessageBoxButton.OK, MessageBoxImage.Error);
                                if (null != CustomBootstrapperApplication.View)
                                {
                                    CustomBootstrapperApplication.View.Close();
                                }
                            }
                            );
                        }
    
                        State = InstallState.Failed;
                        return;
                    }
                }
                else
                {
                    State = InstallState.Failed;
                }
            }
    
            #region 方法
            private void CacheAcquireProgress(object sender, CacheAcquireProgressEventArgs e)
            {
                lock (this)
                {
                    this.cacheProgress = e.OverallPercentage;
                    this.Progress = (this.cacheProgress + this.executeProgress) / this.Phases;
                    e.Result = Canceled ? Result.Cancel : Result.Ok;
                }
            }
            private void ApplyExecuteProgress(object sender, ExecuteProgressEventArgs e)
            {
                lock (this)
                {
    
                    this.executeProgress = e.OverallPercentage;
                    this.Progress = (this.cacheProgress + this.executeProgress) / 2; // always two phases if we hit execution.
    
                    if (CustomBootstrapperApplication.Model.Command.Display == Display.Embedded)
                    {
                        CustomBootstrapperApplication.Model.Engine.SendEmbeddedProgress(e.ProgressPercentage, this.Progress);
                    }
    
                    e.Result =  Canceled ? Result.Cancel : Result.Ok;
                }
            }
    
            /// <summary>
            /// 这个方法 会在Detect中被调用
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            protected void DetectPackageComplete(object sender, DetectPackageCompleteEventArgs e)
            {
                PackageId = e.PackageId;
                //对应的是MsiPackage Id="DIAView"
                if (e.PackageId.Equals("DIAView", StringComparison.Ordinal))
                {
                    State = e.State == PackageState.Present ? InstallState.Present : InstallState.NotPresent;
                }
            }
    
    
            private void PlanBegin(object sender, PlanBeginEventArgs e)
            {
                lock (this)
                {
                    if (InstallEnabled)
                    {
                        this.progressPhases = (LaunchAction.Layout == CustomBootstrapperApplication.Model.PlannedAction) ? 1 : 2;
                    }
                    else
                    {
                        LabelBack = false;
                    }
                    InstallText = "";
                    RepairText = "";
                    OnPropertyChanged("Phases");
                    OnPropertyChanged("InstallEnabled");
                    OnPropertyChanged("InstallText");
                    OnPropertyChanged("RepairText");
                    this.executingPackageOrderIndex.Clear();
                }
            }
            private void PlanPackageComplete(object sender, PlanPackageCompleteEventArgs e)
            {
                if (ActionState.None != e.Execute)
                {
                    lock (this)
                    {
                        Debug.Assert(!this.executingPackageOrderIndex.ContainsKey(e.PackageId));
                        this.executingPackageOrderIndex.Add(e.PackageId, this.executingPackageOrderIndex.Count);
                    }
                }
            }
    
            /// <summary>
            /// PlanAction 结束后会触发这个方法
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            protected void PlanComplete(object sender, PlanCompleteEventArgs e)
            {
                if (State == InstallState.Cancelled)
                {
                    CustomBootstrapperApplication.Dispatcher.InvokeShutdown();
                    return;
                }
                State = InstallState.Applying;
                model.ApplyAction();
            }
            /// <summary>
            /// ApplyAction 开始 
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            protected void ApplyBegin(object sender, ApplyBeginEventArgs e)
            {
                State = InstallState.Applying;
                OnPropertyChanged("ProgressEnabled");
                OnPropertyChanged("CancelEnabled");
            }
            /// <summary>
            /// 安装
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            protected void ExecutePackageBegin(object sender, ExecutePackageBeginEventArgs e)
            {
                if (State == InstallState.Cancelled)
                {
                    e.Result = Result.Cancel;
                }
            }
    
            private void ExecuteMsiMessage(object sender, ExecuteMsiMessageEventArgs e)
            {
                lock (this)
                {
                    if (e.MessageType == InstallMessage.ActionStart)
                    {
                        this.Message = e.Message;
                    }
    
                    e.Result = Canceled ? Result.Cancel : Result.Ok;
                }
            }
            private void CacheComplete(object sender, CacheCompleteEventArgs e)
            {
                lock (this)
                {
                    this.cacheProgress = 100;
                    this.Progress = (this.cacheProgress + this.executeProgress) / this.progressPhases;
                }
            }
            /// <summary>
            /// 卸载
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            protected void ExecutePackageComplete(object sender, ExecutePackageCompleteEventArgs e)
            {
                if (State == InstallState.Cancelled)
                {
                    e.Result = Result.Cancel;
                }
            }
            /// <summary>
            /// Apply结束
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            protected void ApplyComplete(object sender, ApplyCompleteEventArgs e)
            {
                model.FinalResult = e.Status;
                State = InstallState.Applied;
                isUnstalling = false;
                OnPropertyChanged("CompleteEnabled");
                OnPropertyChanged("ProgressEnabled");
               // CustomBootstrapperApplication.Dispatcher.InvokeShutdown();
            }
    
            private void ApplyProgress(object sender, ProgressEventArgs e)
            {
                lock (this)
                {
                    e.Result =  Canceled ? Result.Cancel : Result.Ok;
                }
            }
            /// <summary>
            /// 刷新命令状态 从而改变UI是否使能
            /// </summary>
            private void Refresh()
            {
                CustomBootstrapperApplication.Dispatcher.Invoke(
                (Action)(() =>
                {
                    //((RelayCommand)InstallCommand).CanExecute(this);
                    //.RaiseCanExecuteChanged();
                    //((RelayCommand)UninstallCommand)
                    //.RaiseCanExecuteChanged();
                    //((DelegateCommand)CancelCommand)
                    //.RaiseCanExecuteChanged();
                }));
            }
            /// <summary>
            /// 事件订阅
            /// </summary>
            private void WireUpEventHandlers()
            {
                model.BootstrapperApplication.DetectPackageComplete += DetectPackageComplete;
    
                model.BootstrapperApplication.PlanComplete += PlanComplete;
    
                model.BootstrapperApplication.ApplyComplete += ApplyComplete;
    
                model.BootstrapperApplication.ApplyBegin += ApplyBegin;
    
                model.BootstrapperApplication.ExecutePackageBegin += ExecutePackageBegin;
    
                model.BootstrapperApplication.ExecutePackageComplete += ExecutePackageComplete;
    
            }
            #endregion
    
    
        }
    }
    View Code

    在构造函数内部,WireUpEventHandlers方法里面的事件订阅处理的是安装的状态。其他就是处理安装进度。安装进度是由两部分组成的,CacheAcquireProgress 和ExecuteProgress,基于二者完成的进度平均得到当前的安装进度。Wix3.6书中的代码如下:

         this.model.BootstrapperApplication.CacheAcquireProgress +=
                (sender, args) =>
                {
                    this.cacheProgress = args.OverallPercentage;
                    this.Progress = (this.cacheProgress + this.executeProgress) / 2;
                };
                this.model.BootstrapperApplication.ExecuteProgress +=
                (sender, args) =>
                {
                    this.executeProgress = args.OverallPercentage;
                    this.Progress = (this.cacheProgress + this.executeProgress) / 2;
                };

    我按照wix3.8中WixBA的源码调整了下。卸载的时候常数是1而不是2.

     3.在Views中新建一个InstallView.Xaml 。

     我们先做一个简单的。只有几个按钮。最后的界面如下(美观度先不考虑,;-)): 只有安装,卸载和取消三个按钮,再加一个进度条和一个消息提示。

    XAML:

      <Grid>
              <StackPanel>
                <Label Content="{Binding Message}" />
                <Button Command="{Binding InstallCommand}">Install</Button>
                <Button Command="{Binding UninstallCommand}">Uninstall</Button>
                <Button Command="{Binding CancelCommand}">Cancel</Button>
                <Label VerticalAlignment="Center">Progress:</Label>
                <Label Content="{Binding Progress}" />
                <ProgressBar Width="200" Height="30" Value="{Binding Progress}" Minimum="0" Maximum="100" />
            </StackPanel>
        </Grid>

    CS:

     public InstallView(InstallViewModel viewModel)
            {
                this.InitializeComponent();
                this.DataContext = viewModel;
                this.Closed += (sender, e) =>
                viewModel.CancelCommand.Execute(this);
            }

    4.入口对象CustomBootstrapperApplication。

      public class CustomBootstrapperApplication:BootstrapperApplication
        {
            public static Dispatcher Dispatcher { get; set; }
            static public DiaViewModel Model { get; private set; }
            static public InstallView View { get; private set; }
         
            protected override void Run()
            {
                Model = new DiaViewModel(this);
                Dispatcher = Dispatcher.CurrentDispatcher;
                var model = new BootstrapperApplicationModel(this);
                var viewModel = new InstallViewModel(model);
                View = new InstallView(viewModel);
                model.SetWindowHandle(View);
                this.Engine.Detect();
                View.Show();
                Dispatcher.Run();
                this.Engine.Quit(model.FinalResult);
            }
        }
    重载了Run方法,是UI的主要入口函数,它会被Burn引擎调用,Dispatcher 用于UI和后台线程直接通信。它提供了Invoke方法,我们可以用来更新UI控件的状态。而这里的Model相当于一个辅助对象。
      public class DiaViewModel
        {
            private Version _version;
            private const string BurnBundleInstallDirectoryVariable = "InstallFolder";
            private const string BurnBundleLayoutDirectoryVariable = "WixBundleLayoutDirectory";
    
            public DiaViewModel(BootstrapperApplication bootstrapper)
            {
                Bootstrapper = bootstrapper;
                Telemetry = new List<KeyValuePair<string, string>>();
            }
    
            /// <summary>
            /// Gets the bootstrapper.
            /// </summary>
            public BootstrapperApplication Bootstrapper { get; private set; }
    
            /// <summary>
            /// Gets the bootstrapper command-line.
            /// </summary>
            public Command Command { get { return Bootstrapper.Command; } }
    
            /// <summary>
            /// Gets the bootstrapper engine.
            /// </summary>
            public Engine Engine { get { return Bootstrapper.Engine; } }
    
            /// <summary>
            /// Gets the key/value pairs used in telemetry.
            /// </summary>
            public List<KeyValuePair<string, string>> Telemetry { get; private set; }
    
            /// <summary>
            /// Get or set the final result of the installation.
            /// </summary>
            public int Result { get; set; }
    
            /// <summary>
            /// Get the version of the install.
            /// </summary>
            public Version Version
            {
                get
                {
                    if (null == _version)
                    {
                        Assembly assembly = Assembly.GetExecutingAssembly();
                        FileVersionInfo fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
    
                        _version = new Version(fileVersion.FileVersion);
                    }
    
                    return _version;
                }
            }
    
            /// <summary>
            /// Get or set the path where the bundle is installed.
            /// </summary>
            public string InstallDirectory
            {
                get
                {
                    if (!Engine.StringVariables.Contains(BurnBundleInstallDirectoryVariable))
                    {
                        return null;
                    }
    
                    return Engine.StringVariables[BurnBundleInstallDirectoryVariable];
                }
    
                set
                {
                    Engine.StringVariables[BurnBundleInstallDirectoryVariable] = value;
                }
            }
    
            /// <summary>
            /// Get or set the path for the layout to be created.
            /// </summary>
            public string LayoutDirectory
            {
                get
                {
                    if (!Engine.StringVariables.Contains(BurnBundleLayoutDirectoryVariable))
                    {
                        return null;
                    }
    
                    return Engine.StringVariables[BurnBundleLayoutDirectoryVariable];
                }
    
                set
                {
                    Engine.StringVariables[BurnBundleLayoutDirectoryVariable] = value;
                }
            }
    
            public LaunchAction PlannedAction { get; set; }
    
            /// <summary>
            /// Creates a correctly configured HTTP web request.
            /// </summary>
            /// <param name="uri">URI to connect to.</param>
            /// <returns>Correctly configured HTTP web request.</returns>
            public HttpWebRequest CreateWebRequest(string uri)
            {
                var request = (HttpWebRequest)WebRequest.Create(uri);
                request.UserAgent = String.Concat("WixInstall", Version.ToString());
                return request;
            }
        }
    View Code
     
    model.SetWindowHandle(view);  
    这个方法会控制wpf窗口,在Burn 引擎安装或卸载的时候会用到。
    this.Engine.Detect();
    它会让Burn去检查是否bundle已经安装,这样当窗口显示出来的时候,我们需要知道是显示一个安装按钮还是一个卸载按钮。show方法会显示WPF窗口,接着我们调用了
    Dispatcher.Run()
    它会一直执行到Dispatcher关闭或遇到任务取消,关闭窗口我们可以调用Dispatcher的InvokeShutdown方法。
    Engine.Quit
    会优雅的退出bootstrapper 进程。而不是直接kill。
     
    5.更改配置
      以上wpf这边就是已经准备好,现在就需要告诉Bootstrapper工程。
     首先我们需要引用CustomBA这个工程,以及相关的dll
       
     然后修改Bundle中的BootstrapperApplicationRef,使用payload相当于是把CustomBA下面的DLL都copy过来。但如果没有使用prism,就可以将其全部拿掉。
     <BootstrapperApplicationRef Id="ManagedBootstrapperApplicationHost">
          <Payload SourceFile="$(var.CustomBA.TargetDir)CustomBA.dll" />
          <Payload SourceFile="$(var.CustomBA.TargetDir)BootstrapperCore.config" />
          <Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.Composition.dll" />
          <Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.Interactivity.dll" />
          <Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.Mvvm.Desktop.dll" />
          <Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.Mvvm.dll" />
          <Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.PubSubEvents.dll" />
          <Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.SharedInterfaces.dll" />
          <Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.ServiceLocation.dll" />
        </BootstrapperApplicationRef>

    然后再编译运行,就ok了。 运行一下安装包,wpf界面出来,有点小激动,比起xml配置的界面还是要好多了,主要是给了各种可能。

    三、界面优化

     当然,上面的界面还不能满足我们的胃口,我就先把WIXBA的界面抓过来了。

    修改了下安装界面。明白了安装步骤和事件控制,怎么折腾界面那都是你自己的事情了。这个界面也不是很好,进度条不显示的时候纠结显示些别的什么。

      安装前:                                                                             卸载中:

             

    <Window x:Class="CustomBA.Views.InstallView"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            WindowStartupLocation="CenterScreen"
        WindowStyle="None"
        AllowsTransparency="True"
        Background="{x:Null}"
        Width="400"
        Height="400">
        <Window.Resources>
            <ResourceDictionary Source="Styles.xaml"    />
        </Window.Resources>
    
        <Grid>
            <Rectangle MouseLeftButtonDown="Background_MouseLeftButtonDown" Fill="{StaticResource BackgroundBrush}"/>
            <Grid VerticalAlignment="Stretch" Margin="15">
                <Grid.RowDefinitions>
                    <RowDefinition Height="1*"/>
                    <RowDefinition Height="1*"/>
                    <RowDefinition Height="1*"  />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="1*" />
                </Grid.ColumnDefinitions>
                <Button Grid.Row="0" Grid.ColumnSpan="2" Background="#33CCFF"   />
                <TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource StatusTextStyle}" Padding="12 0  0 0" >stoneniqiu</TextBlock>
                <TextBlock Grid.Row="0"  Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource StatusTextStyle}" HorizontalAlignment="Right"  Text="{Binding Version}"/>
                <TextBlock Grid.ColumnSpan="2"  Grid.Row="0" Grid.Column="0" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="50" Foreground="White" IsHitTestVisible="False">YourSoft</TextBlock>
    
                <Label Grid.Row="2" Grid.Column="0"  Margin="3"  Grid.ColumnSpan="3" Background="#33CCFF"  ></Label>
    
    
                <Grid Grid.Row="2" Grid.Column="0" ColumnSpan="3" >
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions> 
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition></ColumnDefinition>
                        <ColumnDefinition></ColumnDefinition>
                        <ColumnDefinition></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <ProgressBar Grid.Row="0" Margin="4" VerticalAlignment="Bottom"  Grid.Column="0"   Grid.ColumnSpan="3"  Width="350" Height="8" Value="{Binding Progress}" Minimum="0" Maximum="100" 
                                 Visibility="{Binding ProgressEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" />
                    <Label Grid.Row="1"   Grid.Column="0" Background="#33CCFF" Foreground="White"  Margin="3"  Visibility="{Binding ProgressEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"   Content="{Binding Info}"  ></Label>
                    <Button Grid.Row="1" Grid.Column="1" Click="ButtonBase_OnClick" Content="" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" ></Button>
                    <Label  Grid.Row="1" Grid.Column="2"  Background="#33CCFF"  Foreground="White" Margin="3"   Visibility="{Binding ProgressEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"  HorizontalContentAlignment="Right"  Content="{Binding Persent}"   />
                </Grid>
    
    
    
                <!-- Install -->
                <Button Grid.Row="1" Grid.ColumnSpan="3" Tag="安装"  Grid.Column="0"  Command="{Binding InstallCommand}" Visibility="{Binding InstallEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" Content="Install" />
                <Image Grid.Row="1" Grid.Column="2" Source="..
    esourcesgear.png"  Visibility="{Binding InstallEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"/>
    
                <Label Grid.Row="1" Grid.Column="0" Margin="3" Grid.ColumnSpan="3" Background="#33CCFF"   Visibility="{Binding ProgressEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" ></Label>
                
                <!--<TextBlock Grid.Row="2"   Grid.ColumnSpan="3" Style="{StaticResource StatusTextStyle}"  Visibility="{Binding ProgressEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" Text="{Binding Message}"/>-->
                <Image Grid.Row="1" Grid.Column="2" Source="..
    esourcesgear.png"   Visibility="{Binding ProgressEnabled, Converter={StaticResource BooleanToVisibilityConverter}}">
                    <Image.RenderTransform>
                        <RotateTransform x:Name="ProgressRotateTransform" Angle="1"/>
                    </Image.RenderTransform>
                    <Image.Triggers>
                        <EventTrigger RoutedEvent="Image.Loaded">
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="ProgressRotateTransform" Storyboard.TargetProperty="Angle" From="0.0" To="360.0" Duration="0:0:4" RepeatBehavior="Forever"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </Image.Triggers>
                </Image>
                <!-- Uninstall -->
                <Button Grid.Row="1" Grid.Column="0"  Command="{Binding UninstallCommand}" Visibility="{Binding UninstallEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" Content="{Binding InstallText}" />
                <!--<Image Grid.Row="1" Grid.Column="0" Source="..
    esourcesgear.png"  Visibility="{Binding UninstallEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"/>-->
    
                <Image Grid.Row="1" Grid.Column="0" Source="..
    esourcesgear.png"   Visibility="{Binding UninstallEnabled, Converter={StaticResource BooleanToVisibilityConverter}}">
                    <Image.RenderTransform>
                        <RotateTransform x:Name="UpdateRotateTransform" Angle="1"/>
                    </Image.RenderTransform>
                    <Image.Triggers>
                        <EventTrigger RoutedEvent="Image.Loaded">
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="UpdateRotateTransform" Storyboard.TargetProperty="Angle" From="0.0" To="360.0" Duration="0:0:4" RepeatBehavior="Forever"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </Image.Triggers>
                </Image>
                
    
                <!-- Repair -->
                <Button Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="1"  Command="{Binding RepairCommand}" Visibility="{Binding RepairEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"  Content="Repair"  />
                <Image Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Source="..
    esourceswrench.png"   Visibility="{Binding RepairEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"/>
    
                <!-- Complete -->
                <Button Grid.Row="1" Grid.ColumnSpan="3"  Grid.Column="0"  IsEnabled="False" Visibility="{Binding CompleteEnabled, Converter={StaticResource BooleanToVisibilityConverter}}">Complete</Button>
                <Image Grid.Row="1" Grid.Column="2" Source="..
    esourcesgear.png"   Visibility="{Binding CompleteEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"/>
    
    
                <TextBlock Grid.Row="1" Grid.Column="0"  Grid.ColumnSpan="3" Style="{StaticResource StatusTextStyle}"  Visibility="{Binding ProgressEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" Text="{Binding Message}"/>
    
    
                
                <!-- Failed -->
                <!--<Button Grid.Row="1" Grid.ColumnSpan="3" Grid.Column="0" Background="#33CCFF"   Command="{Binding TryAgainCommand}" Visibility="{Binding TryAgainEnabled, Converter={StaticResource BooleanToVisibilityConverter}}">Failed. Try Again?</Button>
                <TextBlock Grid.Row="1" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="60" Foreground="White" IsHitTestVisible="False"    Visibility="{Binding TryAgainEnabled, Converter={StaticResource BooleanToVisibilityConverter}}">X</TextBlock>-->
    
    
    
                <!-- Cancel -->
                <Button Grid.Row="0" Grid.Column="2"  Visibility="{Binding CancelEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" Command="{Binding CancelCommand}">Cancel</Button>
                <TextBlock Grid.Row="0" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="60" Foreground="White" IsHitTestVisible="False"   Visibility="{Binding CancelEnabled, Converter={StaticResource BooleanToVisibilityConverter}}">X</TextBlock>
    
                <!-- Exit -->
                <Button Grid.Row="0" Grid.Column="2"  Visibility="{Binding ExitEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"  Name="Exit" Click="Exit_OnClick"  >Exit</Button>
                <Image Grid.Row="0" Grid.Column="2" Source="..
    esourcesexit.png"  Visibility="{Binding ExitEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" />
    
            </Grid>
    
            <!--<StackPanel>
                <Label Content="{Binding Message}" />
                <Label Content="{Binding PackageId}" />
                <Button Command="{Binding InstallCommand}">Install</Button>
                <Button Command="{Binding UninstallCommand}">Uninstall</Button>
                <Button Command="{Binding CancelCommand}">Cancel</Button>
                <Label VerticalAlignment="Center">Username:</Label>
                <TextBox Text="{Binding Username}" Margin="10"  MinWidth="150" />
                <Label VerticalAlignment="Center">Progress:</Label>
                <Label Content="{Binding Progress}" />
                <ProgressBar Width="200" Height="30" Value="{Binding Progress}" Minimum="0" Maximum="100" />
            </StackPanel>-->
        </Grid>
    
    
    </Window>
    View Code

     小结: 以上就是整个过程,基本走通了,但还没有达到最终目的。要制作想QQ那样三步的安装界面,还有2个关键比较重要的问题没有解决。

     1.使用了wpf界面后,bundle无法再先安装.net . 这意味着要在我们的wpf界面之前安装.net(不然wpf界面跑不起来),用wix3.9编译,会给出提示界面。但安装.net失败。

     2.用户选择路径问题,原本我们是通过msi的安装界面来让用户选择安装路径,但现在msi的安装界面就显得有些多余。但这是两个工程,如何把安装界面传递过去。也是个问题。

     这两个问题解决后,我会在后面的博客中更新。那样才算完整。

      需要的demo的留邮箱,觉得可以的就点个赞~

      参考资料:

     1.书 Wix3.6:A Develop's Guide to Windows Installer XML

     2.源码:wix3.8  

  • 相关阅读:
    第五十六课 树中结点的删除操作
    第五十五课 树中节点的清除操作
    常用的机器学习&数据挖掘知识点总结
    leetcode 211. Add and Search Word
    leetcode 349. Intersection of Two Arrays
    leetcode 659. Split Array into Consecutive Subsequences
    leetcode 658. Find K Closest Elements
    leetcode 657. Judge Route Circle
    leetcode 179. Largest Number
    leetcode 660. Remove 9
  • 原文地址:https://www.cnblogs.com/stoneniqiu/p/4399343.html
Copyright © 2020-2023  润新知