From 3f583916719eeef598d10a5d4e14ef14f008243b Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Tue, 11 May 2021 07:36:37 -0700 Subject: Merge Dtf --- src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs | 5 + src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj | 56 +++++++ .../Dtf/EmbeddedUI/InstallProgressCounter.cs | 176 +++++++++++++++++++++ src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs | 132 ++++++++++++++++ src/samples/Dtf/EmbeddedUI/SetupWizard.xaml | 17 ++ src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs | 111 +++++++++++++ 6 files changed, 497 insertions(+) create mode 100644 src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs create mode 100644 src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj create mode 100644 src/samples/Dtf/EmbeddedUI/InstallProgressCounter.cs create mode 100644 src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs create mode 100644 src/samples/Dtf/EmbeddedUI/SetupWizard.xaml create mode 100644 src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs (limited to 'src/samples/Dtf/EmbeddedUI') diff --git a/src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs b/src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs new file mode 100644 index 00000000..7a2fa039 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// 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.Reflection; + +[assembly: AssemblyDescription("Sample managed embedded external UI")] diff --git a/src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj b/src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj new file mode 100644 index 00000000..e4c52a26 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj @@ -0,0 +1,56 @@ + + + + + {864B8C50-7895-4485-AC89-900D86FD8C0D} + Library + WixToolset.Dtf.Samples.EmbeddedUI + WixToolset.Dtf.Samples.EmbeddedUI + v3.5 + 512 + + + + + + + SetupWizard.xaml + + + + + MSBuild:Compile + Designer + + + + + 3.0 + + + 3.0 + + + + 3.5 + + + + 3.0 + + + + + {24121677-0ed0-41b5-833f-1b9a18e87bf4} + WixToolset.Dtf.WindowsInstaller + + + + + + + diff --git a/src/samples/Dtf/EmbeddedUI/InstallProgressCounter.cs b/src/samples/Dtf/EmbeddedUI/InstallProgressCounter.cs new file mode 100644 index 00000000..df77e106 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/InstallProgressCounter.cs @@ -0,0 +1,176 @@ +// 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.Dtf.Samples.EmbeddedUI +{ + using System; + using WixToolset.Dtf.WindowsInstaller; + + /// + /// Tracks MSI progress messages and converts them to usable progress. + /// + public class InstallProgressCounter + { + private int total; + private int completed; + private int step; + private bool moveForward; + private bool enableActionData; + private int progressPhase; + private double scriptPhaseWeight; + + public InstallProgressCounter() : this(0.3) + { + } + + public InstallProgressCounter(double scriptPhaseWeight) + { + if (!(0 <= scriptPhaseWeight && scriptPhaseWeight <= 1)) + { + throw new ArgumentOutOfRangeException("scriptPhaseWeight"); + } + + this.scriptPhaseWeight = scriptPhaseWeight; + } + + /// + /// Gets a number between 0 and 1 that indicates the overall installation progress. + /// + public double Progress { get; private set; } + + public void ProcessMessage(InstallMessage messageType, Record messageRecord) + { + // This MSI progress-handling code was mostly borrowed from burn and translated from C++ to C#. + + switch (messageType) + { + case InstallMessage.ActionStart: + if (this.enableActionData) + { + this.enableActionData = false; + } + break; + + case InstallMessage.ActionData: + if (this.enableActionData) + { + if (this.moveForward) + { + this.completed += this.step; + } + else + { + this.completed -= this.step; + } + + this.UpdateProgress(); + } + break; + + case InstallMessage.Progress: + this.ProcessProgressMessage(messageRecord); + break; + } + } + + private void ProcessProgressMessage(Record progressRecord) + { + // This MSI progress-handling code was mostly borrowed from burn and translated from C++ to C#. + + if (progressRecord == null || progressRecord.FieldCount == 0) + { + return; + } + + int fieldCount = progressRecord.FieldCount; + int progressType = progressRecord.GetInteger(1); + string progressTypeString = String.Empty; + switch (progressType) + { + case 0: // Master progress reset + if (fieldCount < 4) + { + return; + } + + this.progressPhase++; + + this.total = progressRecord.GetInteger(2); + if (this.progressPhase == 1) + { + // HACK!!! this is a hack courtesy of the Windows Installer team. It seems the script planning phase + // is always off by "about 50". So we'll toss an extra 50 ticks on so that the standard progress + // doesn't go over 100%. If there are any custom actions, they may blow the total so we'll call this + // "close" and deal with the rest. + this.total += 50; + } + + this.moveForward = (progressRecord.GetInteger(3) == 0); + this.completed = (this.moveForward ? 0 : this.total); // if forward start at 0, if backwards start at max + this.enableActionData = false; + + this.UpdateProgress(); + break; + + case 1: // Action info + if (fieldCount < 3) + { + return; + } + + if (progressRecord.GetInteger(3) == 0) + { + this.enableActionData = false; + } + else + { + this.enableActionData = true; + this.step = progressRecord.GetInteger(2); + } + break; + + case 2: // Progress report + if (fieldCount < 2 || this.total == 0 || this.progressPhase == 0) + { + return; + } + + if (this.moveForward) + { + this.completed += progressRecord.GetInteger(2); + } + else + { + this.completed -= progressRecord.GetInteger(2); + } + + this.UpdateProgress(); + break; + + case 3: // Progress total addition + this.total += progressRecord.GetInteger(2); + break; + } + } + + private void UpdateProgress() + { + if (this.progressPhase < 1 || this.total == 0) + { + this.Progress = 0; + } + else if (this.progressPhase == 1) + { + this.Progress = this.scriptPhaseWeight * Math.Min(this.completed, this.total) / this.total; + } + else if (this.progressPhase == 2) + { + this.Progress = this.scriptPhaseWeight + + (1 - this.scriptPhaseWeight) * Math.Min(this.completed, this.total) / this.total; + } + else + { + this.Progress = 1; + } + } + } +} diff --git a/src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs b/src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs new file mode 100644 index 00000000..9b26bef5 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs @@ -0,0 +1,132 @@ +// 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.Dtf.Samples.EmbeddedUI +{ + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Threading; + using System.Windows; + using System.Windows.Threading; + using WixToolset.Dtf.WindowsInstaller; + using Application = System.Windows.Application; + + public class SampleEmbeddedUI : IEmbeddedUI + { + private Thread appThread; + private Application app; + private SetupWizard setupWizard; + private ManualResetEvent installStartEvent; + private ManualResetEvent installExitEvent; + + /// + /// Initializes the embedded UI. + /// + /// Handle to the installer which can be used to get and set properties. + /// The handle is only valid for the duration of this method call. + /// Path to the directory that contains all the files from the MsiEmbeddedUI table. + /// On entry, contains the current UI level for the installation. After this + /// method returns, the installer resets the UI level to the returned value of this parameter. + /// True if the embedded UI was successfully initialized; false if the installation + /// should continue without the embedded UI. + /// The installation was canceled by the user. + /// The embedded UI failed to initialize and + /// causes the installation to fail. + public bool Initialize(Session session, string resourcePath, ref InstallUIOptions internalUILevel) + { + if (session != null) + { + if ((internalUILevel & InstallUIOptions.Full) != InstallUIOptions.Full) + { + // Don't show custom UI when the UI level is set to basic. + return false; + + // An embedded UI could display an alternate dialog sequence for reduced or + // basic modes, but it's not implemented here. We'll just fall back to the + // built-in MSI basic UI. + } + + if (String.Equals(session["REMOVE"], "All", StringComparison.OrdinalIgnoreCase)) + { + // Don't show custom UI when uninstalling. + return false; + + // An embedded UI could display an uninstall wizard, it's just not imlemented here. + } + } + + // Start the setup wizard on a separate thread. + this.installStartEvent = new ManualResetEvent(false); + this.installExitEvent = new ManualResetEvent(false); + this.appThread = new Thread(this.Run); + this.appThread.SetApartmentState(ApartmentState.STA); + this.appThread.Start(); + + // Wait for the setup wizard to either kickoff the install or prematurely exit. + int waitResult = WaitHandle.WaitAny(new WaitHandle[] { this.installStartEvent, this.installExitEvent }); + if (waitResult == 1) + { + // The setup wizard set the exit event instead of the start event. Cancel the installation. + throw new InstallCanceledException(); + } + else + { + // Start the installation with a silenced internal UI. + // This "embedded external UI" will handle message types except for source resolution. + internalUILevel = InstallUIOptions.NoChange | InstallUIOptions.SourceResolutionOnly; + return true; + } + } + + /// + /// Processes information and progress messages sent to the user interface. + /// + /// Message type. + /// Record that contains message data. + /// Message box buttons. + /// Message box icon. + /// Message box default button. + /// Result of processing the message. + public MessageResult ProcessMessage(InstallMessage messageType, Record messageRecord, + MessageButtons buttons, MessageIcon icon, MessageDefaultButton defaultButton) + { + // Synchronously send the message to the setup wizard window on its thread. + object result = this.setupWizard.Dispatcher.Invoke(DispatcherPriority.Send, + new Func(delegate() + { + return this.setupWizard.ProcessMessage(messageType, messageRecord, buttons, icon, defaultButton); + })); + return (MessageResult) result; + } + + /// + /// Shuts down the embedded UI at the end of the installation. + /// + /// + /// If the installation was canceled during initialization, this method will not be called. + /// If the installation was canceled or failed at any later point, this method will be called at the end. + /// + public void Shutdown() + { + // Wait for the user to exit the setup wizard. + this.setupWizard.Dispatcher.BeginInvoke(DispatcherPriority.Normal, + new Action(delegate() + { + this.setupWizard.EnableExit(); + })); + this.appThread.Join(); + } + + /// + /// Creates the setup wizard and runs the application thread. + /// + private void Run() + { + this.app = new Application(); + this.setupWizard = new SetupWizard(this.installStartEvent); + this.setupWizard.InitializeComponent(); + this.app.Run(this.setupWizard); + this.installExitEvent.Set(); + } + } +} diff --git a/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml b/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml new file mode 100644 index 00000000..a43059e8 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs b/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs new file mode 100644 index 00000000..b25b8a9e --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs @@ -0,0 +1,111 @@ +// 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.Dtf.Samples.EmbeddedUI +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Windows; + using System.Windows.Controls; + using System.Windows.Data; + using System.Windows.Documents; + using System.Windows.Input; + using System.Windows.Media; + using System.Windows.Media.Imaging; + using System.Windows.Navigation; + using System.Windows.Shapes; + using WixToolset.Dtf.WindowsInstaller; + + /// + /// Interaction logic for SetupWizard.xaml + /// + public partial class SetupWizard : Window + { + private ManualResetEvent installStartEvent; + private InstallProgressCounter progressCounter; + private bool canceled; + + public SetupWizard(ManualResetEvent installStartEvent) + { + this.installStartEvent = installStartEvent; + this.progressCounter = new InstallProgressCounter(0.5); + } + + public MessageResult ProcessMessage(InstallMessage messageType, Record messageRecord, + MessageButtons buttons, MessageIcon icon, MessageDefaultButton defaultButton) + { + try + { + this.progressCounter.ProcessMessage(messageType, messageRecord); + this.progressBar.Value = this.progressBar.Minimum + + this.progressCounter.Progress * (this.progressBar.Maximum - this.progressBar.Minimum); + this.progressLabel.Content = "" + (int) Math.Round(100 * this.progressCounter.Progress) + "%"; + + switch (messageType) + { + case InstallMessage.Error: + case InstallMessage.Warning: + case InstallMessage.Info: + string message = String.Format("{0}: {1}", messageType, messageRecord); + this.LogMessage(message); + break; + } + + if (this.canceled) + { + this.canceled = false; + return MessageResult.Cancel; + } + } + catch (Exception ex) + { + this.LogMessage(ex.ToString()); + this.LogMessage(ex.StackTrace); + } + + return MessageResult.OK; + } + + private void LogMessage(string message) + { + this.messagesTextBox.Text += Environment.NewLine + message; + this.messagesTextBox.ScrollToEnd(); + } + + internal void EnableExit() + { + this.progressBar.Visibility = Visibility.Hidden; + this.progressLabel.Visibility = Visibility.Hidden; + this.cancelButton.Visibility = Visibility.Hidden; + this.exitButton.Visibility = Visibility.Visible; + } + + private void installButton_Click(object sender, RoutedEventArgs e) + { + this.installButton.Visibility = Visibility.Hidden; + this.progressBar.Visibility = Visibility.Visible; + this.progressLabel.Visibility = Visibility.Visible; + this.installStartEvent.Set(); + } + + private void exitButton_Click(object sender, RoutedEventArgs e) + { + this.Close(); + } + + private void cancelButton_Click(object sender, RoutedEventArgs e) + { + if (this.installButton.Visibility == Visibility.Visible) + { + this.Close(); + } + else + { + this.canceled = true; + this.cancelButton.IsEnabled = false; + } + } + } +} -- cgit v1.2.3-55-g6feb