|
// 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 radio button control of a task dialog.
/// </summary>
public sealed class TaskDialogRadioButton : TaskDialogControl
{
private string? _text;
private int _radioButtonID;
private bool _enabled = true;
private bool _checked;
private TaskDialogRadioButtonCollection? _collection;
private bool _ignoreRadioButtonClickedNotification;
/// <summary>
/// Occurs when the value of the <see cref="Checked"/> property changes
/// while this control is shown in a task dialog.
/// </summary>
public event EventHandler? CheckedChanged;
/// <summary>
/// Initializes a new instance of the <see cref="TaskDialogRadioButton"/> class.
/// </summary>
public TaskDialogRadioButton()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TaskDialogRadioButton"/> class
/// using the given <paramref name="text"/>.
/// </summary>
public TaskDialogRadioButton(string? text)
: this()
{
_text = text;
}
/// <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 radio 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!.SetRadioButtonEnabled(_radioButtonID, value);
}
_enabled = 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>
/// 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 radio button instance is currently bound to a task dialog.
/// </exception>
public string? Text
{
get => _text;
set
{
DenyIfBound();
_text = value;
}
}
/// <summary>
/// Gets or set a value indicating whether the <see cref="TaskDialogRadioButton"/> is
/// in the checked state.
/// </summary>
/// <value>
/// <see langword="true"/> if the <see cref="TaskDialogRadioButton"/> is in the checked state;
/// otherwise, <see langword="false"/>. The default value is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// While the dialog is shown, this property can only be set to <see langword="true"/> and you cannot
/// set it from within the <see cref="CheckedChanged"/> event.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The property is set and the task dialog has started navigating to a new page containing this radio button instance,
/// but the <see cref="TaskDialogPage.Created"/> event has not been raised yet.
/// - or -
/// The property is set on a radio button instance that is currently bound to a task dialog,
/// but the value to be set is <see langword="false"/>.
/// - or -
/// The property is set within the <see cref="CheckedChanged"/> event of one of the radio buttons of the
/// currently bound task dialog.
/// - or -
/// The property is set on a radio button instance that is currently bound to a task dialog, but the dialog
/// has just started navigating to a different page.
/// </exception>
public bool Checked
{
get => _checked;
set
{
DenyIfBoundAndNotCreated(); // Shouldn't throw here as the control must have been created.
DenyIfWaitingForInitialization();
if (BoundPage is null)
{
_checked = value;
// If we are part of a collection, set the checked value of all
// all other buttons to False.
// Note that this does not handle buttons that are added later
// to the collection.
if (_collection is not null && value)
{
foreach (TaskDialogRadioButton radioButton in _collection)
{
radioButton._checked = radioButton == this;
}
}
return;
}
// Unchecking a radio button is not possible in the task dialog.
// TODO: Should we throw only if the new value is different than the
// old one?
if (!value)
{
throw new InvalidOperationException(SR.TaskDialogCannotUncheckRadioButtonWhileBound);
}
// Note: We do not allow to set the "Checked" property of any
// radio button of the current task dialog while we are within
// the TDN_RADIO_BUTTON_CLICKED notification handler. This is
// because the logic of the task dialog is such that the radio
// button will be selected AFTER the callback returns (not
// before it is called), at least when the event is caused by
// code sending the TDM_CLICK_RADIO_BUTTON message. This is
// mentioned in the documentation for TDM_CLICK_RADIO_BUTTON:
// "The specified radio button ID is sent to the
// TaskDialogCallbackProc callback function as part of a
// TDN_RADIO_BUTTON_CLICKED notification code. After the
// callback function returns, the radio button will be
// selected."
//
// While we handle this by ignoring the
// TDN_RADIO_BUTTON_CLICKED notification when it is caused by
// sending a TDM_CLICK_RADIO_BUTTON message, and then raising
// the events after the notification handler returned, this
// still seems to cause problems for TDN_RADIO_BUTTON_CLICKED
// notifications initially caused by the user clicking the radio
// button in the UI.
//
// For example, consider a scenario with two radio buttons
// [ID 1 and 2], and the user added an event handler to
// automatically select the first radio button (ID 1) when the
// second one (ID 2) is selected in the UI.
// This means the stack will then look as follows:
// Show() ->
// Callback: TDN_RADIO_BUTTON_CLICKED [ID 2] ->
// SendMessage: TDM_CLICK_RADIO_BUTTON [ID 1] ->
// Callback: TDN_RADIO_BUTTON_CLICKED [ID 1]
//
// However, when the initial TDN_RADIO_BUTTON_CLICKED handler
// (ID 2) returns, the task dialog again calls the handler for
// ID 1 (which wouldn't be a problem), and then again calls it
// for ID 2, which is unexpected (and it doesn't seem that we
// can prevent this by returning S_FALSE in the notification
// handler). Additionally, after that it even seems we get an
// endless loop of TDN_RADIO_BUTTON_CLICKED notifications even
// when we don't send any further messages to the dialog.
// See documentation/repro in
// /docs/src/System/Windows/Forms/TaskDialog/Issue_RadioButton_InfiniteLoop.md
//
// See also:
// /docs/src/System/Windows/Forms/TaskDialog/Issue_RadioButton_WeirdBehavior.md
if (BoundPage.BoundDialog!.RadioButtonClickedStackCount > 0)
{
throw new InvalidOperationException(string.Format(
SR.TaskDialogCannotSetRadioButtonCheckedWithinCheckedChangedEvent,
$"{nameof(TaskDialogRadioButton)}.{nameof(Checked)}",
$"{nameof(TaskDialogRadioButton)}.{nameof(CheckedChanged)}"));
}
// Click the radio button which will (recursively) raise the
// TDN_RADIO_BUTTON_CLICKED notification. However, we ignore
// the notification and then raise the events afterwards - see
// above.
_ignoreRadioButtonClickedNotification = true;
try
{
BoundPage.BoundDialog.ClickRadioButton(_radioButtonID);
}
finally
{
_ignoreRadioButtonClickedNotification = false;
}
// Now raise the events.
// Note: We also increment the stack count here to prevent
// navigating the dialog and setting the Checked property
// within the event handlers here even though this would work
// correctly for the native API (as we are not in the
// TDN_RADIO_BUTTON_CLICKED notification), because we are
// raising two events (Unchecked+Checked), and when the
// second event is called, the dialog might already be
// navigated or another radio button may have been checked.
TaskDialog boundDialog = BoundPage.BoundDialog;
checked
{
boundDialog.RadioButtonClickedStackCount++;
}
try
{
HandleRadioButtonClicked();
}
finally
{
boundDialog.RadioButtonClickedStackCount--;
}
}
}
internal int RadioButtonID => _radioButtonID;
internal TaskDialogRadioButtonCollection? Collection
{
get => _collection;
set => _collection = value;
}
/// <summary>
/// Returns a string that represents the current <see cref="TaskDialogRadioButton"/> 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 radioButtonID)
{
TASKDIALOG_FLAGS result = Bind(page);
_radioButtonID = radioButtonID;
return result;
}
internal void HandleRadioButtonClicked()
{
// Check if we need to ignore the notification when it is caused by
// sending the TDM_CLICK_RADIO_BUTTON message.
if (_ignoreRadioButtonClickedNotification)
{
return;
}
if (_checked)
{
return;
}
_checked = true;
// Before raising the CheckedChanged event for the current button,
// uncheck the other radio buttons and call their events (there
// should be no more than one other button that is already checked).
foreach (TaskDialogRadioButton radioButton in BoundPage!.RadioButtons)
{
if (radioButton != this && radioButton._checked)
{
radioButton._checked = false;
radioButton.OnCheckedChanged(EventArgs.Empty);
}
}
// Finally, call the event for the current button.
OnCheckedChanged(EventArgs.Empty);
}
private protected override void UnbindCore()
{
_radioButtonID = 0;
base.UnbindCore();
}
private protected override void ApplyInitializationCore()
{
// Re-set the properties so they will make the necessary calls.
if (!_enabled)
{
Enabled = _enabled;
}
}
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;
}
private void OnCheckedChanged(EventArgs e) => CheckedChanged?.Invoke(this, e);
}
|