|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Windows.Forms;
/// <summary>
/// Represents a button control of a task dialog.
/// </summary>
/// <remarks>
/// <para>
/// A button can either be a standard button (whose text is provided by the OS), or
/// a custom button (or command link) where you can provide your own text.
/// </para>
/// <para>
/// <see cref="TaskDialogButton"/> instances retrieved by static getters like <see cref="OK"/> are
/// standard buttons. Their <see cref="Text"/> property cannot be set as the OS will provide the
/// localized text for the buttons when showing them in the dialog.
/// </para>
/// <para>
/// Button instances created with one of the constructors are custom buttons, which allow you to provide
/// your own text as button label.
/// </para>
/// <para>
/// Note: It's not possible to show both custom buttons and command links (<see cref="TaskDialogCommandLinkButton"/> instances)
/// at the same time - it's only one or the other. In either case, you can combine them with
/// standard buttons.
/// </para>
/// </remarks>
public class TaskDialogButton : TaskDialogControl
{
private readonly TaskDialogResult? _standardButtonResult;
private bool _enabled = true;
private bool _showShieldIcon;
private bool _visible = true;
private string? _text;
private int _customButtonID;
/// <summary>
/// Occurs when the button is clicked.
/// </summary>
/// <remarks>
/// <para>
/// By default, the dialog will be closed after the event handler returns
/// (except for the <see cref="Help"/> button, which instead will raise the
/// <see cref="TaskDialogPage.HelpRequest"/> event afterwards).
/// </para>
/// <para>
/// To prevent the dialog from closing when this button is clicked, set the
/// <see cref="AllowCloseDialog"/> property to <see langword="false"/>.
/// </para>
/// </remarks>
public event EventHandler? Click;
/// <summary>
/// Initializes a new instance of the <see cref="TaskDialogButton"/> class.
/// </summary>
// TODO: Find a way to avoid making the class inheritable
public TaskDialogButton()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TaskDialogButton"/> class
/// using the given text and, optionally, a description text.
/// </summary>
/// <param name="text">The text of the control.</param>
/// <param name="enabled">A value that indicates if the button should be enabled.</param>
/// <param name="allowCloseDialog">A value that indicates whether the task dialog should close
/// when this button is clicked.
/// </param>
public TaskDialogButton(string? text, bool enabled = true, bool allowCloseDialog = true) : this()
{
_text = text;
Enabled = enabled;
AllowCloseDialog = allowCloseDialog;
}
internal TaskDialogButton(TaskDialogResult standardButtonResult)
{
_standardButtonResult = standardButtonResult;
_text = standardButtonResult.ToString();
}
// Static factory properties that return a new instance of the button.
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "OK" button.
/// </summary>
public static TaskDialogButton OK => new(TaskDialogResult.OK);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Cancel" button.
/// </summary>
/// <remarks>
/// <para>
/// Note: Adding a Cancel button will automatically add a close button
/// to the task dialog's title bar and will allow to close the dialog by
/// pressing ESC or Alt+F4 (just as if you enabled
/// <see cref="TaskDialogPage.AllowCancel"/>).
/// </para>
/// </remarks>
public static TaskDialogButton Cancel => new(TaskDialogResult.Cancel);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Abort" button.
/// </summary>
public static TaskDialogButton Abort => new(TaskDialogResult.Abort);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Retry" button.
/// </summary>
public static TaskDialogButton Retry => new(TaskDialogResult.Retry);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Ignore" button.
/// </summary>
public static TaskDialogButton Ignore => new(TaskDialogResult.Ignore);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Yes" button.
/// </summary>
public static TaskDialogButton Yes => new(TaskDialogResult.Yes);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "No" button.
/// </summary>
public static TaskDialogButton No => new(TaskDialogResult.No);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Close" button.
/// </summary>
public static TaskDialogButton Close => new(TaskDialogResult.Close);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Help" button.
/// </summary>
/// <remarks>
/// <para>
/// Note: Clicking this button will not close the dialog, but will raise the
/// <see cref="TaskDialogPage.HelpRequest"/> event.
/// </para>
/// </remarks>
public static TaskDialogButton Help => new(TaskDialogResult.Help);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Try Again" button.
/// </summary>
public static TaskDialogButton TryAgain => new(TaskDialogResult.TryAgain);
/// <summary>
/// Gets a standard <see cref="TaskDialogButton"/> instance representing the "Continue" button.
/// </summary>
public static TaskDialogButton Continue => new(TaskDialogResult.Continue);
/// <summary>
/// Gets or sets a value that indicates whether the task dialog should close
/// when this button is clicked. Or, if this button is the
/// <see cref="Help"/> button, indicates whether the
/// <see cref="TaskDialogPage.HelpRequest"/> should be raised.
/// </summary>
/// <value>
/// <see langword="true"/> if the task dialog should close when
/// this button is clicked; otherwise, <see langword="false"/>. The default value is <see langword="true"/>.
/// </value>
public bool AllowCloseDialog { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the button can respond to user interaction.
/// </summary>
/// <value>
/// <see langword="true"/> if the button can respond to user interaction; otherwise,
/// <see langword="false"/>. The default value is <see langword="true"/>.
/// </value>
/// <remarks>
/// <para>
/// This property can be set while the dialog is shown.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set on a button that is currently bound to a task dialog, but the dialog
/// has just started navigating to a different page.
/// </exception>
public bool Enabled
{
get => _enabled;
set
{
if (CanUpdate())
{
BoundPage!.BoundDialog!.SetButtonEnabled(ButtonID, value);
}
_enabled = value;
}
}
/// <summary>
/// Gets or sets a value that indicates if the User Account Control (UAC) shield icon
/// should be shown near the button; that is, whether the action invoked by the button
/// requires elevation.
/// </summary>
/// <value>
/// <see langword="true"/> to show the UAC shield icon; otherwise, <see langword="false"/>.
/// The default value is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// This property can be set while the dialog is shown.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set on a button that is currently bound to a task dialog, but the dialog
/// has just started navigating to a different page.
/// </exception>
public bool ShowShieldIcon
{
get => _showShieldIcon;
set
{
if (CanUpdate())
{
BoundPage!.BoundDialog!.SetButtonElevationRequiredState(ButtonID, value);
}
_showShieldIcon = value;
}
}
/// <summary>
/// Gets or sets a value that indicates if this
/// <see cref="TaskDialogButton"/> should be shown when displaying
/// the task dialog.
/// </summary>
/// <remarks>
/// <para>
/// Setting this property to <see langword="false"/> allows you to still receive the
/// <see cref="Click"/> event (e.g. for the
/// <see cref="Cancel"/> button when
/// <see cref="TaskDialogPage.AllowCancel"/> is set), or to call the
/// <see cref="PerformClick"/> method even if the button
/// is not shown.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this button instance is currently bound to a task dialog.
/// </exception>
public bool Visible
{
get => _visible;
set
{
DenyIfBound();
_visible = value;
}
}
/// <summary>
/// Gets or sets the text associated with this control.
/// </summary>
/// <value>
/// The text associated with this control. The default value is <see langword="null"/>.
/// </value>
/// <remarks>
/// <para>
/// You cannot set this property if this button is a standard button, as its text will be provided by the OS.
/// </para>
/// <para>
/// This property must not be <see langword="null"/> or an empty string when showing or navigating
/// the dialog; otherwise, the operation will fail.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and this button instance is a standard button, for which the text is provided by the OS.
/// - or -
/// The property is set and this button instance is currently bound to a task dialog.
/// </exception>
public string? Text
{
get => _text;
set
{
// For standard buttons, the text is set by the constructor (but it's only
// the enum value name, the actual text is provided by the OS).
if (IsStandardButton)
throw new InvalidOperationException(SR.TaskDialogCannotSetTextForStandardButton);
DenyIfBound();
_text = value;
}
}
internal override bool IsCreatable => base.IsCreatable && _visible;
internal bool IsStandardButton => _standardButtonResult is not null;
internal TaskDialogResult StandardButtonResult => _standardButtonResult ?? throw new InvalidOperationException();
internal int ButtonID => IsStandardButton ? (int)StandardButtonResult : _customButtonID;
internal TaskDialogButtonCollection? Collection { get; set; }
public static bool operator ==(TaskDialogButton? b1, TaskDialogButton? b2)
{
return Equals(b1, b2);
}
public static bool operator !=(TaskDialogButton? b1, TaskDialogButton? b2)
{
return !(b1 == b2);
}
private static TASKDIALOG_COMMON_BUTTON_FLAGS GetStandardButtonFlagForResult(TaskDialogResult result) => result switch
{
TaskDialogResult.OK => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_OK_BUTTON,
TaskDialogResult.Cancel => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_CANCEL_BUTTON,
TaskDialogResult.Abort => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_ABORT_BUTTON,
TaskDialogResult.Retry => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_RETRY_BUTTON,
TaskDialogResult.Ignore => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_IGNORE_BUTTON,
TaskDialogResult.Yes => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_YES_BUTTON,
TaskDialogResult.No => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_NO_BUTTON,
TaskDialogResult.Close => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_CLOSE_BUTTON,
TaskDialogResult.Help => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_HELP_BUTTON,
TaskDialogResult.TryAgain => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_TRYAGAIN_BUTTON,
TaskDialogResult.Continue => TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_CONTINUE_BUTTON,
_ => default
};
/// <summary>
/// Simulates a click on this button.
/// </summary>
/// <exception cref="InvalidOperationException">
/// This button instance is not currently bound to a task dialog.
/// - or -
/// The task dialog has started navigating to a new page containing this button instance,
/// but the <see cref="TaskDialogPage.Created"/> event has not been raised yet.
/// - or -
/// This button is currently bound to a task dialog, but the dialog has just started navigating to a different page.
/// </exception>
public void PerformClick()
{
// Note: We allow a click even if the button is not visible/created.
DenyIfNotBoundOrWaitingForInitialization();
BoundPage!.BoundDialog!.ClickButton(ButtonID);
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
// For standard buttons, we consider them to be equal if they have the same
// dialog result. This will allow for checking the return value of
// TaskDialog.ShowDialog with code like "if (result == TaskDialogButton.Yes)".
if (IsStandardButton && obj is TaskDialogButton otherButton && otherButton.IsStandardButton)
return _standardButtonResult!.Value == otherButton._standardButtonResult!.Value;
// Otherwise, check for reference equality.
return base.Equals(obj);
}
/// <inheritdoc/>
public override int GetHashCode()
{
if (IsStandardButton)
return (int)_standardButtonResult!.Value;
return base.GetHashCode();
}
/// <summary>
/// Returns a string that represents the current <see cref="TaskDialogButton"/> control.
/// </summary>
/// <returns>A string that contains the control text.</returns>
public override string ToString() => _text ?? base.ToString() ?? string.Empty;
internal TASKDIALOG_FLAGS Bind(TaskDialogPage page, int customButtonID)
{
if (_standardButtonResult is not null)
throw new InvalidOperationException();
TASKDIALOG_FLAGS result = Bind(page);
_customButtonID = customButtonID;
return result;
}
internal bool HandleButtonClicked()
{
OnClick(EventArgs.Empty);
return AllowCloseDialog;
}
internal virtual string? GetResultingText()
{
// Remove LFs from the text. Otherwise, the dialog would display the
// part of the text after the LF in the command link note, but for
// this we have the "DescriptionText" property, so we should ensure that
// there is not an discrepancy here and that the contents of the "Text"
// property are not displayed in the command link note.
// Therefore, we replace a combined CR+LF with CR, and then also single
// LFs with CR, because CR is treated as a line break.
string? text = _text?.Replace("\r\n", "\r").Replace("\n", "\r");
return text;
}
internal TASKDIALOG_COMMON_BUTTON_FLAGS GetStandardButtonFlag() => !IsStandardButton
? throw new InvalidOperationException()
: GetStandardButtonFlagForResult(_standardButtonResult!.Value);
private protected override void ApplyInitializationCore()
{
// Re-set the properties so they will make the necessary calls.
if (!_enabled)
{
Enabled = _enabled;
}
if (_showShieldIcon)
{
ShowShieldIcon = _showShieldIcon;
}
}
private protected override void UnbindCore()
{
_customButtonID = 0;
base.UnbindCore();
}
private protected void OnClick(EventArgs e) => Click?.Invoke(this, e);
private bool CanUpdate()
{
// Only update the button when bound to a task dialog, the button has actually been
// created, and we are not waiting for the Navigated event. In the latter case we
// don't throw an exception however, because ApplyInitialization() will be called in
// the Navigated handler that does the necessary updates.
return BoundPage?.WaitingForInitialization == false && IsCreated;
}
}
|