using System; using System.ComponentModel; using System.Drawing; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace Vanara.Windows.Forms { /// /// Multi-level, auto-sizing, progress dialog supporting asyncronous tasks. The background activities are provided as asyncronous methods who have a /// and an instance passed as parameters. The method uses the /// instance to determine if the user has pressed the "Cancel" button and the /// method to report progress. /// /// /// public async Task ShowProgress() /// { /// var progressDialog = new ProgressDialog { Title = "Progress" }; /// await progressDialog.ShowDialog(null, (cancellationToken, progressReporter) => SomeTask(listOfItemsToProcess, cancellationToken, progressReporter)); /// } /// /// private Task SomeTask(List<Item> items, CancellationToken token, IProgress<ProgresEventArgs> reporter) /// { /// return Task.Run(() => { /// for (var i = 0; i < items.Count; i++) /// { /// if (token.IsCancellationRequested) break; /// SomeLongJob(items[i]); /// reporter.Report(new ProgresEventArgs(items[i].ToString(), ProgressDialog.Percent(i, items.Count))); /// } /// }); /// } /// public class ProgressDialog : CommonDialog { private const string defaultCancelText = "&Cancel"; private readonly InternalProgressDialog progressDlg; private readonly Progress update; private CancellationTokenSource cancelToken; private IWin32Window parent; private bool running; /// Initializes a new instance of the class. public ProgressDialog() { progressDlg = new InternalProgressDialog(); progressDlg.Cancelled += (o, a) => cancelToken?.Cancel(); update = new Progress(UpdateProgress); } /// Background task to run when calling without any specified action. /// The background task. [Browsable(false)] public Func, Task> BackgroundTask { get; set; } /// Gets or sets the cancel button text. /// The cancel button text. [DefaultValue(defaultCancelText), Category("Appearance"), Description("The text on the Cancel button.")] [Localizable(true), Bindable(true)] public string CancelButtonText { get => progressDlg.cancelBtn.Text; set => progressDlg.cancelBtn.Text = value; } /// Gets or sets the progress dialog box title. /// The progress dialog box title. The default value is an empty string (""). [DefaultValue(""), Category("Window"), Description("The progress dialog box title.")] [Localizable(true), Bindable(true)] public string Title { get => progressDlg.Text; set => progressDlg.Text = value; } /// Builds an integer percent value. /// The index of the item being processed. /// The total count of the items being processed /// A value with which to pad the starting value. /// The percentage of the way through the count. public static int Percent(int idx, int count, int start = 0) => start + (int)Math.Floor(idx * (float)(100 - start) / count); /// When overridden in a derived class, resets the properties of a common dialog box to their default values. public override void Reset() { if (cancelToken != null && !cancelToken.IsCancellationRequested) cancelToken.Cancel(); if (progressDlg.Visible) progressDlg.Hide(); cancelToken = null; parent = null; running = false; } /// Shows the progress dialog as a modal dialog box with the specified owner while executing the supplied function. /// Any object that implements that represents the top-level window that will own the model dialog box. /// The function whose execution is run in the background of the progress dialog. /// A task that represents the asynchronous operation. /// The value cannot be null. /// Another instance is already running. public async Task ShowDialog(IWin32Window owner, Func, Task> function) { if (function == null) throw new ArgumentNullException(); if (running) throw new InvalidOperationException("Another instance is already running."); try { parent = owner; cancelToken = new CancellationTokenSource(); running = true; await function(cancelToken.Token, update); } finally { Reset(); } } /// Shows the progress dialog as a modal dialog box with the specified owner while executing the supplied function. /// The type of the value returned by . /// Any object that implements that represents the top-level window that will own the model dialog box. /// The function whose execution is run in the background of the progress dialog. /// The task object representing the asynchronous operation. The property on the task object returns the value returned by . /// The value cannot be null. /// Another instance is already running. public async Task ShowDialog(IWin32Window owner, Func, Task> function) { if (function == null) throw new ArgumentNullException(); if (running) throw new InvalidOperationException("Another instance is already running."); try { parent = owner; cancelToken = new CancellationTokenSource(); running = true; return await function(cancelToken.Token, update); } finally { Reset(); } } /// When overridden in a derived class, specifies a common dialog box. /// A value that represents the window handle of the owner window for the common dialog box. /// if the dialog box was successfully run; otherwise, . protected override bool RunDialog(IntPtr hwndOwner) { try { ShowDialog(NativeWindow.FromHandle(hwndOwner), BackgroundTask).Wait(); return true; } catch { return false; } } private void UpdateProgress(ProgressEventArgs p) { if (p.PercentComplete == 100) { if (progressDlg.Visible) progressDlg.Hide(); } else { if (!progressDlg.Visible) progressDlg.ShowDialog(parent); progressDlg.PercentComplete = p.PercentComplete; progressDlg.StatusText = p.StatusText; progressDlg.MacroPercentComplete = p.MacroPercentComplete; progressDlg.MacroStatusText = p.MacroStatusText; } } internal class InternalProgressDialog : Form { internal Button cancelBtn; private TableLayoutPanel commandPanel; private TableLayoutPanel contentPanel; private Panel dividerPanel; private ProgressBar macroProgressBar; private Label macroStatusLabel; private ProgressBar progressBar; private Label statusLabel; /// Initializes a new instance of the class. public InternalProgressDialog() { InitializeComponent(); MaximumSize = new Size(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); } /// Occurs when the Cancel button is pressed. public event CancelEventHandler Cancelled; /// /// Gets or sets the value of the macro progress bar. Valid values are 0 to 100. If this value is 0 and is null or /// empty, the macro items will be hidden. /// /// The macro percent complete. [DefaultValue(0)] public int MacroPercentComplete { get => macroProgressBar.Value; set { macroProgressBar.Value = value; MacroItemsVisible = macroProgressBar.Value != 0 && !string.IsNullOrEmpty(macroStatusLabel.Text); } } /// /// Gets or sets the status text displayed above the macro progress bar. If this value is null or empty and is 0, /// the macro items will be hidden. /// /// The macro status text. [DefaultValue("")] public string MacroStatusText { get => macroStatusLabel.Text; set { macroStatusLabel.Text = value; MacroItemsVisible = macroProgressBar.Value != 0 && !string.IsNullOrEmpty(macroStatusLabel.Text); } } /// Gets or sets the value of the standard progress bar. Valid values are 0 to 100. /// The percent complete. [DefaultValue(0)] public int PercentComplete { get => progressBar.Value; set => progressBar.Value = value; } /// Gets or sets the status text displayed above the standard progress bar. /// The status text. [DefaultValue("")] public string StatusText { get => statusLabel.Text; set => statusLabel.Text = value; } /// Gets or sets a value indicating whether [task visible]. /// true if [task visible]; otherwise, false. private bool MacroItemsVisible { get => macroProgressBar.Visible; set => macroProgressBar.Visible = macroStatusLabel.Visible = value; } /// Raises the event. /// The instance containing the event data. protected virtual void OnCancelled(CancelEventArgs e) { Cancelled?.Invoke(this, e); } /// Raises the event. /// A that contains the event data. protected override void OnFormClosed(FormClosedEventArgs e) { base.OnFormClosed(e); MacroItemsVisible = false; } /// Handles the Click event of the cancelBtn control. /// The source of the event. /// The instance containing the event data. private void CancelBtn_Click(object sender, EventArgs e) { OnCancelled(new CancelEventArgs(true)); } private void InitializeComponent() { progressBar = new ProgressBar(); statusLabel = new Label(); commandPanel = new TableLayoutPanel(); cancelBtn = new Button(); dividerPanel = new Panel(); contentPanel = new TableLayoutPanel(); macroStatusLabel = new Label(); macroProgressBar = new ProgressBar(); commandPanel.SuspendLayout(); contentPanel.SuspendLayout(); SuspendLayout(); // progressBar progressBar.Dock = DockStyle.Top; progressBar.Location = new Point(11, 75); progressBar.Margin = new Padding(0, 7, 0, 0); progressBar.Size = new Size(346, 9); progressBar.TabIndex = 0; // statusLabel statusLabel.AutoSize = true; statusLabel.Dock = DockStyle.Top; statusLabel.Location = new Point(11, 53); statusLabel.Margin = new Padding(0); statusLabel.Size = new Size(346, 15); statusLabel.TabIndex = 1; // commandPanel commandPanel.AutoSize = true; commandPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink; commandPanel.BackColor = SystemColors.Control; commandPanel.ColumnCount = 2; commandPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); commandPanel.ColumnStyles.Add(new ColumnStyle()); commandPanel.Controls.Add(cancelBtn, 1, 1); commandPanel.Controls.Add(dividerPanel, 0, 0); commandPanel.Dock = DockStyle.Top; commandPanel.Location = new Point(0, 95); commandPanel.Margin = new Padding(0); commandPanel.RowCount = 2; commandPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 1F)); commandPanel.RowStyles.Add(new RowStyle()); commandPanel.Size = new Size(368, 46); commandPanel.TabIndex = 6; // cancelBtn cancelBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; cancelBtn.DialogResult = DialogResult.Cancel; cancelBtn.Location = new Point(282, 12); cancelBtn.Margin = new Padding(0, 11, 11, 11); cancelBtn.Size = new Size(75, 23); cancelBtn.TabIndex = 2; cancelBtn.Text = defaultCancelText; cancelBtn.UseVisualStyleBackColor = true; cancelBtn.Click += CancelBtn_Click; // dividerPanel dividerPanel.BackColor = Color.FromArgb(223, 223, 223); commandPanel.SetColumnSpan(dividerPanel, 2); dividerPanel.Dock = DockStyle.Fill; dividerPanel.Location = new Point(0, 0); dividerPanel.Margin = new Padding(0); dividerPanel.Size = new Size(368, 1); dividerPanel.TabIndex = 3; // contentPanel contentPanel.AutoSize = true; contentPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink; contentPanel.ColumnCount = 1; contentPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); contentPanel.Controls.Add(macroStatusLabel, 0, 0); contentPanel.Controls.Add(macroProgressBar, 0, 1); contentPanel.Controls.Add(statusLabel, 0, 2); contentPanel.Controls.Add(progressBar, 0, 3); contentPanel.Dock = DockStyle.Top; contentPanel.Location = new Point(0, 0); contentPanel.Margin = new Padding(0); contentPanel.Padding = new Padding(11); contentPanel.RowCount = 4; contentPanel.RowStyles.Add(new RowStyle()); contentPanel.RowStyles.Add(new RowStyle()); contentPanel.RowStyles.Add(new RowStyle()); contentPanel.RowStyles.Add(new RowStyle()); contentPanel.Size = new Size(368, 95); contentPanel.TabIndex = 7; // macroStatusLabel macroStatusLabel.AutoSize = true; macroStatusLabel.Dock = DockStyle.Top; macroStatusLabel.Location = new Point(11, 11); macroStatusLabel.Margin = new Padding(0); macroStatusLabel.Size = new Size(346, 15); macroStatusLabel.TabIndex = 3; macroStatusLabel.Visible = false; // macroProgressBar macroProgressBar.Dock = DockStyle.Top; macroProgressBar.Location = new Point(11, 33); macroProgressBar.Margin = new Padding(0, 7, 0, 11); macroProgressBar.Size = new Size(346, 9); macroProgressBar.TabIndex = 2; macroProgressBar.Visible = false; // ProgressDialog AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; BackColor = SystemColors.Window; CancelButton = cancelBtn; ClientSize = new Size(368, 141); ControlBox = false; Controls.Add(commandPanel); Controls.Add(contentPanel); Font = new Font("Segoe UI", 9F); FormBorderStyle = FormBorderStyle.FixedSingle; Margin = new Padding(3, 4, 3, 4); MaximizeBox = false; MinimizeBox = false; MinimumSize = new Size(320, 132); ShowIcon = false; ShowInTaskbar = false; StartPosition = FormStartPosition.CenterParent; commandPanel.ResumeLayout(false); contentPanel.ResumeLayout(false); contentPanel.PerformLayout(); ResumeLayout(false); PerformLayout(); } } } /// Updates progress on a . public class ProgressEventArgs : EventArgs { /// Initializes a new instance of the class. /// The status text. /// The percent complete. public ProgressEventArgs(string statusText, int percentComplete) { StatusText = statusText; PercentComplete = percentComplete; } /// Initializes a new instance of the class. /// The macro status text. /// The macro percent complete. /// The status text. /// The percent complete. public ProgressEventArgs(string macroStatusText, int macroPercentComplete, string statusText, int percentComplete) : this(statusText, percentComplete) { MacroStatusText = macroStatusText; MacroPercentComplete = macroPercentComplete; } /// /// Gets or sets the value of the macro progress bar. Valid values are 0 to 100. If this value is 0 and is null or empty, /// the macro items will be hidden. /// /// The macro percent complete. [DefaultValue(0)] public int MacroPercentComplete { get; set; } /// /// Gets or sets the status text displayed above the macro progress bar. If this value is null or empty and is 0, the /// macro items will be hidden. /// /// The macro status text. [DefaultValue("")] public string MacroStatusText { get; set; } /// Gets or sets the value of the standard progress bar. Valid values are 0 to 100. /// The percent complete. [DefaultValue(0)] public int PercentComplete { get; set; } /// Gets or sets the status text displayed above the standard progress bar. /// The status text. [DefaultValue("")] public string StatusText { get; set; } } }