|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using TASKDIALOGCONFIG_FooterIcon = Windows.Win32.UI.Controls.TASKDIALOGCONFIG._Anonymous2_e__Union;
using TASKDIALOGCONFIG_MainIcon = Windows.Win32.UI.Controls.TASKDIALOGCONFIG._Anonymous1_e__Union;
namespace System.Windows.Forms;
/// <summary>
/// Represents a page of content of a task dialog.
/// </summary>
/// <remarks>
/// <para>
/// It is possible to navigate a task dialog while it is shown by invoking the
/// <see cref="Navigate(TaskDialogPage)"/> method with a target <see cref="TaskDialogPage"/>
/// instance.
/// </para>
/// </remarks>
public class TaskDialogPage
{
/// <summary>
/// The start ID for custom buttons.
/// </summary>
/// <remarks>
/// <para>
/// We need to ensure we don't use a ID that is already used for a
/// standard button (TaskDialogResult), so we start with 100 to be safe
/// (100 is also used as first ID in MSDN examples for the task dialog).
/// </para>
/// </remarks>
private const int CustomButtonStartID = 100;
/// <summary>
/// The start ID for radio buttons.
/// </summary>
/// <remarks>
/// <para>
/// This must be at least 1 because 0 already stands for "no button".
/// </para>
/// </remarks>
private const int RadioButtonStartID = 1;
private TaskDialogButtonCollection _buttons;
private TaskDialogRadioButtonCollection _radioButtons;
private TaskDialogVerificationCheckBox? _checkBox;
private TaskDialogExpander? _expander;
private TaskDialogFootnote? _footnote;
private TaskDialogProgressBar? _progressBar;
private TASKDIALOG_FLAGS _flags;
private TaskDialogIcon? _icon;
private string? _caption;
private string? _heading;
private string? _text;
private bool _boundIconIsFromHandle;
private TaskDialogButton[]? _boundCustomButtons;
private Dictionary<int, TaskDialogButton>? _boundStandardButtonsByID;
private bool _appliedInitialization;
private bool _updateHeadingOnInitialization;
private bool _updateTextOnInitialization;
/// <summary>
/// Occurs after this instance is bound to a task dialog and the task dialog
/// has created the GUI elements represented by this <see cref="TaskDialogPage"/> instance.
/// </summary>
/// <remarks>
/// <para>
/// This will happen after showing or navigating the dialog.
/// </para>
/// <para>
/// When this event occurs, the <see cref="BoundDialog"/> property will return
/// the <see cref="TaskDialog"/> instance which this page is bound to.
/// </para>
/// </remarks>
public event EventHandler? Created;
/// <summary>
/// Occurs when the task dialog is about to destroy the GUI elements represented
/// by this <see cref="TaskDialogPage"/> instance and it is about to be
/// unbound from the task dialog.
/// </summary>
/// <remarks>
/// <para>
/// This will happen when closing or navigating the dialog.
/// </para>
/// <para>
/// After this event occurs, the <see cref="BoundDialog"/> property will return
/// <see langword="null"/>.
/// </para>
/// </remarks>
public event EventHandler? Destroyed;
/// <summary>
/// Occurs when the user presses F1 while the task dialog has focus, or when the
/// user clicks the <see cref="TaskDialogButton.Help"/> button.
/// </summary>
public event EventHandler? HelpRequest;
/// <summary>
/// Initializes a new instance of the <see cref="TaskDialogPage"/> class.
/// </summary>
public TaskDialogPage()
{
_buttons = [];
_radioButtons = [];
// Create empty (hidden) controls.
_checkBox = new TaskDialogVerificationCheckBox();
_expander = new TaskDialogExpander();
_footnote = new TaskDialogFootnote();
_progressBar = new TaskDialogProgressBar(TaskDialogProgressBarState.None);
}
/// <summary>
/// Gets or sets the collection of push buttons
/// to be shown in this page.
/// </summary>
/// <value>
/// The collection of custom buttons to be shown in this page.
/// </value>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public TaskDialogButtonCollection Buttons
{
get => _buttons;
set
{
// We must deny this if we are bound because we need to be able to
// access the controls from the task dialog's callback.
DenyIfBound();
_buttons = value.OrThrowIfNull();
}
}
/// <summary>
/// Gets or sets the default button in the task dialog.
/// </summary>
/// <value>
/// The default button in the task dialog.
/// </value>
public TaskDialogButton? DefaultButton { get; set; }
/// <summary>
/// Gets or sets the collection of radio buttons
/// to be shown in this page.
/// </summary>
/// <value>
/// The collection of radio buttons to be shown in this page.
/// </value>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public TaskDialogRadioButtonCollection RadioButtons
{
get => _radioButtons;
set
{
// We must deny this if we are bound because we need to be able to
// access the controls from the task dialog's callback.
DenyIfBound();
_radioButtons = value.OrThrowIfNull();
}
}
/// <summary>
/// Gets or sets the verification checkbox to be shown in this page.
/// </summary>
/// <remarks>
/// <para>
/// The verification checkbox will only be shown if its <see cref="TaskDialogVerificationCheckBox.Text"/>
/// property is not <see langword="null"/> or an empty string.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public TaskDialogVerificationCheckBox? Verification
{
get => _checkBox;
set
{
// We must deny this if we are bound because we need to be able to
// access the control from the task dialog's callback.
DenyIfBound();
_checkBox = value;
}
}
/// <summary>
/// Gets or sets the dialog expander to be shown in this page.
/// </summary>
/// <remarks>
/// <para>
/// The expander button (and the expanded area) will only be shown if its
/// <see cref="TaskDialogExpander.Text"/> property is not <see langword="null"/> or an empty string.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public TaskDialogExpander? Expander
{
get => _expander;
set
{
// We must deny this if we are bound because we need to be able to
// access the control from the task dialog's callback.
DenyIfBound();
_expander = value;
}
}
/// <summary>
/// Gets or sets the footnote to be shown in this page.
/// </summary>
/// <remarks>
/// <para>
/// The footnote will only be shown if its <see cref="TaskDialogFootnote.Text"/> property
/// is not <see langword="null"/> or an empty string.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public TaskDialogFootnote? Footnote
{
get => _footnote;
set
{
// We must deny this if we are bound because we need to be able to
// access the control from the task dialog's callback.
DenyIfBound();
_footnote = value;
}
}
/// <summary>
/// Gets or sets the progress bar to be shown in this page.
/// </summary>
/// <remarks>
/// <para>
/// The progress bar will only be shown if its <see cref="TaskDialogProgressBar.State"/>
/// property is not <see cref="TaskDialogProgressBarState.None"/>.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public TaskDialogProgressBar? ProgressBar
{
get => _progressBar;
set
{
// We must deny this if we are bound because we need to be able to
// access the control from the task dialog's callback.
DenyIfBound();
_progressBar = value;
}
}
/// <summary>
/// Gets or sets the text to display in the title bar of the task dialog.
/// </summary>
/// <remarks>
/// <para>
/// This property can be set while the dialog is shown.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and the task dialog has started navigating to this page instance,
/// but the <see cref="Created"/> event has not been raised yet.
/// - or -
/// The property is set on a page instance that is currently bound to a task dialog, but the dialog
/// has just started navigating to a different page.
/// </exception>
public string? Caption
{
get => _caption;
set
{
DenyIfWaitingForInitialization();
// Note: We set the field values after calling the method to ensure
// it still has the previous value it the method throws.
BoundDialog?.UpdateCaption(value);
_caption = value;
}
}
/// <summary>
/// Gets or sets the heading (main instruction).
/// </summary>
/// <remarks>
/// <para>
/// This property can be set while the dialog is shown.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set on a page instance that is currently bound to a task dialog, but the dialog
/// has just started navigating to a different page.
/// </exception>
public string? Heading
{
get => _heading;
set
{
if (BoundDialog is not null)
{
// If we are bound but waiting for initialization (e.g. immediately after
// starting a navigation), we buffer the change until we apply the
// initialization (when navigation is finished).
if (WaitingForInitialization)
{
_updateHeadingOnInitialization = true;
}
else
{
BoundDialog.UpdateTextElement(TASKDIALOG_ELEMENTS.TDE_MAIN_INSTRUCTION, value);
}
}
_heading = value;
}
}
/// <summary>
/// Gets or sets the dialog's primary text content.
/// </summary>
/// <remarks>
/// <para>
/// This property can be set while the dialog is shown.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set on a page instance that is currently bound to a task dialog, but the dialog
/// has just started navigating to a different page.
/// </exception>
public string? Text
{
get => _text;
set
{
if (BoundDialog is not null)
{
if (WaitingForInitialization)
{
_updateTextOnInitialization = true;
}
else
{
BoundDialog.UpdateTextElement(TASKDIALOG_ELEMENTS.TDE_CONTENT, value);
}
}
_text = value;
}
}
/// <summary>
/// Gets or sets the main icon.
/// </summary>
/// <remarks>
/// <para>
/// This property can be set while the dialog is shown, however, it
/// cannot be switched between instances created from an
/// <see cref="Drawing.Icon"/> (or from a handle pointer)
/// and standard icon instances.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and the task dialog has started navigating to this page instance,
/// but the <see cref="Created"/> event has not been raised yet.
/// - or -
/// The property is set on a page instance that is currently bound to a task dialog, but the dialog
/// has just started navigating to a different page.
/// </exception>
public unsafe TaskDialogIcon? Icon
{
get => _icon;
set
{
// We currently don't support to buffer changes while waiting for the
// initialization like it is done for string properties (Text, Heading),
// because for handle icons, this would mean that the previous icon cannot
// yet be disposed (until initialization is applied) even though the property
// has been updated to a different icon.
// It would be possible to initially specify a NULL icon in the TASKDIALOFCONFIG
// and then update to the actual icon in ApplyInitialization() to avoid this, but
// that seems like an overkill.
DenyIfWaitingForInitialization();
if (BoundDialog is not null)
{
(TASKDIALOGCONFIG_MainIcon icon, bool? iconIsFromHandle) =
GetMainIconValue(value);
// The native task dialog icon cannot be updated from a handle
// type to a non-handle type and vice versa, so we need to throw
// throw in such a case.
if (iconIsFromHandle is not null && iconIsFromHandle != _boundIconIsFromHandle)
{
throw new InvalidOperationException(SR.TaskDialogCannotUpdateIconType);
}
BoundDialog.UpdateIconElement(
TASKDIALOG_ICON_ELEMENTS.TDIE_ICON_MAIN,
_boundIconIsFromHandle ? icon.hMainIcon : (IntPtr)(char*)icon.pszMainIcon);
}
_icon = value;
}
}
/// <summary>
/// Gets or sets a value that indicates whether the task dialog can be closed with
/// <see cref="TaskDialogButton.Cancel"/> as resulting button by pressing ESC or Alt+F4
/// or by clicking the title bar's close button, even if a <see cref="TaskDialogButton.Cancel"/>
/// button isn't added to the <see cref="Buttons"/> collection.
/// </summary>
/// <value>
/// <see langword="true"/> to allow to close the dialog by pressing ESC or Alt+F4 or by clicking
/// the title bar's close button; otherwise, <see langword="false"/>. The default value is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// You can intercept cancellation of the dialog without displaying a "Cancel"
/// button by adding the <see cref="TaskDialogButton.Cancel"/> button that has its
/// <see cref="TaskDialogButton.Visible"/> property set to <see langword="false"/>.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public bool AllowCancel
{
get => GetFlag(TASKDIALOG_FLAGS.TDF_ALLOW_DIALOG_CANCELLATION);
set => SetFlag(TASKDIALOG_FLAGS.TDF_ALLOW_DIALOG_CANCELLATION, value);
}
/// <summary>
/// Gets or sets a value that indicates whether text and controls are displayed
/// reading right to left.
/// </summary>
/// <value>
/// <see langword="true"/> to display text and controls reading right to left; <see langword="false"/>
/// to display controls reading left to right. The default value is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// Note that once a task dialog has been opened with or has navigated to a
/// <see cref="TaskDialogPage"/> where this flag is set, it will keep on
/// subsequent navigations to a new <see cref="TaskDialogPage"/> even when
/// it doesn't have this flag set.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public bool RightToLeftLayout
{
get => GetFlag(TASKDIALOG_FLAGS.TDF_RTL_LAYOUT);
set => SetFlag(TASKDIALOG_FLAGS.TDF_RTL_LAYOUT, value);
}
/// <summary>
/// Gets or sets a value that indicates whether the task dialog can be minimized
/// when it is shown modeless.
/// </summary>
/// <value>
/// <see langword="true"/> to specify that the task dialog can be minimized; otherwise, <see langword="false"/>.
/// The default value is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// When setting this property to <see langword="true"/>, <see cref="AllowCancel"/> is
/// automatically implied.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public bool AllowMinimize
{
get => GetFlag(TASKDIALOG_FLAGS.TDF_CAN_BE_MINIMIZED);
set => SetFlag(TASKDIALOG_FLAGS.TDF_CAN_BE_MINIMIZED, value);
}
/// <summary>
/// Indicates that the width of the task dialog is determined by the width
/// of its content area (similar to Message Box sizing behavior).
/// </summary>
/// <value>
/// <see langword="true"/> to determine the width of the task dialog by the width of
/// its content area; otherwise, <see langword="false"/>. The default value is <see langword="false"/>.
/// </value>
/// <exception cref="InvalidOperationException">
/// The property is set and this page instance is currently bound to a task dialog.
/// </exception>
public bool SizeToContent
{
get => GetFlag(TASKDIALOG_FLAGS.TDF_SIZE_TO_CONTENT);
set => SetFlag(TASKDIALOG_FLAGS.TDF_SIZE_TO_CONTENT, value);
}
/// <summary>
/// <para>
/// Gets or sets a value that specifies whether the task dialog should
/// interpret strings in the form <c><a href="target">link Text</a></c>
/// as hyperlink when specified in the <see cref="Text"/>,
/// <see cref="TaskDialogExpander.Text"/>,
/// or <see cref="TaskDialogFootnote.Text"/> properties.
/// When the user clicks on such a link, the <see cref="LinkClicked"/>
/// event is raised, containing the value of the <c>target</c> attribute.
/// </para>
/// </summary>
/// <value>
/// <see langword="true"/> to enable links; otherwise, <see langword="false"/>.
/// The default value is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// The Task Dialog will not actually execute any links.
/// Link execution must be handled in the <see cref="LinkClicked"/> event.
/// </para>
/// <para>
/// Note: Enabling this setting causes the <c>"&"</c> character to be
/// interpreted as a prefix for an access key character (mnemonic) if at least
/// one link is used. To show a literal <c>"&"</c> character, it must be escaped
/// as <c>"&&"</c>.
/// </para>
/// <para>
/// When you enable this setting and you want to display text
/// without interpreting links, you must replace the strings <c>"<a"</c>
/// and <c>"<A"</c> with something like <c>"<\u200Ba"</c>.
/// </para>
/// </remarks>
public bool EnableLinks { get; set; }
/// <summary>
/// Occurs when the user has clicked on a link.
/// </summary>
/// <remarks>
/// <para>
/// This event will only be raised if <see cref="EnableLinks"/> is set to <see langword="true"/>.
/// </para>
/// </remarks>
public event EventHandler<TaskDialogLinkClickedEventArgs>? LinkClicked;
/// <summary>
/// Gets the <see cref="TaskDialog"/> instance which this page
/// is currently bound to.
/// </summary>
/// <value>
/// The <see cref="TaskDialog"/> instance which this page is bound to, or <see langword="null"/>
/// if this page is not currently bound.
/// </value>
/// <remarks>
/// <para>
/// A page will be bound while it is being displayed, which is indicated by the events
/// <see cref="Created"/> and <see cref="Destroyed"/>.
/// </para>
/// <para>
/// While a page is bound to a task dialog, you cannot show that page instance using a
/// different <see cref="TaskDialog"/> instance at the same time.
/// </para>
/// </remarks>
public TaskDialog? BoundDialog { get; private set; }
/// <summary>
/// Gets a value that indicates if the <see cref="BoundDialog"/>
/// started navigation to this page but navigation did not yet complete
/// (in which case we cannot modify the dialog even though we are bound).
/// </summary>
internal bool WaitingForInitialization => BoundDialog is not null && !_appliedInitialization;
/// <summary>
/// Shows the new content in the current task dialog.
/// <paramref name="page"/>.
/// </summary>
/// <param name="page">
/// The page instance that contains the contents which this task dialog will display.
/// </param>
/// <remarks>
/// <para>
/// During the navigation the dialog will recreate the dialog from the specified
/// <paramref name="page"/> and its controls, and unbind and destroy the currently shown page.
/// </para>
/// <para>
/// You can't manipulate the page or its controls
/// immediately after navigating the dialog (except for calling
/// <see cref="TaskDialog.Close"/> or navigating the dialog again).
/// You will need to wait for the <see cref="Created"/>
/// event to occur before you can manipulate the page or its controls.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException">
/// <paramref name="page"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="InvalidOperationException">
/// The page instance is not currently bound to a dialog, <see cref="BoundDialog"/> is <see langword="null"/>.
/// - or -
/// This page instance contains an invalid configuration.
/// - or -
/// This method is called from within the <see cref="TaskDialogRadioButton.CheckedChanged"/> event
/// of one of the radio buttons of the current task dialog.
/// - or -
/// The task dialog has already been closed.
/// </exception>
public void Navigate(TaskDialogPage page)
{
ArgumentNullException.ThrowIfNull(page);
if (BoundDialog is null)
{
throw new InvalidOperationException(SR.TaskDialogCannotNavigateWithoutDialog);
}
BoundDialog.Navigate(page);
}
internal static bool IsNativeStringNullOrEmpty([NotNullWhen(false)] string? str)
{
// From a native point of view, the string is empty if its first
// character is a NUL char.
return string.IsNullOrEmpty(str) || str[0] == '\0';
}
internal static unsafe (TASKDIALOGCONFIG_MainIcon iconUnion, bool? iconIsFromHandle)
GetMainIconValue(TaskDialogIcon? icon)
{
TASKDIALOGCONFIG_MainIcon iconUnion = default;
bool? iconIsFromHandle = null;
// If no icon is specified (icon is null), the iconIsFromHandle variable will be
// null, which allows to clear the dialog's icon while it is shown, regardless
// of whether the bound icon is a handle icon or a non-handle icon.
if (icon?.IsHandleIcon == true)
{
iconUnion.hMainIcon = (HICON)icon.IconHandle;
iconIsFromHandle = true;
}
else if (icon?.IsStandardIcon == true)
{
// Convert the value to an ushort before converting it to a pointer,
// which corresponds to using the MAKEINTRESOURCEW macro in native code.
iconUnion.pszMainIcon = (char*)checked((ushort)icon.StandardIcon);
iconIsFromHandle = false;
}
return (iconUnion, iconIsFromHandle);
}
internal static unsafe (TASKDIALOGCONFIG_FooterIcon iconUnion, bool? iconIsFromHandle)
GetFooterIconValue(TaskDialogIcon? icon)
{
TASKDIALOGCONFIG_FooterIcon iconUnion = default;
bool? iconIsFromHandle = null;
// If no icon is specified (icon is null), the iconIsFromHandle variable will be
// null, which allows to clear the dialog's icon while it is shown, regardless
// of whether the bound icon is a handle icon or a non-handle icon.
if (icon?.IsHandleIcon == true)
{
iconUnion.hFooterIcon = (HICON)icon.IconHandle;
iconIsFromHandle = true;
}
else if (icon?.IsStandardIcon == true)
{
// Convert the value to an ushort before converting it to a pointer,
// which corresponds to using the MAKEINTRESOURCEW macro in native code.
iconUnion.pszFooterIcon = (char*)checked((ushort)icon.StandardIcon);
iconIsFromHandle = false;
}
return (iconUnion, iconIsFromHandle);
}
internal void DenyIfBound()
{
if (BoundDialog is not null)
{
throw new InvalidOperationException(SR.TaskDialogCannotSetPropertyOfBoundPage);
}
}
internal void DenyIfWaitingForInitialization()
{
if (WaitingForInitialization)
{
throw new InvalidOperationException(string.Format(
SR.TaskDialogNavigationNotCompleted,
$"{nameof(TaskDialogPage)}.{nameof(Created)}"));
}
}
internal TaskDialogButton? GetBoundButtonByID(int buttonID)
{
if (BoundDialog is null)
{
throw new InvalidOperationException();
}
if (buttonID == 0)
{
return null;
}
// Check if the button is part of the custom buttons.
if (buttonID >= CustomButtonStartID)
{
return _boundCustomButtons![buttonID - CustomButtonStartID];
}
else
{
// Note: We deliberately return null instead of throwing when
// the common button ID is not part of the collection, because
// the caller might not know if such a button exists.
_boundStandardButtonsByID!.TryGetValue(buttonID, out TaskDialogButton? button);
return button;
}
}
internal TaskDialogRadioButton? GetBoundRadioButtonByID(int buttonID)
{
if (BoundDialog is null)
{
throw new InvalidOperationException();
}
return buttonID == 0 ? null : _radioButtons[buttonID - RadioButtonStartID];
}
internal void Validate()
{
// Check that this page instance is not already bound to a TaskDialog instance.
if (BoundDialog is not null)
{
throw new InvalidOperationException(string.Format(
SR.TaskDialogPageIsAlreadyBound,
nameof(TaskDialogPage),
nameof(TaskDialog)));
}
// We also need to check the controls (and collections) since they could also be
// bound to a TaskDialogPage at the same time.
if (_buttons.BoundPage is not null ||
_radioButtons.BoundPage is not null)
{
throw new InvalidOperationException(string.Format(
SR.TaskDialogCollectionAlreadyBound,
nameof(TaskDialogPage),
nameof(TaskDialog)));
}
if (_buttons.Concat<TaskDialogControl>(_radioButtons)
.Append(_checkBox)
.Append(_expander)
.Append(_footnote)
.Append(_progressBar)
.Any(c => c?.BoundPage is not null))
{
throw new InvalidOperationException(string.Format(
SR.TaskDialogControlAlreadyBound,
nameof(TaskDialogPage),
nameof(TaskDialog)));
}
// Note: Theoretically we would need to exclude the standard buttons from the
// "_buttons" collection when retrieving the Count (because they already have
// a fixed button ID), but since not excluding them won't allow more buttons
// than possible, we don't need to do it.
if (_buttons.Count > int.MaxValue - CustomButtonStartID + 1 ||
_radioButtons.Count > int.MaxValue - RadioButtonStartID + 1)
{
throw new InvalidOperationException(SR.TaskDialogTooManyButtonsAdded);
}
if (_radioButtons.Count(e => e.Checked) > 1)
{
throw new InvalidOperationException(SR.TaskDialogOnlySingleRadioButtonCanBeChecked);
}
// Check that we don't have custom buttons and command links at the same time.
bool foundCustomButton = false;
bool foundCommandLink = false;
foreach (TaskDialogButton button in _buttons)
{
if (button is TaskDialogCommandLinkButton commandLink)
{
foundCommandLink = true;
}
else if (!button.IsStandardButton)
{
foundCustomButton = true;
}
}
if (foundCustomButton && foundCommandLink)
{
throw new InvalidOperationException(SR.TaskDialogCannotShowCustomButtonsAndCommandLinks);
}
// For custom and radio buttons, we need to ensure the strings are not null or empty
// (except for invisible buttons), as otherwise an error would occur when
// showing/navigating the dialog.
if (_buttons.Any(e => e.IsCreatable && !e.IsStandardButton && IsNativeStringNullOrEmpty(e.Text)))
{
throw new InvalidOperationException(SR.TaskDialogButtonTextMustNotBeNull);
}
if (_radioButtons.Any(e => e.IsCreatable && IsNativeStringNullOrEmpty(e.Text)))
{
throw new InvalidOperationException(SR.TaskDialogRadioButtonTextMustNotBeNull);
}
// Ensure the default button is part of the button collection.
if (DefaultButton is not null && !_buttons.Contains(DefaultButton))
{
throw new InvalidOperationException(SR.TaskDialogDefaultButtonMustExistInCollection);
}
}
internal void Bind(
TaskDialog owner,
out TASKDIALOG_FLAGS flags,
out TASKDIALOG_COMMON_BUTTON_FLAGS buttonFlags,
out IEnumerable<(int buttonID, string text)> customButtonElements,
out IEnumerable<(int buttonID, string text)> radioButtonElements,
out TASKDIALOGCONFIG_MainIcon mainIcon,
out TASKDIALOGCONFIG_FooterIcon footnoteIcon,
out int defaultButtonID,
out int defaultRadioButtonID)
{
if (BoundDialog is not null)
{
throw new InvalidOperationException();
}
// This method assumes Validate() has already been called.
BoundDialog = owner;
flags = _flags;
_updateTextOnInitialization = false;
_updateHeadingOnInitialization = false;
(TASKDIALOGCONFIG_MainIcon localIconValue, bool? iconIsFromHandle) = GetMainIconValue(_icon);
(mainIcon, _boundIconIsFromHandle) = (localIconValue, iconIsFromHandle ?? false);
if (_boundIconIsFromHandle)
{
flags |= TASKDIALOG_FLAGS.TDF_USE_HICON_MAIN;
}
TaskDialogButtonCollection buttons = _buttons;
TaskDialogRadioButtonCollection radioButtons = _radioButtons;
buttons.BoundPage = this;
radioButtons.BoundPage = this;
// Sort the buttons.
_boundCustomButtons = buttons.Where(e => !e.IsStandardButton).ToArray();
_boundStandardButtonsByID = new Dictionary<int, TaskDialogButton>(
buttons.Where(e => e.IsStandardButton)
.Select(e => new KeyValuePair<int, TaskDialogButton>(e.ButtonID, e)));
// Assign IDs to the buttons based on their index.
defaultButtonID = 0;
buttonFlags = default;
foreach (TaskDialogButton standardButton in _boundStandardButtonsByID.Values)
{
flags |= standardButton.Bind(this);
if (standardButton.IsCreated)
{
buttonFlags |= standardButton.GetStandardButtonFlag();
}
}
for (int i = 0; i < _boundCustomButtons.Length; i++)
{
TaskDialogButton customButton = _boundCustomButtons[i];
flags |= customButton.Bind(this, CustomButtonStartID + i);
}
if (DefaultButton is not null)
{
// Retrieve the button from the collection, to handle the case for standard buttons
// when the user set an equal (but not same) instance as default button.
TaskDialogButton defaultButton = buttons[buttons.IndexOf(DefaultButton)];
if (defaultButton.IsCreated)
{
defaultButtonID = defaultButton.ButtonID;
}
}
defaultRadioButtonID = 0;
for (int i = 0; i < radioButtons.Count; i++)
{
TaskDialogRadioButton radioButton = radioButtons[i];
flags |= radioButton.Bind(this, RadioButtonStartID + i);
// Validate() will already have verified that no more than one radio button is checked.
if (radioButton.IsCreated && radioButton.Checked)
{
defaultRadioButtonID = radioButton.RadioButtonID;
}
}
if (defaultRadioButtonID == 0)
{
flags |= TASKDIALOG_FLAGS.TDF_NO_DEFAULT_RADIO_BUTTON;
}
customButtonElements = _boundCustomButtons.Where(e => e.IsCreated).Select(e => (e.ButtonID, e.GetResultingText()!));
radioButtonElements = radioButtons.Where(e => e.IsCreated).Select(e => (e.RadioButtonID, e.Text!));
// If we have command links, specify the TDF_USE_COMMAND_LINKS flag.
// Note: The USE_COMMAND_LINKS_NO_ICON is currently not used.
if (_boundCustomButtons.Any(e => e.IsCreated && e is TaskDialogCommandLinkButton))
flags |= TASKDIALOG_FLAGS.TDF_USE_COMMAND_LINKS;
if (EnableLinks)
{
flags |= TASKDIALOG_FLAGS.TDF_ENABLE_HYPERLINKS;
}
if (_checkBox is not null)
{
flags |= _checkBox.Bind(this);
}
if (_expander is not null)
{
flags |= _expander.Bind(this);
}
if (_footnote is not null)
{
flags |= _footnote.Bind(this, out footnoteIcon);
}
else
{
footnoteIcon = default;
}
if (_progressBar is not null)
{
flags |= _progressBar.Bind(this);
}
}
internal void Unbind()
{
if (BoundDialog is null)
{
throw new InvalidOperationException();
}
_boundCustomButtons = null;
_boundStandardButtonsByID = null;
TaskDialogButtonCollection buttons = _buttons;
TaskDialogRadioButtonCollection radioButtons = _radioButtons;
foreach (TaskDialogButton button in buttons)
{
button.Unbind();
}
foreach (TaskDialogRadioButton radioButton in radioButtons)
{
radioButton.Unbind();
}
buttons.BoundPage = null;
radioButtons.BoundPage = null;
_checkBox?.Unbind();
_expander?.Unbind();
_footnote?.Unbind();
_progressBar?.Unbind();
BoundDialog = null;
_appliedInitialization = false;
}
internal void ApplyInitialization()
{
if (_appliedInitialization)
{
throw new InvalidOperationException();
}
_appliedInitialization = true;
// Check if we need to update some of our elements (if they have been modified
// after starting navigation, but before navigation was finished).
if (_updateHeadingOnInitialization)
{
Heading = _heading;
_updateHeadingOnInitialization = false;
}
if (_updateTextOnInitialization)
{
Text = _text;
_updateTextOnInitialization = false;
}
TaskDialogButtonCollection buttons = _buttons;
TaskDialogRadioButtonCollection radioButtons = _radioButtons;
foreach (TaskDialogButton button in buttons)
{
button.ApplyInitialization();
}
foreach (TaskDialogRadioButton button in radioButtons)
{
button.ApplyInitialization();
}
_checkBox?.ApplyInitialization();
_expander?.ApplyInitialization();
_footnote?.ApplyInitialization();
_progressBar?.ApplyInitialization();
}
/// <summary>
/// Raises the <see cref="Created"/> event.
/// </summary>
/// <param name="e">An <see cref="EventArgs"/> that contains the event data.</param>
protected internal void OnCreated(EventArgs e) => Created?.Invoke(this, e);
/// <summary>
/// Raises the <see cref="Destroyed"/> event.
/// </summary>
/// <param name="e">An <see cref="EventArgs"/> that contains the event data.</param>
protected internal void OnDestroyed(EventArgs e) => Destroyed?.Invoke(this, e);
/// <summary>
/// Raises the <see cref="HelpRequest"/> event.
/// </summary>
/// <param name="e">An <see cref="EventArgs"/> that contains the event data.</param>
protected internal void OnHelpRequest(EventArgs e) => HelpRequest?.Invoke(this, e);
/// <summary>
/// Raises the <see cref="LinkClicked"/> event.
/// </summary>
/// <param name="e">A <see cref="TaskDialogLinkClickedEventArgs"/> that contains the event data.</param>
protected internal void OnLinkClicked(TaskDialogLinkClickedEventArgs e) => LinkClicked?.Invoke(this, e);
private bool GetFlag(TASKDIALOG_FLAGS flag) => (_flags & flag) == flag;
private void SetFlag(TASKDIALOG_FLAGS flag, bool value)
{
DenyIfBound();
if (value)
{
_flags |= flag;
}
else
{
_flags &= ~flag;
}
}
}
|