經常安裝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工程。
<?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 後面可以用來檢測目前電腦是否安裝了這個軟體。
二、建立WPF界面程式。
1).建立一個C# Library工程CustomBA,引用BootstrapperCore.dll 及 WPF相關dll
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);
}
}
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);
}
}
}
使用方法差别不大。
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>
修改為:
<?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
}
}
在構造函數内部,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;
}
}
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="..\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>
小結: 以上就是整個過程,基本走通了,但還沒有達到最終目的。要制作想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 XML2.源碼:
wix3.8