using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Windows.Forms; using Vanara.Interop; namespace AeroWizard { /// /// Button state for buttons controlling the wizard. /// public enum WizardCommandButtonState { /// Button is enabled and can be clicked. Enabled, /// Button is disabled and cannot be clicked. Disabled, /// Button is hidden from the user. Hidden } /// /// Control providing a collection of wizard style navigable pages. /// [Designer(typeof(Design.WizardBaseDesigner))] [ToolboxItem(true), ToolboxBitmap(typeof(WizardControl), "WizardControl.bmp")] [Description("Provides a container for wizard pages.")] [DefaultProperty("Pages"), DefaultEvent("SelectedPageChanged")] public class WizardPageContainer : ContainerControl, ISupportInitialize { private string finishBtnText; private bool initialized; private bool initializing; private bool nextButtonShieldEnabled; private string nextBtnText; private readonly Stack pageHistory; private Timer progressTimer; private WizardPage selectedPage; private bool showProgressInTaskbarIcon; private NativeMethods.ITaskbarList4 taskbar; private ButtonBase backButton, cancelButton, nextButton; /// /// Initializes a new instance of the class. /// public WizardPageContainer() { pageHistory = new Stack(); Pages = new WizardPageCollection(this); Pages.ItemAdded += Pages_AddItem; Pages.ItemDeleted += Pages_RemoveItem; Pages.Reset += Pages_Reset; OnRightToLeftChanged(EventArgs.Empty); // Get localized defaults for button text ResetBackButtonText(); ResetCancelButtonText(); ResetFinishButtonText(); ResetNextButtonText(); } /// /// Occurs when the button's state has changed. /// [Category("Property Changed"), Description("Occurs when any of the button's state has changed.")] public event EventHandler ButtonStateChanged; /// /// Occurs when the user clicks the Cancel button and allows for programmatic cancellation. /// [Category("Behavior"), Description("Occurs when the user clicks the Cancel button and allows for programmatic cancellation.")] public event CancelEventHandler Cancelling; /// /// Occurs when the user clicks the Next/Finish button and the page is set to or this is the last page in the collection. /// [Category("Behavior"), Description("Occurs when the user clicks the Next/Finish button on last page.")] public event EventHandler Finished; /// /// Occurs when the property has changed. /// [Category("Property Changed"), Description("Occurs when the SelectedPage property has changed.")] public event EventHandler SelectedPageChanged; /// /// Gets or sets the button assigned to control backing up through the pages. /// /// The back button control. [Category("Wizard"), Description("Button used to command backward wizard flow.")] public ButtonBase BackButton { get { return backButton; } set { if (backButton != null) backButton.Click -= backButton_Click; if (value != null) { backButton = value; backButton.Click += backButton_Click; } } } /// /// Gets or sets the state of the back button. /// /// The state of the back button. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public WizardCommandButtonState BackButtonState { get { return GetCmdButtonState(BackButton); } internal set { SetCmdButtonState(BackButton, value); } } /// /// Gets or sets the back button text. /// /// The cancel button text. [Category("Wizard"), Localizable(true), Description("The back button text")] public string BackButtonText { get { return GetCmdButtonText(BackButton); } set { SetCmdButtonText(BackButton, value); } } /// /// Gets or sets the button assigned to canceling the page flow. /// /// The cancel button control. [Category("Wizard"), Description("Button used to cancel wizard flow.")] public ButtonBase CancelButton { get { return cancelButton; } set { if (cancelButton != null) cancelButton.Click -= cancelButton_Click; if (value != null) { cancelButton = value; cancelButton.Click += cancelButton_Click; } } } /// /// Gets the state of the cancel button. /// /// The state of the cancel button. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public WizardCommandButtonState CancelButtonState { get { return GetCmdButtonState(CancelButton); } internal set { SetCmdButtonState(CancelButton, value); } } /// /// Gets or sets the cancel button text. /// /// The cancel button text. [Category("Wizard"), Localizable(true), Description("The cancel button text")] public string CancelButtonText { get { return GetCmdButtonText(CancelButton); } set { SetCmdButtonText(CancelButton, value); } } /// /// Gets or sets the finish button text. /// /// The finish button text. [Category("Wizard"), Localizable(true), Description("The finish button text")] public string FinishButtonText { get { return finishBtnText; } set { finishBtnText = value; if (selectedPage != null && selectedPage.IsFinishPage && !this.IsDesignMode()) { SetCmdButtonText(NextButton, value); } } } /// /// Gets or sets the button assigned to control moving forward through the pages. /// /// The next button control. [Category("Wizard"), Description("Button used to command forward wizard flow.")] public ButtonBase NextButton { get { return nextButton; } set { if (nextButton != null) nextButton.Click -= nextButton_Click; if (value != null) { nextButton = value; nextButton.Click += nextButton_Click; } } } /// /// Gets or sets the shield icon on the next button. /// /// true if Next button should display a shield; otherwise, false. /// Setting a UAF shield on a button only works on Vista and later versions of Windows. [DefaultValue(false), Category("Wizard"), Description("Show a shield icon on the next button")] public Boolean NextButtonShieldEnabled { get { return nextButtonShieldEnabled; } set { if (nextButtonShieldEnabled != value) { nextButtonShieldEnabled = value; NextButton.SetElevationRequiredState(value); } } } /// /// Gets the state of the next button. /// /// The state of the next button. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public WizardCommandButtonState NextButtonState { get { return GetCmdButtonState(NextButton); } internal set { SetCmdButtonState(NextButton, value); } } /// /// Gets or sets the next button text. /// /// The next button text. [Category("Wizard"), Localizable(true), Description("The next button text.")] public string NextButtonText { get { return nextBtnText; } set { nextBtnText = value; if (!this.IsDesignMode() && (selectedPage == null || !selectedPage.IsFinishPage)) { SetCmdButtonText(NextButton, value); } } } /// /// Gets the collection of wizard pages in this wizard control. /// /// The that contains the objects in this . [Category("Wizard"), Description("Collection of wizard pages.")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public WizardPageCollection Pages { get; } /// /// Gets how far the wizard has progressed, as a percentage. /// /// A value between 0 and 100. [Browsable(false), Description("The percentage of the current page against all pages at run-time.")] public virtual short PercentComplete { get { var pg = SelectedPage; if (pg == null) return 0; if (IsLastPage(pg)) return 100; return Convert.ToInt16(Math.Ceiling(((double)Pages.IndexOf(SelectedPage) + 1) * 100f / Pages.Count)); } } /// /// Gets the currently selected wizard page. /// /// The selected wizard page. null if no page is active. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual WizardPage SelectedPage { get { if (selectedPage == null || Pages.Count == 0) return null; return selectedPage; } internal set { if (value != null && !Pages.Contains(value)) throw new ArgumentException("WizardPage is not in the Pages collection for the control."); System.Diagnostics.Debug.WriteLine($"SelectPage: New={(value == null ? "null" : value.Name)},Prev={(selectedPage == null ? "null" : selectedPage.Name)}"); if (value != selectedPage) { var prev = selectedPage; selectedPage?.Hide(); selectedPage = value; var idx = SelectedPageIndex; if (!this.IsDesignMode()) { while (selectedPage != null && idx < Pages.Count - 1 && selectedPage.Suppress) selectedPage = Pages[++idx]; } if (selectedPage != null) { //this.HeaderText = selectedPage.Text; selectedPage.InitializePage(prev); selectedPage.Dock = DockStyle.Fill; selectedPage.PerformLayout(); selectedPage.Show(); selectedPage.BringToFront(); selectedPage.Focus(); } UpdateUIDependencies(); OnSelectedPageChanged(); } } } /// /// Gets or sets a value indicating whether to show progress in form's taskbar icon. /// /// /// This will only work on Windows 7 or later and the parent form must be showing its icon in the taskbar. No exception is thrown on failure. /// /// /// true to show progress in taskbar icon; otherwise, false. /// [Category("Wizard"), DefaultValue(false), Description("Indicates whether to show progress in form's taskbar icon")] public bool ShowProgressInTaskbarIcon { get { return showProgressInTaskbarIcon; } set { if (RunningOnWin7) { showProgressInTaskbarIcon = value; UpdateTaskbarProgress(); } } } /// /// Gets a value indicating whether running on win7. /// /// true if [running on win7]; otherwise, false. private static bool RunningOnWin7 => (Environment.OSVersion.Platform == PlatformID.Win32NT) && (Environment.OSVersion.Version.CompareTo(new Version(6, 1)) >= 0); /// /// Gets the task bar interface for the current form. /// /// The task bar. private NativeMethods.ITaskbarList4 TaskBar { get { if (taskbar == null) { taskbar = (NativeMethods.ITaskbarList4)new NativeMethods.CTaskbarList(); taskbar.HrInit(); if (ParentForm != null) taskbar.SetProgressState(ParentForm.Handle, NativeMethods.TBPF.NORMAL); } return taskbar; } } [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] internal bool DesignerSelected { get; set; } /// /// Gets the index of the currently selected page. /// /// The index of the selected page. internal int SelectedPageIndex { get { if (selectedPage == null) return -1; return Pages.IndexOf(selectedPage); } } /// /// Signals the object that initialization is starting. /// public void BeginInit() { initializing = true; } /// /// Signals the object that initialization is complete. /// public void EndInit() { initializing = false; } /// /// Advances to the specified page. /// /// The wizard page to go to next. /// if set to true skip event. /// When specifying a value for nextPage, it must already be in the Pages collection. public virtual void NextPage(WizardPage nextPage = null, bool skipCommit = false) { if (this.IsDesignMode()) { var idx = SelectedPageIndex; if (idx < Pages.Count - 1) SelectedPage = Pages[idx + 1]; return; } if (skipCommit || SelectedPage.CommitPage()) { if (nextPage != null) { if (!Pages.Contains(nextPage)) throw new ArgumentException(@"When specifying a value for nextPage, it must already be in the Pages collection.", nameof(nextPage)); pageHistory.Push(SelectedPage); SelectedPage = nextPage; } else { var selNext = GetNextPage(SelectedPage); // Check for last page if (SelectedPage.IsFinishPage || selNext == null) { OnFinished(); return; } // Set new SelectedPage value pageHistory.Push(SelectedPage); SelectedPage = selNext; } } } /// /// Returns to the previous page. /// public virtual void PreviousPage() { if (this.IsDesignMode()) { var idx = SelectedPageIndex; if (idx > 0) SelectedPage = Pages[idx - 1]; return; } if (SelectedPage.RollbackPage()) SelectedPage = pageHistory.Pop(); } /// /// Restarts the wizard pages from the first page. /// public void RestartPages() { initialized = false; InitialSetup(); } /// /// Raises the event. /// protected virtual void OnCancelling() { var arg = new CancelEventArgs(true); Cancelling?.Invoke(this, arg); } /// /// Raises the event. /// protected virtual void OnFinished() { Finished?.Invoke(this, EventArgs.Empty); } /// /// Raises the event. /// /// An that contains the event data. protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); selectedPage?.Focus(); } /// /// Raises the event. /// /// An that contains the event data. protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); InitialSetup(); } /// /// Raises the event. /// protected void OnSelectedPageChanged() { SelectedPageChanged?.Invoke(this, EventArgs.Empty); } /// /// Updates the buttons and taskbar according to current sequence and history. /// protected internal void UpdateUIDependencies() { System.Diagnostics.Debug.WriteLine($"UpdBtn: hstCnt={pageHistory.Count},pgIdx={SelectedPageIndex}:{Pages.Count},isFin={selectedPage != null && selectedPage.IsFinishPage}"); if (selectedPage == null) { CancelButtonState = this.IsDesignMode() ? WizardCommandButtonState.Disabled : WizardCommandButtonState.Enabled; NextButtonState = BackButtonState = WizardCommandButtonState.Hidden; } else { if (this.IsDesignMode()) { CancelButtonState = WizardCommandButtonState.Disabled; NextButtonState = SelectedPageIndex == Pages.Count - 1 ? WizardCommandButtonState.Disabled : WizardCommandButtonState.Enabled; BackButtonState = SelectedPageIndex <= 0 ? WizardCommandButtonState.Disabled : WizardCommandButtonState.Enabled; } else { CancelButtonState = selectedPage.ShowCancel ? (selectedPage.AllowCancel && !this.IsDesignMode() ? WizardCommandButtonState.Enabled : WizardCommandButtonState.Disabled) : WizardCommandButtonState.Hidden; NextButtonState = selectedPage.ShowNext ? (selectedPage.AllowNext ? WizardCommandButtonState.Enabled : WizardCommandButtonState.Disabled) : WizardCommandButtonState.Hidden; if (selectedPage.IsFinishPage || IsLastPage(SelectedPage)) SetCmdButtonText(NextButton, FinishButtonText); else SetCmdButtonText(NextButton, NextButtonText); BackButtonState = pageHistory.Count == 0 || !selectedPage.AllowBack ? WizardCommandButtonState.Disabled : WizardCommandButtonState.Enabled; UpdateTaskbarProgress(); } } if (Controls.ContainsKey("stepList")) Controls["stepList"].Refresh(); } private void backButton_Click(object sender, EventArgs e) { PreviousPage(); } private void cancelButton_Click(object sender, EventArgs e) { OnCancelling(); } internal WizardCommandButtonState GetCmdButtonState(ButtonBase btn) { if (btn?.Tag == null) return WizardCommandButtonState.Hidden; if (btn.Tag is WizardCommandButtonState) return (WizardCommandButtonState)btn.Tag; if (btn.Enabled) return WizardCommandButtonState.Enabled; if (!btn.Visible) return WizardCommandButtonState.Hidden; return WizardCommandButtonState.Disabled; } private string GetCmdButtonText(ButtonBase btn) => btn == null ? string.Empty : btn.Text; private WizardPage GetNextPage(WizardPage page) { if (page == null || page.IsFinishPage) return null; do { var pgIdx = Pages.IndexOf(page); if (page.NextPage != null) page = page.NextPage; else if (pgIdx == Pages.Count - 1) page = null; else page = Pages[pgIdx + 1]; } while (page != null && page.Suppress); return page; } private void InitialSetup() { if (!initialized) { pageHistory.Clear(); selectedPage = null; var firstPage = Pages.Find(p => !p.Suppress); if (firstPage != null) SelectedPage = firstPage; if (selectedPage == null) UpdateUIDependencies(); if (showProgressInTaskbarIcon) { progressTimer = new Timer() { Interval = 1000, Enabled = true }; progressTimer.Tick += progressTimer_Tick; } initialized = true; } } private bool IsLastPage(WizardPage page) => GetNextPage(page) == null; private void nextButton_Click(object sender, EventArgs e) { NextPage(); } private void Pages_AddItem(object sender, EventedList.ListChangedEventArgs e) { Pages_AddItemHandler(e.Item, !initializing); } private void Pages_AddItemHandler(WizardPage item, bool selectAfterAdd) { System.Diagnostics.Debug.WriteLine($"AddPage: {(item == null ? "null" : item.Text)},sel={selectAfterAdd}"); item.Owner = this; item.Visible = false; if (!Contains(item)) Controls.Add(item); if (selectAfterAdd) SelectedPage = item; } private void Pages_RemoveItem(object sender, EventedList.ListChangedEventArgs e) { Controls.Remove(e.Item); if (e.Item == selectedPage && Pages.Count > 0) SelectedPage = Pages[e.ItemIndex == Pages.Count ? e.ItemIndex - 1 : e.ItemIndex]; else UpdateUIDependencies(); } private void Pages_Reset(object sender, EventedList.ListChangedEventArgs e) { var curPage = selectedPage; SelectedPage = null; Controls.Clear(); foreach (var item in Pages) Pages_AddItemHandler(item, false); if (Pages.Count > 0) SelectedPage = Pages.Contains(curPage) ? curPage : Pages[0]; } private void progressTimer_Tick(object sender, EventArgs e) { if (ParentForm != null && ParentForm.Visible) { progressTimer.Enabled = false; progressTimer = null; UpdateTaskbarProgress(); } } private void UpdateTaskbarProgress() { if (showProgressInTaskbarIcon && selectedPage != null && Pages.Count > 0 && !this.IsDesignMode() && ParentForm != null && ParentForm.ShowInTaskbar) TaskBar.SetProgressValue(ParentForm.Handle, Convert.ToUInt64(PercentComplete), 100ul); } internal void ResetBackButtonText() { BackButtonText = Properties.Resources.WizardBackText; } internal void ResetCancelButtonText() { CancelButtonText = Properties.Resources.WizardCancelText; } internal void ResetFinishButtonText() { FinishButtonText = Properties.Resources.WizardFinishText; } internal void ResetNextButtonText() { NextButtonText = Properties.Resources.WizardNextText; } private void SetCmdButtonState(ButtonBase btn, WizardCommandButtonState value) { if (btn == null) return; var prevVal = GetCmdButtonState(btn); switch (value) { case WizardCommandButtonState.Disabled: btn.Enabled = false; btn.Visible = true; break; case WizardCommandButtonState.Hidden: btn.Enabled = false; if (btn != BackButton) btn.Visible = false; break; case WizardCommandButtonState.Enabled: btn.Enabled = true; btn.Visible = true; break; default: throw new ArgumentOutOfRangeException(nameof(value), value, null); } if (prevVal != value) { btn.Tag = value; ButtonStateChanged?.Invoke(btn, EventArgs.Empty); } Invalidate(); } private void SetCmdButtonText(ButtonBase btn, string text) { if (btn == null) return; btn.Text = text; Invalidate(); } internal bool ShouldSerializeBackButtonText() => BackButtonText != Properties.Resources.WizardBackText; internal bool ShouldSerializeCancelButtonText() => CancelButtonText != Properties.Resources.WizardCancelText; internal bool ShouldSerializeFinishButtonText() => FinishButtonText != Properties.Resources.WizardFinishText; internal bool ShouldSerializeNextButtonText() => NextButtonText != Properties.Resources.WizardNextText; } }