From 8d92f6d48aa428b1e92a26de53fbf511e780793c Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Fri, 15 Feb 2019 18:49:04 -0600 Subject: Import WixBA from old v4 repo --- src/WixToolset.WixBA/BrowserProperties.cs | 40 ++ src/WixToolset.WixBA/Hresult.cs | 22 + src/WixToolset.WixBA/InstallationViewModel.cs | 670 +++++++++++++++++++++ src/WixToolset.WixBA/Model.cs | 129 ++++ src/WixToolset.WixBA/NewsItem.cs | 18 + src/WixToolset.WixBA/ProgressViewModel.cs | 194 ++++++ src/WixToolset.WixBA/Properties/AssemblyInfo.cs | 22 + src/WixToolset.WixBA/PropertyNotifyBase.cs | 59 ++ src/WixToolset.WixBA/RelayCommand.cs | 45 ++ .../Resources/logo-black-hollow.png | Bin 0 -> 47472 bytes .../Resources/logo-white-hollow.png | Bin 0 -> 60557 bytes src/WixToolset.WixBA/RootView.xaml | 420 +++++++++++++ src/WixToolset.WixBA/RootView.xaml.cs | 51 ++ src/WixToolset.WixBA/RootViewModel.cs | 204 +++++++ src/WixToolset.WixBA/Styles.xaml | 194 ++++++ src/WixToolset.WixBA/UpdateViewModel.cs | 207 +++++++ src/WixToolset.WixBA/WindowProperties.cs | 65 ++ src/WixToolset.WixBA/WixBA.BootstrapperCore.config | 16 + src/WixToolset.WixBA/WixBA.cs | 216 +++++++ src/WixToolset.WixBA/WixBA.csproj | 64 ++ 20 files changed, 2636 insertions(+) create mode 100644 src/WixToolset.WixBA/BrowserProperties.cs create mode 100644 src/WixToolset.WixBA/Hresult.cs create mode 100644 src/WixToolset.WixBA/InstallationViewModel.cs create mode 100644 src/WixToolset.WixBA/Model.cs create mode 100644 src/WixToolset.WixBA/NewsItem.cs create mode 100644 src/WixToolset.WixBA/ProgressViewModel.cs create mode 100644 src/WixToolset.WixBA/Properties/AssemblyInfo.cs create mode 100644 src/WixToolset.WixBA/PropertyNotifyBase.cs create mode 100644 src/WixToolset.WixBA/RelayCommand.cs create mode 100644 src/WixToolset.WixBA/Resources/logo-black-hollow.png create mode 100644 src/WixToolset.WixBA/Resources/logo-white-hollow.png create mode 100644 src/WixToolset.WixBA/RootView.xaml create mode 100644 src/WixToolset.WixBA/RootView.xaml.cs create mode 100644 src/WixToolset.WixBA/RootViewModel.cs create mode 100644 src/WixToolset.WixBA/Styles.xaml create mode 100644 src/WixToolset.WixBA/UpdateViewModel.cs create mode 100644 src/WixToolset.WixBA/WindowProperties.cs create mode 100644 src/WixToolset.WixBA/WixBA.BootstrapperCore.config create mode 100644 src/WixToolset.WixBA/WixBA.cs create mode 100644 src/WixToolset.WixBA/WixBA.csproj diff --git a/src/WixToolset.WixBA/BrowserProperties.cs b/src/WixToolset.WixBA/BrowserProperties.cs new file mode 100644 index 00000000..f40d4ed9 --- /dev/null +++ b/src/WixToolset.WixBA/BrowserProperties.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System.Windows; + using System.Windows.Controls; + + /// + /// Dependency Properties to support using a WebBrowser object. + /// + class BrowserProperties + { + /// + /// Dependency Propery used to pass an HTML string to the webBrowser object. + /// + public static readonly DependencyProperty HtmlDocProperty = + DependencyProperty.RegisterAttached("HtmlDoc", typeof(string), typeof(BrowserProperties), new PropertyMetadata(OnHtmlDocChanged)); + + public static string GetHtmlDoc(DependencyObject dependencyObject) + { + return (string)dependencyObject.GetValue(HtmlDocProperty); + } + + public static void SetHtmlDoc(DependencyObject dependencyObject, string htmldoc) + { + dependencyObject.SetValue(HtmlDocProperty, htmldoc); + } + + /// + /// Event handler that passes the HtmlDoc Dependency Property to MavigateToString method. + /// + /// + /// + private static void OnHtmlDocChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var webBrowser = (WebBrowser)d; + webBrowser.NavigateToString((string)e.NewValue); + } + } +} diff --git a/src/WixToolset.WixBA/Hresult.cs b/src/WixToolset.WixBA/Hresult.cs new file mode 100644 index 00000000..68b4c5ea --- /dev/null +++ b/src/WixToolset.WixBA/Hresult.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + + /// + /// Utility class to work with HRESULTs + /// + internal class Hresult + { + /// + /// Determines if an HRESULT was a success code or not. + /// + /// HRESULT to verify. + /// True if the status is a success code. + public static bool Succeeded(int status) + { + return status >= 0; + } + } +} diff --git a/src/WixToolset.WixBA/InstallationViewModel.cs b/src/WixToolset.WixBA/InstallationViewModel.cs new file mode 100644 index 00000000..6bec427a --- /dev/null +++ b/src/WixToolset.WixBA/InstallationViewModel.cs @@ -0,0 +1,670 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Linq; + using System.Reflection; + using System.Windows; + using System.Windows.Input; + using IO = System.IO; + using WixToolset.Bootstrapper; + + /// + /// The states of detection. + /// + public enum DetectionState + { + Absent, + Present, + Newer, + } + + /// + /// The states of installation. + /// + public enum InstallationState + { + Initializing, + Detecting, + Waiting, + Planning, + Applying, + Applied, + Failed, + } + + /// + /// The model of the installation view in WixBA. + /// + public class InstallationViewModel : PropertyNotifyBase + { + private RootViewModel root; + + private Dictionary downloadRetries; + private bool downgrade; + private string downgradeMessage; + + private ICommand licenseCommand; + private ICommand launchHomePageCommand; + private ICommand launchNewsCommand; + private ICommand launchVSExtensionPageCommand; + private ICommand installCommand; + private ICommand repairCommand; + private ICommand uninstallCommand; + private ICommand openLogCommand; + private ICommand openLogFolderCommand; + private ICommand tryAgainCommand; + + private string message; + private DateTime cachePackageStart; + private DateTime executePackageStart; + + /// + /// Creates a new model of the installation view. + /// + public InstallationViewModel(RootViewModel root) + { + this.root = root; + this.downloadRetries = new Dictionary(); + + this.root.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.RootPropertyChanged); + + WixBA.Model.Bootstrapper.DetectBegin += this.DetectBegin; + WixBA.Model.Bootstrapper.DetectRelatedBundle += this.DetectedRelatedBundle; + WixBA.Model.Bootstrapper.DetectComplete += this.DetectComplete; + WixBA.Model.Bootstrapper.PlanPackageBegin += this.PlanPackageBegin; + WixBA.Model.Bootstrapper.PlanComplete += this.PlanComplete; + WixBA.Model.Bootstrapper.ApplyBegin += this.ApplyBegin; + WixBA.Model.Bootstrapper.CacheAcquireBegin += this.CacheAcquireBegin; + WixBA.Model.Bootstrapper.CacheAcquireComplete += this.CacheAcquireComplete; + WixBA.Model.Bootstrapper.ExecutePackageBegin += this.ExecutePackageBegin; + WixBA.Model.Bootstrapper.ExecutePackageComplete += this.ExecutePackageComplete; + WixBA.Model.Bootstrapper.Error += this.ExecuteError; + WixBA.Model.Bootstrapper.ResolveSource += this.ResolveSource; + WixBA.Model.Bootstrapper.ApplyComplete += this.ApplyComplete; + } + + void RootPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (("DetectState" == e.PropertyName) || ("InstallState" == e.PropertyName)) + { + base.OnPropertyChanged("RepairEnabled"); + base.OnPropertyChanged("InstallEnabled"); + base.OnPropertyChanged("IsComplete"); + base.OnPropertyChanged("IsSuccessfulCompletion"); + base.OnPropertyChanged("IsFailedCompletion"); + base.OnPropertyChanged("StatusText"); + base.OnPropertyChanged("UninstallEnabled"); + } + } + + /// + /// Gets the version for the application. + /// + public string Version + { + get { return String.Concat("v", WixBA.Model.Version.ToString()); } + } + + /// + /// The Publisher of this bundle. + /// + public string Publisher + { + get + { + string company = "[AssemblyCompany]"; + return WixDistribution.ReplacePlaceholders(company, typeof(WixBA).Assembly); + } + } + + /// + /// The Publisher of this bundle. + /// + public string SupportUrl + { + get + { + return WixDistribution.SupportUrl; + } + } + public string VSExtensionUrl + { + get + { + return WixDistribution.VSExtensionsLandingUrl; + } + } + + public string Message + { + get + { + return this.message; + } + + set + { + if (this.message != value) + { + this.message = value; + base.OnPropertyChanged("Message"); + } + } + } + + /// + /// Gets and sets whether the view model considers this install to be a downgrade. + /// + public bool Downgrade + { + get + { + return this.downgrade; + } + + set + { + if (this.downgrade != value) + { + this.downgrade = value; + base.OnPropertyChanged("Downgrade"); + } + } + } + + public string DowngradeMessage + { + get + { + return this.downgradeMessage; + } + set + { + if (this.downgradeMessage != value) + { + this.downgradeMessage = value; + base.OnPropertyChanged("DowngradeMessage"); + } + } + } + + public ICommand LaunchHomePageCommand + { + get + { + if (this.launchHomePageCommand == null) + { + this.launchHomePageCommand = new RelayCommand(param => WixBA.LaunchUrl(this.SupportUrl), param => true); + } + + return this.launchHomePageCommand; + } + } + + public ICommand LaunchNewsCommand + { + get + { + if (this.launchNewsCommand == null) + { + this.launchNewsCommand = new RelayCommand(param => WixBA.LaunchUrl(WixDistribution.NewsUrl), param => true); + } + + return this.launchNewsCommand; + } + } + + public ICommand LaunchVSExtensionPageCommand + { + get + { + if (this.launchVSExtensionPageCommand == null) + { + this.launchVSExtensionPageCommand = new RelayCommand(param => WixBA.LaunchUrl(WixDistribution.VSExtensionsLandingUrl), param => true); + } + + return this.launchVSExtensionPageCommand; + } + } + + public ICommand LicenseCommand + { + get + { + if (this.licenseCommand == null) + { + this.licenseCommand = new RelayCommand(param => this.LaunchLicense(), param => true); + } + + return this.licenseCommand; + } + } + + public bool LicenseEnabled + { + get { return this.LicenseCommand.CanExecute(this); } + } + + public ICommand CloseCommand + { + get { return this.root.CloseCommand; } + } + + public bool IsComplete + { + get { return IsSuccessfulCompletion || IsFailedCompletion; } + } + + public bool IsSuccessfulCompletion + { + get { return InstallationState.Applied == this.root.InstallState; } + } + + public bool IsFailedCompletion + { + get { return InstallationState.Failed == this.root.InstallState; } + } + + public ICommand InstallCommand + { + get + { + if (this.installCommand == null) + { + this.installCommand = new RelayCommand(param => WixBA.Plan(LaunchAction.Install), param => this.root.DetectState == DetectionState.Absent && this.root.InstallState == InstallationState.Waiting); + } + + return this.installCommand; + } + } + + public bool InstallEnabled + { + get { return this.InstallCommand.CanExecute(this); } + } + + public ICommand RepairCommand + { + get + { + if (this.repairCommand == null) + { + this.repairCommand = new RelayCommand(param => WixBA.Plan(LaunchAction.Repair), param => this.root.DetectState == DetectionState.Present && this.root.InstallState == InstallationState.Waiting); + } + + return this.repairCommand; + } + } + + public bool RepairEnabled + { + get { return this.RepairCommand.CanExecute(this); } + } + + public ICommand UninstallCommand + { + get + { + if (this.uninstallCommand == null) + { + this.uninstallCommand = new RelayCommand(param => WixBA.Plan(LaunchAction.Uninstall), param => this.root.DetectState == DetectionState.Present && this.root.InstallState == InstallationState.Waiting); + } + + return this.uninstallCommand; + } + } + + public bool UninstallEnabled + { + get { return this.UninstallCommand.CanExecute(this); } + } + + public ICommand OpenLogCommand + { + get + { + if (this.openLogCommand == null) + { + this.openLogCommand = new RelayCommand(param => WixBA.OpenLog(new Uri(WixBA.Model.Engine.StringVariables["WixBundleLog"]))); + } + return this.openLogCommand; + } + } + + public ICommand OpenLogFolderCommand + { + get + { + if (this.openLogFolderCommand == null) + { + string logFolder = IO.Path.GetDirectoryName(WixBA.Model.Engine.StringVariables["WixBundleLog"]); + this.openLogFolderCommand = new RelayCommand(param => WixBA.OpenLogFolder(logFolder)); + } + return this.openLogFolderCommand; + } + } + + public ICommand TryAgainCommand + { + get + { + if (this.tryAgainCommand == null) + { + this.tryAgainCommand = new RelayCommand(param => + { + this.root.Canceled = false; + WixBA.Plan(WixBA.Model.PlannedAction); + }, param => IsFailedCompletion); + } + + return this.tryAgainCommand; + } + } + + public string StatusText + { + get + { + switch(this.root.InstallState) + { + case InstallationState.Applied: + return "Complete"; + case InstallationState.Failed: + return this.root.Canceled ? "Cancelled" : "Failed"; + default: + return "Unknown"; // this shouldn't be shown in the UI. + } + } + } + + /// + /// Launches the license in the default viewer. + /// + private void LaunchLicense() + { + string folder = IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + WixBA.LaunchUrl(IO.Path.Combine(folder, "License.txt")); + } + + private void DetectBegin(object sender, DetectBeginEventArgs e) + { + this.root.DetectState = e.Installed ? DetectionState.Present : DetectionState.Absent; + WixBA.Model.PlannedAction = LaunchAction.Unknown; + } + + private void DetectedRelatedBundle(object sender, DetectRelatedBundleEventArgs e) + { + if (e.Operation == RelatedOperation.Downgrade) + { + this.Downgrade = true; + } + + if (!WixBA.Model.Bootstrapper.BAManifest.Bundle.Packages.ContainsKey(e.ProductCode)) + { + WixBA.Model.Bootstrapper.BAManifest.Bundle.AddRelatedBundleAsPackage(e); + } + } + + private void DetectComplete(object sender, DetectCompleteEventArgs e) + { + // Parse the command line string before any planning. + this.ParseCommandLine(); + this.root.InstallState = InstallationState.Waiting; + + if (LaunchAction.Uninstall == WixBA.Model.Command.Action && + ResumeType.Arp != WixBA.Model.Command.Resume) // MSI and WixStdBA require some kind of confirmation before proceeding so WixBA should, too. + { + WixBA.Model.Engine.Log(LogLevel.Verbose, "Invoking automatic plan for uninstall"); + WixBA.Plan(LaunchAction.Uninstall); + } + else if (Hresult.Succeeded(e.Status)) + { + if (this.Downgrade) + { + this.root.DetectState = DetectionState.Newer; + IEnumerable relatedPackages = WixBA.Model.Bootstrapper.BAManifest.Bundle.Packages.Values.Where(p => p.Type == PackageType.UpgradeBundle); + Version installedVersion = relatedPackages.Any() ? new Version(relatedPackages.Max(p => p.Version)) : null; + if (installedVersion != null && installedVersion < new Version(4, 1) && installedVersion.Build > 10) + { + this.DowngradeMessage = "You must uninstall WiX v" + installedVersion + " before you can install this."; + } + else + { + this.DowngradeMessage = "There is already a newer version of WiX installed on this machine."; + } + } + + if (LaunchAction.Layout == WixBA.Model.Command.Action) + { + WixBA.PlanLayout(); + } + else if (WixBA.Model.Command.Display != Display.Full) + { + // If we're not waiting for the user to click install, dispatch plan with the default action. + WixBA.Model.Engine.Log(LogLevel.Verbose, "Invoking automatic plan for non-interactive mode."); + WixBA.Plan(WixBA.Model.Command.Action); + } + } + else + { + this.root.InstallState = InstallationState.Failed; + } + + // Force all commands to reevaluate CanExecute. + // InvalidateRequerySuggested must be run on the UI thread. + root.Dispatcher.Invoke(new Action(CommandManager.InvalidateRequerySuggested)); + } + + private void PlanPackageBegin(object sender, PlanPackageBeginEventArgs e) + { + // If we're able to run our BA, we don't want to install the .NET Framework since the framework on the machine is already good enough. + if ( e.PackageId.StartsWith("NetFx4", StringComparison.OrdinalIgnoreCase)) + { + e.State = RequestState.None; + } + } + + private void PlanComplete(object sender, PlanCompleteEventArgs e) + { + if (Hresult.Succeeded(e.Status)) + { + this.root.PreApplyState = this.root.InstallState; + this.root.InstallState = InstallationState.Applying; + WixBA.Model.Engine.Apply(this.root.ViewWindowHandle); + } + else + { + this.root.InstallState = InstallationState.Failed; + } + } + + private void ApplyBegin(object sender, ApplyBeginEventArgs e) + { + this.downloadRetries.Clear(); + } + + private void CacheAcquireBegin(object sender, CacheAcquireBeginEventArgs e) + { + this.cachePackageStart = DateTime.Now; + } + + private void CacheAcquireComplete(object sender, CacheAcquireCompleteEventArgs e) + { + this.AddPackageTelemetry("Cache", e.PackageOrContainerId ?? String.Empty, DateTime.Now.Subtract(this.cachePackageStart).TotalMilliseconds, e.Status); + } + + private void ExecutePackageBegin(object sender, ExecutePackageBeginEventArgs e) + { + lock (this) + { + this.executePackageStart = e.ShouldExecute ? DateTime.Now : DateTime.MinValue; + } + } + + private void ExecutePackageComplete(object sender, ExecutePackageCompleteEventArgs e) + { + lock (this) + { + if (DateTime.MinValue < this.executePackageStart) + { + this.AddPackageTelemetry("Execute", e.PackageId ?? String.Empty, DateTime.Now.Subtract(this.executePackageStart).TotalMilliseconds, e.Status); + this.executePackageStart = DateTime.MinValue; + } + } + } + + private void ExecuteError(object sender, ErrorEventArgs e) + { + lock (this) + { + if (!this.root.Canceled) + { + // If the error is a cancel coming from the engine during apply we want to go back to the preapply state. + if (InstallationState.Applying == this.root.InstallState && (int)Error.UserCancelled == e.ErrorCode) + { + this.root.InstallState = this.root.PreApplyState; + } + else + { + this.Message = e.ErrorMessage; + + if (Display.Full == WixBA.Model.Command.Display) + { + // On HTTP authentication errors, have the engine try to do authentication for us. + if (ErrorType.HttpServerAuthentication == e.ErrorType || ErrorType.HttpProxyAuthentication == e.ErrorType) + { + e.Result = Result.TryAgain; + } + else // show an error dialog. + { + MessageBoxButton msgbox = MessageBoxButton.OK; + switch (e.UIHint & 0xF) + { + case 0: + msgbox = MessageBoxButton.OK; + break; + case 1: + msgbox = MessageBoxButton.OKCancel; + break; + // There is no 2! That would have been MB_ABORTRETRYIGNORE. + case 3: + msgbox = MessageBoxButton.YesNoCancel; + break; + case 4: + msgbox = MessageBoxButton.YesNo; + break; + // default: stay with MBOK since an exact match is not available. + } + + MessageBoxResult result = MessageBoxResult.None; + WixBA.View.Dispatcher.Invoke((Action)delegate() + { + result = MessageBox.Show(WixBA.View, e.ErrorMessage, "WiX Toolset", msgbox, MessageBoxImage.Error); + } + ); + + // If there was a match from the UI hint to the msgbox value, use the result from the + // message box. Otherwise, we'll ignore it and return the default to Burn. + if ((e.UIHint & 0xF) == (int)msgbox) + { + e.Result = (Result)result; + } + } + } + } + } + else // canceled, so always return cancel. + { + e.Result = Result.Cancel; + } + } + } + + private void ResolveSource(object sender, ResolveSourceEventArgs e) + { + int retries = 0; + + this.downloadRetries.TryGetValue(e.PackageOrContainerId, out retries); + this.downloadRetries[e.PackageOrContainerId] = retries + 1; + + e.Action = retries < 3 && !String.IsNullOrEmpty(e.DownloadSource) ? BOOTSTRAPPER_RESOLVESOURCE_ACTION.Download : BOOTSTRAPPER_RESOLVESOURCE_ACTION.None; + } + + private void ApplyComplete(object sender, ApplyCompleteEventArgs e) + { + WixBA.Model.Result = e.Status; // remember the final result of the apply. + + // Set the state to applied or failed unless the state has already been set back to the preapply state + // which means we need to show the UI as it was before the apply started. + if (this.root.InstallState != this.root.PreApplyState) + { + this.root.InstallState = Hresult.Succeeded(e.Status) ? InstallationState.Applied : InstallationState.Failed; + } + + // If we're not in Full UI mode, we need to alert the dispatcher to stop and close the window for passive. + if (Bootstrapper.Display.Full != WixBA.Model.Command.Display) + { + // If its passive, send a message to the window to close. + if (Bootstrapper.Display.Passive == WixBA.Model.Command.Display) + { + WixBA.Model.Engine.Log(LogLevel.Verbose, "Automatically closing the window for non-interactive install"); + WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close)); + } + else + { + WixBA.Dispatcher.InvokeShutdown(); + } + return; + } + else if (Hresult.Succeeded(e.Status) && LaunchAction.UpdateReplace == WixBA.Model.PlannedAction) // if we successfully applied an update close the window since the new Bundle should be running now. + { + WixBA.Model.Engine.Log(LogLevel.Verbose, "Automatically closing the window since update successful."); + WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close)); + return; + } + else if (root.AutoClose) + { + // Automatically closing since the user clicked the X button. + WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close)); + return; + } + + // Force all commands to reevaluate CanExecute. + // InvalidateRequerySuggested must be run on the UI thread. + root.Dispatcher.Invoke(new Action(CommandManager.InvalidateRequerySuggested)); + } + + private void ParseCommandLine() + { + // Get array of arguments based on the system parsing algorithm. + string[] args = WixBA.Model.Command.GetCommandLineArgs(); + for (int i = 0; i < args.Length; ++i) + { + if (args[i].StartsWith("InstallFolder=", StringComparison.InvariantCultureIgnoreCase)) + { + // Allow relative directory paths. Also validates. + string[] param = args[i].Split(new char[] {'='}, 2); + this.root.InstallDirectory = IO.Path.Combine(Environment.CurrentDirectory, param[1]); + } + } + } + + private void AddPackageTelemetry(string prefix, string id, double time, int result) + { + lock (this) + { + string key = String.Format("{0}Time_{1}", prefix, id); + string value = time.ToString(); + WixBA.Model.Telemetry.Add(new KeyValuePair(key, value)); + + key = String.Format("{0}Result_{1}", prefix, id); + value = String.Concat("0x", result.ToString("x")); + WixBA.Model.Telemetry.Add(new KeyValuePair(key, value)); + } + } + } +} diff --git a/src/WixToolset.WixBA/Model.cs b/src/WixToolset.WixBA/Model.cs new file mode 100644 index 00000000..9f03e95b --- /dev/null +++ b/src/WixToolset.WixBA/Model.cs @@ -0,0 +1,129 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + using System.Collections.Generic; + using System.Net; + using WixToolset.Bootstrapper; + + /// + /// The model. + /// + public class Model + { + private const string BurnBundleInstallDirectoryVariable = "InstallFolder"; + private const string BurnBundleLayoutDirectoryVariable = "WixBundleLayoutDirectory"; + private const string BurnBundleVersionVariable = "WixBundleVersion"; + + /// + /// Creates a new model for the UX. + /// + /// Bootstrapper hosting the UX. + public Model(BootstrapperApplication bootstrapper) + { + this.Bootstrapper = bootstrapper; + this.Telemetry = new List>(); + this.Version = this.Engine.VersionVariables[BurnBundleVersionVariable]; + } + + /// + /// Gets the bootstrapper. + /// + public BootstrapperApplication Bootstrapper { get; private set; } + + /// + /// Gets the bootstrapper command-line. + /// + public Command Command { get { return this.Bootstrapper.Command; } } + + /// + /// Gets the bootstrapper engine. + /// + public Engine Engine { get { return this.Bootstrapper.Engine; } } + + /// + /// Gets the key/value pairs used in telemetry. + /// + public List> Telemetry { get; private set; } + + /// + /// Get or set the final result of the installation. + /// + public int Result { get; set; } + + /// + /// Get the version of the install. + /// + public Version Version { get; private set; } + + /// + /// Get or set the path where the bundle is installed. + /// + public string InstallDirectory + { + get + { + if (!this.Engine.StringVariables.Contains(BurnBundleInstallDirectoryVariable)) + { + return null; + } + + return this.Engine.StringVariables[BurnBundleInstallDirectoryVariable]; + } + + set + { + this.Engine.StringVariables[BurnBundleInstallDirectoryVariable] = value; + } + } + + /// + /// Get or set the path for the layout to be created. + /// + public string LayoutDirectory + { + get + { + if (!this.Engine.StringVariables.Contains(BurnBundleLayoutDirectoryVariable)) + { + return null; + } + + return this.Engine.StringVariables[BurnBundleLayoutDirectoryVariable]; + } + + set + { + this.Engine.StringVariables[BurnBundleLayoutDirectoryVariable] = value; + } + } + + public LaunchAction PlannedAction { get; set; } + + /// + /// Creates a correctly configured HTTP web request. + /// + /// URI to connect to. + /// Correctly configured HTTP web request. + public HttpWebRequest CreateWebRequest(string uri) + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); + request.UserAgent = String.Concat("WixInstall", this.Version.ToString()); + + return request; + } + + /// + /// Gets the display name for a package if possible. + /// + /// Identity of the package to find the display name. + /// Display name of the package if found or the package id if not. + public string GetPackageName(string packageId) + { + PackageInfo package; + + return this.Bootstrapper.BAManifest.Bundle.Packages.TryGetValue(packageId, out package) ? package.DisplayName : packageId; + } + } +} diff --git a/src/WixToolset.WixBA/NewsItem.cs b/src/WixToolset.WixBA/NewsItem.cs new file mode 100644 index 00000000..a8350104 --- /dev/null +++ b/src/WixToolset.WixBA/NewsItem.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + + /// + /// The model for an individual news item. + /// + public class NewsItem + { + public string Author { get; set; } + public string Title { get; set; } + public string Url { get; set; } + public string Snippet { get; set; } + public DateTime Updated { get; set; } + } +} diff --git a/src/WixToolset.WixBA/ProgressViewModel.cs b/src/WixToolset.WixBA/ProgressViewModel.cs new file mode 100644 index 00000000..30aee5f1 --- /dev/null +++ b/src/WixToolset.WixBA/ProgressViewModel.cs @@ -0,0 +1,194 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Text.RegularExpressions; +using WixToolset.Bootstrapper; + +namespace WixToolset.UX +{ + public class ProgressViewModel : PropertyNotifyBase + { + private static readonly Regex TrimActionTimeFromMessage = new Regex(@"^\w+\s+\d+:\d+:\d+:\s+", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.Singleline); + + private RootViewModel root; + private Dictionary executingPackageOrderIndex; + + private int progressPhases; + private int progress; + private int cacheProgress; + private int executeProgress; + private string package; + private string message; + + public ProgressViewModel(RootViewModel root) + { + this.root = root; + this.executingPackageOrderIndex = new Dictionary(); + + this.root.PropertyChanged += this.RootPropertyChanged; + + WixBA.Model.Bootstrapper.ExecutePackageBegin += this.ExecutePackageBegin; + WixBA.Model.Bootstrapper.ExecutePackageComplete += this.ExecutePackageComplete; + WixBA.Model.Bootstrapper.ExecuteProgress += this.ApplyExecuteProgress; + WixBA.Model.Bootstrapper.PlanBegin += this.PlanBegin; + WixBA.Model.Bootstrapper.PlanPackageComplete += this.PlanPackageComplete; + WixBA.Model.Bootstrapper.ApplyBegin += this.ApplyBegin; + WixBA.Model.Bootstrapper.Progress += this.ApplyProgress; + WixBA.Model.Bootstrapper.CacheAcquireProgress += this.CacheAcquireProgress; + WixBA.Model.Bootstrapper.CacheComplete += this.CacheComplete; + } + + public bool ProgressEnabled + { + get { return this.root.InstallState == InstallationState.Applying; } + } + + public int Progress + { + get + { + return this.progress; + } + + set + { + if (this.progress != value) + { + this.progress = value; + base.OnPropertyChanged("Progress"); + } + } + } + + public string Package + { + get + { + return this.package; + } + + set + { + if (this.package != value) + { + this.package = value; + base.OnPropertyChanged("Package"); + } + } + } + + public string Message + { + get + { + return this.message; + } + + set + { + if (this.message != value) + { + this.message = value; + base.OnPropertyChanged("Message"); + } + } + } + + void RootPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if ("InstallState" == e.PropertyName) + { + base.OnPropertyChanged("ProgressEnabled"); + } + } + + private void PlanBegin(object sender, PlanBeginEventArgs e) + { + lock (this) + { + 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); + } + } + } + + private void ExecutePackageBegin(object sender, ExecutePackageBeginEventArgs e) + { + lock (this) + { + this.Package = WixBA.Model.GetPackageName(e.PackageId); + this.Message = String.Format("Processing: {0}", this.Package); + e.Cancel = this.root.Canceled; + } + } + + private void ExecutePackageComplete(object sender, ExecutePackageCompleteEventArgs e) + { + lock (this) + { // avoid a stale display + this.Message = String.Empty; + } + } + + private void ApplyBegin(object sender, ApplyBeginEventArgs e) + { + this.progressPhases = e.PhaseCount; + } + + private void ApplyProgress(object sender, ProgressEventArgs e) + { + lock (this) + { + e.Cancel = this.root.Canceled; + } + } + + private void CacheAcquireProgress(object sender, CacheAcquireProgressEventArgs e) + { + lock (this) + { + this.cacheProgress = e.OverallPercentage; + this.Progress = (this.cacheProgress + this.executeProgress) / this.progressPhases; + e.Cancel = this.root.Canceled; + } + } + + private void CacheComplete(object sender, CacheCompleteEventArgs e) + { + lock (this) + { + this.cacheProgress = 100; + this.Progress = (this.cacheProgress + this.executeProgress) / this.progressPhases; + } + } + + private void ApplyExecuteProgress(object sender, ExecuteProgressEventArgs e) + { + lock (this) + { + this.executeProgress = e.OverallPercentage; + this.Progress = (this.cacheProgress + this.executeProgress) / this.progressPhases; + + if (WixBA.Model.Command.Display == Display.Embedded) + { + WixBA.Model.Engine.SendEmbeddedProgress(e.ProgressPercentage, this.Progress); + } + + e.Cancel = this.root.Canceled; + } + } + } +} diff --git a/src/WixToolset.WixBA/Properties/AssemblyInfo.cs b/src/WixToolset.WixBA/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..58d88046 --- /dev/null +++ b/src/WixToolset.WixBA/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using WixToolset.Bootstrapper; +using WixToolset.UX; + + +[assembly: AssemblyTitle("WixBA")] +[assembly: AssemblyDescription("WiX User Experience")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] +[assembly: Guid("0ffc4944-9295-40b7-adac-3a6864b5219b")] +[assembly: CLSCompliantAttribute(true)] + +// Identifies the class that derives from UserExperience and is the UX class that gets +// instantiated by the interop layer +[assembly: BootstrapperApplication(typeof(WixBA))] diff --git a/src/WixToolset.WixBA/PropertyNotifyBase.cs b/src/WixToolset.WixBA/PropertyNotifyBase.cs new file mode 100644 index 00000000..03174306 --- /dev/null +++ b/src/WixToolset.WixBA/PropertyNotifyBase.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + using System.ComponentModel; + using System.Diagnostics; + + /// + /// It provides support for property change notifications. + /// + public abstract class PropertyNotifyBase : INotifyPropertyChanged + { + /// + /// Initializes a new instance of the class. + /// + protected PropertyNotifyBase() + { + } + + /// + /// Raised when a property on this object has a new value. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// 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. + /// + /// Property name to verify. + [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)); + } + } + + /// + /// Raises this object's PropertyChanged event. + /// + /// The property that has a new value. + protected virtual void OnPropertyChanged(string propertyName) + { + this.VerifyPropertyName(propertyName); + + PropertyChangedEventHandler handler = this.PropertyChanged; + if (null != handler) + { + PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName); + handler(this, e); + } + } + } +} diff --git a/src/WixToolset.WixBA/RelayCommand.cs b/src/WixToolset.WixBA/RelayCommand.cs new file mode 100644 index 00000000..ecc482da --- /dev/null +++ b/src/WixToolset.WixBA/RelayCommand.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + using System.Diagnostics; + using System.Windows.Input; + + /// + /// Base class that implements ICommand interface via delegates. + /// + public class RelayCommand : ICommand + { + private readonly Action execute; + private readonly Predicate canExecute; + + public RelayCommand(Action execute) + : this(execute, null) + { + } + + public RelayCommand(Action execute, Predicate 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); + } + } +} diff --git a/src/WixToolset.WixBA/Resources/logo-black-hollow.png b/src/WixToolset.WixBA/Resources/logo-black-hollow.png new file mode 100644 index 00000000..9d0290bd Binary files /dev/null and b/src/WixToolset.WixBA/Resources/logo-black-hollow.png differ diff --git a/src/WixToolset.WixBA/Resources/logo-white-hollow.png b/src/WixToolset.WixBA/Resources/logo-white-hollow.png new file mode 100644 index 00000000..242c7350 Binary files /dev/null and b/src/WixToolset.WixBA/Resources/logo-white-hollow.png differ diff --git a/src/WixToolset.WixBA/RootView.xaml b/src/WixToolset.WixBA/RootView.xaml new file mode 100644 index 00000000..bbf2f9f2 --- /dev/null +++ b/src/WixToolset.WixBA/RootView.xaml @@ -0,0 +1,420 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WixToolset.WixBA/RootView.xaml.cs b/src/WixToolset.WixBA/RootView.xaml.cs new file mode 100644 index 00000000..1d4301f2 --- /dev/null +++ b/src/WixToolset.WixBA/RootView.xaml.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System.ComponentModel; + using System.Windows; + using System.Windows.Interop; + + /// + /// Interaction logic for View.xaml + /// + public partial class RootView : Window + { + /// + /// Creates the view populated with it's model. + /// + /// Model for the view. + public RootView(RootViewModel viewModel) + { + this.DataContext = viewModel; + + this.Loaded += (sender, e) => WixBA.Model.Engine.CloseSplashScreen(); + this.Closed += (sender, e) => this.Dispatcher.InvokeShutdown(); // shutdown dispatcher when the window is closed. + + this.InitializeComponent(); + + viewModel.Dispatcher = this.Dispatcher; + viewModel.ViewWindowHandle = new WindowInteropHelper(this).EnsureHandle(); + } + + /// + /// Event is fired when the window is closing. + /// + /// + /// + private void Window_Closing(object sender, CancelEventArgs e) + { + RootViewModel rvm = this.DataContext as RootViewModel; + if ((null != rvm) && (InstallationState.Applying == rvm.InstallState)) + { + rvm.CancelButton_Click(); + if (rvm.Canceled) + { + // Defer closing until the engine has canceled processing. + e.Cancel = true; + rvm.AutoClose = true; + } + } + } + } +} diff --git a/src/WixToolset.WixBA/RootViewModel.cs b/src/WixToolset.WixBA/RootViewModel.cs new file mode 100644 index 00000000..1de89adf --- /dev/null +++ b/src/WixToolset.WixBA/RootViewModel.cs @@ -0,0 +1,204 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + using System.Diagnostics; + using System.Reflection; + using System.Windows; + using System.Windows.Input; + using System.Windows.Threading; + using WixToolset.Bootstrapper; + + /// + /// The errors returned from the engine + /// + public enum Error + { + UserCancelled = 1223, + } + + /// + /// The model of the root view in WixBA. + /// + public class RootViewModel : PropertyNotifyBase + { + private ICommand cancelCommand; + private ICommand closeCommand; + + private bool canceled; + private InstallationState installState; + private DetectionState detectState; + + /// + /// Creates a new model of the root view. + /// + public RootViewModel() + { + this.InstallationViewModel = new InstallationViewModel(this); + this.ProgressViewModel = new ProgressViewModel(this); + this.UpdateViewModel = new UpdateViewModel(this); + } + + public InstallationViewModel InstallationViewModel { get; private set; } + public ProgressViewModel ProgressViewModel { get; private set; } + public UpdateViewModel UpdateViewModel { get; private set; } + public Dispatcher Dispatcher { get; set; } + public IntPtr ViewWindowHandle { get; set; } + public bool AutoClose { get; set; } + + public ICommand CloseCommand + { + get + { + if (this.closeCommand == null) + { + this.closeCommand = new RelayCommand(param => WixBA.View.Close()); + } + + return this.closeCommand; + } + } + + public ICommand CancelCommand + { + get + { + if (this.cancelCommand == null) + { + this.cancelCommand = new RelayCommand(param => + { + this.CancelButton_Click(); + }, + param => !this.Canceled); + } + + return this.cancelCommand; + } + } + + public bool CancelAvailable + { + get { return InstallationState.Applying == this.InstallState; } + } + + public bool Canceled + { + get + { + return this.canceled; + } + + set + { + if (this.canceled != value) + { + this.canceled = value; + base.OnPropertyChanged("Canceled"); + } + } + } + + /// + /// Gets and sets the detect state of the view's model. + /// + public DetectionState DetectState + { + get + { + return this.detectState; + } + + set + { + if (this.detectState != value) + { + this.detectState = value; + + // Notify all the properties derived from the state that the state changed. + base.OnPropertyChanged("DetectState"); + } + } + } + + /// + /// Gets and sets the installation state of the view's model. + /// + public InstallationState InstallState + { + get + { + return this.installState; + } + + set + { + if (this.installState != value) + { + this.installState = value; + + // Notify all the properties derived from the state that the state changed. + base.OnPropertyChanged("InstallState"); + base.OnPropertyChanged("CancelAvailable"); + } + } + } + + /// + /// Gets and sets the state of the view's model before apply begins in order to return to that state if cancel or rollback occurs. + /// + public InstallationState PreApplyState { get; set; } + + /// + /// Gets and sets the path where the bundle is currently installed or will be installed. + /// + public string InstallDirectory + { + get + { + return WixBA.Model.InstallDirectory; + } + + set + { + if (WixBA.Model.InstallDirectory != value) + { + WixBA.Model.InstallDirectory = value; + base.OnPropertyChanged("InstallDirectory"); + } + } + } + + /// + /// The Title of this bundle. + /// + public string Title + { + get + { + return WixDistribution.ShortProduct; + } + } + + /// + /// Prompts the user to make sure they want to cancel. + /// This needs to run on the UI thread, use Dispatcher.Invoke to call this from a background thread. + /// + public void CancelButton_Click() + { + if (this.Canceled) + { + return; + } + + if (Display.Full == WixBA.Model.Command.Display) + { + this.Canceled = (MessageBoxResult.Yes == MessageBox.Show(WixBA.View, "Are you sure you want to cancel?", "WiX Toolset", MessageBoxButton.YesNo, MessageBoxImage.Error)); + } + else + { + this.Canceled = true; + } + } + } +} diff --git a/src/WixToolset.WixBA/Styles.xaml b/src/WixToolset.WixBA/Styles.xaml new file mode 100644 index 00000000..89ccd2ea --- /dev/null +++ b/src/WixToolset.WixBA/Styles.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + Segoe UI, Arial + 45 + 12 + 14 + + + + + + + #FF1EF1E8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WixToolset.WixBA/UpdateViewModel.cs b/src/WixToolset.WixBA/UpdateViewModel.cs new file mode 100644 index 00000000..6b60112c --- /dev/null +++ b/src/WixToolset.WixBA/UpdateViewModel.cs @@ -0,0 +1,207 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + using System.ComponentModel; + using System.Windows.Input; + using WixToolset.Bootstrapper; + + /// + /// The states of the update view model. + /// + public enum UpdateState + { + Unknown, + Initializing, + Checking, + Current, + Available, + Failed, + } + + /// + /// The model of the update view. + /// + public class UpdateViewModel : PropertyNotifyBase + { + private RootViewModel root; + private UpdateState state; + private ICommand updateCommand; + private string updateVersion; + private string updateChanges; + + + public UpdateViewModel(RootViewModel root) + { + this.root = root; + WixBA.Model.Bootstrapper.DetectUpdateBegin += this.DetectUpdateBegin; + WixBA.Model.Bootstrapper.DetectUpdate += this.DetectUpdate; + WixBA.Model.Bootstrapper.DetectUpdateComplete += this.DetectUpdateComplete; + + this.root.PropertyChanged += new PropertyChangedEventHandler(this.RootPropertyChanged); + + this.State = UpdateState.Initializing; + } + + void RootPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if ("InstallState" == e.PropertyName) + { + base.OnPropertyChanged("CanUpdate"); + } + } + + public bool CheckingEnabled + { + get { return this.State == UpdateState.Initializing || this.State == UpdateState.Checking; } + } + + public bool CanUpdate + { + get + { + switch(this.root.InstallState) + { + case InstallationState.Waiting: + case InstallationState.Applied: + case InstallationState.Failed: + return this.IsUpdateAvailable; + default: + return false; + } + } + } + + public ICommand UpdateCommand + { + get + { + if (this.updateCommand == null) + { + this.updateCommand = new RelayCommand(param => WixBA.Plan(LaunchAction.UpdateReplace), param => this.CanUpdate); + } + + return this.updateCommand; + } + } + + public bool IsUpdateAvailable + { + get { return this.State == UpdateState.Available; } + } + + /// + /// Gets and sets the state of the update view model. + /// + public UpdateState State + { + get + { + return this.state; + } + + set + { + if (this.state != value) + { + this.state = value; + base.OnPropertyChanged("State"); + base.OnPropertyChanged("CanUpdate"); + base.OnPropertyChanged("CheckingEnabled"); + base.OnPropertyChanged("IsUpdateAvailable"); + } + } + } + /// + /// The version of an available update. + /// + public string UpdateVersion + { + get + { + return updateVersion; + } + set + { + if (this.updateVersion != value) + { + this.updateVersion = value; + base.OnPropertyChanged("UpdateVersion"); + } + } + } + + /// + /// The changes in the available update. + /// + public string UpdateChanges + { + get + { + return updateChanges; + } + set + { + if (this.updateChanges != value) + { + this.updateChanges = value; + base.OnPropertyChanged("UpdateChanges"); + } + } + } + + private void DetectUpdateBegin(object sender, Bootstrapper.DetectUpdateBeginEventArgs e) + { + // Don't check for updates if: + // the first check failed (no retry) + // if we are being run as an uninstall + // if we are not under a full UI. + if ((UpdateState.Failed != this.State) && (LaunchAction.Uninstall != WixBA.Model.Command.Action) && (Display.Full == WixBA.Model.Command.Display)) + { + this.State = UpdateState.Checking; + e.Skip = false; + } + } + + private void DetectUpdate(object sender, Bootstrapper.DetectUpdateEventArgs e) + { + // The list of updates is sorted in descending version, so the first callback should be the largest update available. + // This update should be either larger than ours (so we are out of date), the same as ours (so we are current) + // or smaller than ours (we have a private build). If we really wanted to, we could leave the e.StopProcessingUpdates alone and + // enumerate all of the updates. + WixBA.Model.Engine.Log(LogLevel.Verbose, String.Format("Potential update v{0} from '{1}'; current version: v{2}", e.Version, e.UpdateLocation, WixBA.Model.Version)); + if (e.Version > WixBA.Model.Version) + { + WixBA.Model.Engine.SetUpdate(null, e.UpdateLocation, e.Size, UpdateHashType.None, null); + this.UpdateVersion = String.Concat("v", e.Version.ToString()); + string changesFormat = @"{0}"; + this.UpdateChanges = String.Format(changesFormat, e.Content); + this.State = UpdateState.Available; + } + else + { + this.State = UpdateState.Current; + } + e.StopProcessingUpdates = true; + } + + private void DetectUpdateComplete(object sender, Bootstrapper.DetectUpdateCompleteEventArgs e) + { + // Failed to process an update, allow the existing bundle to still install. + if ((UpdateState.Failed != this.State) && !Hresult.Succeeded(e.Status)) + { + this.State = UpdateState.Failed; + WixBA.Model.Engine.Log(LogLevel.Verbose, String.Format("Failed to locate an update, status of 0x{0:X8}, updates disabled.", e.Status)); + e.IgnoreError = true; + } + // If we are uninstalling, we don't want to check or show an update + // If we are checking, then the feed didn't find any valid enclosures + // If we are initializing, we're either uninstalling or not a full UI + else if ((LaunchAction.Uninstall == WixBA.Model.Command.Action) || (UpdateState.Initializing == this.State) || (UpdateState.Checking == this.State)) + { + this.State = UpdateState.Unknown; + } + } + } +} diff --git a/src/WixToolset.WixBA/WindowProperties.cs b/src/WixToolset.WixBA/WindowProperties.cs new file mode 100644 index 00000000..bead5cc1 --- /dev/null +++ b/src/WixToolset.WixBA/WindowProperties.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + using System.Windows; + using System.Windows.Media; + + /// + /// Dependency Properties associated with the main Window object. + /// + public class WindowProperties : DependencyObject + { + /// + /// Dependency Property to hold the result of detecting the relative luminosity (or brightness) of a Windows background. + /// + public static readonly DependencyProperty IsLightBackgroundProperty = DependencyProperty.Register( + "IsLightBackground", typeof(bool), typeof(WindowProperties), new PropertyMetadata( false )); + + private static Lazy _instance = new Lazy(() => + { + WindowProperties wp = new WindowProperties(); + wp.CheckBackgroundBrightness(); + return wp; + }); + + public static WindowProperties Instance + { + get + { + return _instance.Value; + } + } + + + public bool IsLightBackground + { + get { return (bool)GetValue(IsLightBackgroundProperty); } + private set { SetValue(IsLightBackgroundProperty, value); } + } + + /// + /// Use the Luminosity parameter of the background color to detect light vs dark theme settings. + /// + /// + /// This approach detects both the common High Contrast themes (White vs Black) and custom themes which may have relatively lighter backgrounds. + /// + public void CheckBackgroundBrightness() + { + SolidColorBrush windowbrush = System.Windows.SystemColors.WindowBrush; + System.Drawing.Color dcolor = System.Drawing.Color.FromArgb(windowbrush.Color.A, windowbrush.Color.R, windowbrush.Color.G, windowbrush.Color.B); + + var brightness = dcolor.GetBrightness(); + // Test for 'Lightness' at an arbitrary point, approaching 1.0 (White). + if (0.7 < brightness) + { + this.IsLightBackground = true; + } + else + { + this.IsLightBackground = false; + } + } + } +} diff --git a/src/WixToolset.WixBA/WixBA.BootstrapperCore.config b/src/WixToolset.WixBA/WixBA.BootstrapperCore.config new file mode 100644 index 00000000..8e1d4729 --- /dev/null +++ b/src/WixToolset.WixBA/WixBA.BootstrapperCore.config @@ -0,0 +1,16 @@ + + + + + + +
+ + + + + + + + + diff --git a/src/WixToolset.WixBA/WixBA.cs b/src/WixToolset.WixBA/WixBA.cs new file mode 100644 index 00000000..fb69a346 --- /dev/null +++ b/src/WixToolset.WixBA/WixBA.cs @@ -0,0 +1,216 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.UX +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Net; + using System.Text; + using System.Windows.Input; + using Threading = System.Windows.Threading; + using WinForms = System.Windows.Forms; + + using WixToolset.Bootstrapper; + + /// + /// The WiX toolset user experience. + /// + public class WixBA : BootstrapperApplication + { + /// + /// Gets the global model. + /// + static public Model Model { get; private set; } + + /// + /// Gets the global view. + /// + static public RootView View { get; private set; } + // TODO: We should refactor things so we dont have a global View. + + /// + /// Gets the global dispatcher. + /// + static public Threading.Dispatcher Dispatcher { get; private set; } + + /// + /// Launches the default web browser to the provided URI. + /// + /// URI to open the web browser. + public static void LaunchUrl(string uri) + { + WixBA.UseShellExecute(uri); + } + + /// + /// Open a log file. + /// + /// URI to a log file. + internal static void OpenLog(Uri uri) + { + WixBA.UseShellExecute(uri.ToString()); + } + + /// + /// Open a log folder. + /// + /// path to a log folder. + internal static void OpenLogFolder(string logFolder) + { + WixBA.UseShellExecute(logFolder); + } + + /// + /// Open a log folder. + /// + /// path to a log folder. + private static void UseShellExecute(string path) + { + // Switch the wait cursor since shellexec can take a second or so. + System.Windows.Input.Cursor cursor = WixBA.View.Cursor; + WixBA.View.Cursor = System.Windows.Input.Cursors.Wait; + Process process = null; + try + { + process = new Process(); + process.StartInfo.FileName = path; + process.StartInfo.UseShellExecute = true; + process.StartInfo.Verb = "open"; + + process.Start(); + } + finally + { + if (null != process) + { + process.Dispose(); + } + // back to the original cursor. + WixBA.View.Cursor = cursor; + } + } + + /// + /// Starts planning the appropriate action. + /// + /// Action to plan. + public static void Plan(LaunchAction action) + { + WixBA.Model.PlannedAction = action; + WixBA.Model.Engine.Plan(WixBA.Model.PlannedAction); + } + + public static void PlanLayout() + { + // Either default or set the layout directory + if (String.IsNullOrEmpty(WixBA.Model.Command.LayoutDirectory)) + { + WixBA.Model.LayoutDirectory = Directory.GetCurrentDirectory(); + + // Ask the user for layout folder if one wasn't provided and we're in full UI mode + if (WixBA.Model.Command.Display == Display.Full) + { + WixBA.Dispatcher.Invoke((Action)delegate() + { + WinForms.FolderBrowserDialog browserDialog = new WinForms.FolderBrowserDialog(); + browserDialog.RootFolder = Environment.SpecialFolder.MyComputer; + + // Default to the current directory. + browserDialog.SelectedPath = WixBA.Model.LayoutDirectory; + WinForms.DialogResult result = browserDialog.ShowDialog(); + + if (WinForms.DialogResult.OK == result) + { + WixBA.Model.LayoutDirectory = browserDialog.SelectedPath; + WixBA.Plan(WixBA.Model.Command.Action); + } + else + { + WixBA.View.Close(); + } + } + ); + } + } + else + { + WixBA.Model.LayoutDirectory = WixBA.Model.Command.LayoutDirectory; + WixBA.Plan(WixBA.Model.Command.Action); + } + } + + /// + /// Thread entry point for WiX Toolset UX. + /// + protected override void Run() + { + this.Engine.Log(LogLevel.Verbose, "Running the WiX BA."); + WixBA.Model = new Model(this); + WixBA.Dispatcher = Threading.Dispatcher.CurrentDispatcher; + RootViewModel viewModel = new RootViewModel(); + + // Kick off detect which will populate the view models. + this.Engine.Detect(); + + // Create a Window to show UI. + if (WixBA.Model.Command.Display == Display.Passive || + WixBA.Model.Command.Display == Display.Full) + { + this.Engine.Log(LogLevel.Verbose, "Creating a UI."); + WixBA.View = new RootView(viewModel); + WixBA.View.Show(); + } + + Threading.Dispatcher.Run(); + + this.PostTelemetry(); + this.Engine.Quit(WixBA.Model.Result); + } + + private void PostTelemetry() + { + string result = String.Concat("0x", WixBA.Model.Result.ToString("x")); + + StringBuilder telemetryData = new StringBuilder(); + foreach (KeyValuePair kvp in WixBA.Model.Telemetry) + { + telemetryData.AppendFormat("{0}={1}+", kvp.Key, kvp.Value); + } + telemetryData.AppendFormat("Result={0}", result); + + byte[] data = Encoding.UTF8.GetBytes(telemetryData.ToString()); + + try + { + HttpWebRequest post = WixBA.Model.CreateWebRequest(String.Format(WixDistribution.TelemetryUrlFormat, WixBA.Model.Version.ToString(), result)); + post.Method = "POST"; + post.ContentType = "application/x-www-form-urlencoded"; + post.ContentLength = data.Length; + + using (Stream postStream = post.GetRequestStream()) + { + postStream.Write(data, 0, data.Length); + } + + HttpWebResponse response = (HttpWebResponse)post.GetResponse(); + } + catch (ArgumentException) + { + } + catch (FormatException) + { + } + catch (OverflowException) + { + } + catch (ProtocolViolationException) + { + } + catch (WebException) + { + } + } + } +} diff --git a/src/WixToolset.WixBA/WixBA.csproj b/src/WixToolset.WixBA/WixBA.csproj new file mode 100644 index 00000000..6858b172 --- /dev/null +++ b/src/WixToolset.WixBA/WixBA.csproj @@ -0,0 +1,64 @@ + + + + + {7C27518B-84AD-4679-8EF4-29DF552CF1AC} + WixBA + Library + WixToolset.UX + + + + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + + RootView.xaml + + + + + + + + PreserveNewest + Designer + + + + + + + + + + False + + + + + + + + + + + + + + + + + \ No newline at end of file -- cgit v1.2.3-55-g6feb