Wix 安裝部署教程(九) --用WPF作安裝界面

      常常安裝PC端的應用,特別是重裝系統以後,大體分爲兩類。一類像QQ,搜狗輸入法這樣的。分三步走的:第一個頁面能夠自定義安裝路徑和軟件許可。第二個頁面顯示安裝進度條,第三個頁面推薦其餘應用。先無論人家怎麼實現的,咱們先回顧一下。web

     QQ:bootstrap

           

      再一個就是分六步或七步走的,如QQ影音:歡迎界面,用戶許可,安裝組件,安裝目錄,安裝進度,安裝完成,有七步的,通常會多一些軟件推薦。固然還有其餘的,好比是基於ClickOnce打包的,就一個界面,一個進度條。沒有安裝目錄選擇,這通常不是商業軟件。先說第二種,上一步下一步的有不少第三方的打包工具,比較有名的有InstallShield,setupfactory等,看似有了圖形化的操做界面,作簡單的還能夠,可是要是有一些自定義的部分就比較麻煩,由於你沒地方改,好比你要在一個安裝包中去靜默的觸發另外一個安裝包悄悄的安裝,並且這些仍是商業化的。開源的WIX,基於XML文件配置的方法來打包應用。Wix的基本打包(6,7個步驟),能夠出門左拐能夠看我以前的教程。接下來要說的,是基於WIX的Bootstrapper工程用WPF接入安裝界面。app

    WIX是基於BootstrapperCore.dll提供UI擴展的,你可使用WPF,也可使用Winform做爲安裝界面。而這裏先不得不先說其中的兩個對象。一個是Engine,它提供了最根本的安裝方法,如Detect,Plan,Apply和Quit等,另一個就是引導對象BootstrapperApplication,他提供了安裝相關事件,設置窗體對象等。這爲咱們自定義界面的時候控制安裝包提供了基礎。ide

    1、工程準備函數

   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:\TestWix\bin\Debug\zh-cn\TestWix.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 後面能夠用來檢測當前電腦是否安裝了這個軟件。ui

2、建立WPF界面程序。this

  1).建立一個C# Library工程CustomBA,引用BootstrapperCore.dll 及 WPF相關dlllua

  BootstrapperCore在你安裝的wix安裝目錄中就能找到。好比我使用的wix3.8,路徑就是C:\Program Files (x86)\WiX Toolset v3.8\SDK\BootstrapperCore.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.8\SDK 目錄下拷貝一個版本。

<?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="v4\Client" />
        <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="v4\Full" />
        <supportedFramework version="v4\Client" />
      </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配置的界面仍是要好多了,主要是給了各類可能。

3、界面優化

 固然,上面的界面還不能知足咱們的胃口,我就先把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="..\resources\gear.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="..\resources\gear.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="..\resources\gear.png"  Visibility="{Binding UninstallEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"/>-->

            <Image Grid.Row="1" Grid.Column="0" Source="..\resources\gear.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="..\resources\wrench.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="..\resources\gear.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="..\resources\exit.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  

相關文章
相關標籤/搜索