-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Hi,
On Windows Vista and higher, the Task Dialog is available that provides many more features than a Message Box. While you can show a Message Box in WinForms and WPF, there is no "official" implementation of the Task Dialog yet in .NET WinForms/WPF.
There was an implementation in the Windows API Code Pack 1.1, but it is no longer available/updated, it did not implement all features (like navigation or modifying common/standard buttons), and I believe it had some memory management issues (like not calling Marshal.DestroyStructure() after calling Marshal.StructureToPtr() in order to free allocated strings for custom/radio buttons) and a few other issues.
At my company, we currently use the Task Dialog in a (commercial) WPF application to show a marquee progress bar while an operation (like database backup) is running, and then navigate it to one showing a green header to indicate the operation is finished.
Visual Studio is also using a Task Dialog:

Also, the Windows Design Guidelines (Desktop Apps) for Messages and Dialog Boxes show features of the task dialog.
Do you think a Task Dialog could also be added directly to WinForms/WPF? Thank you!
Edit:
Rationale and Usage
The Windows Task Dialog (which is available since Windows Vista) has a lot of configuration options comparing to a regular Message Box, can show additional controls like a progress bar, and supports event handling. However, it has not yet been integrated officially into WinForms/WPF, so if you wanted to use it, you had to implement the native APIs yourself, or use a 3rd party library.
Implementing the Task Dialog directly in WinForms allows users to directly use the Task Dialog in any new WinForms/WPF .NET Core application, just like a MessageBox. You can then either use the simple static Show() method (similar to a MessageBox), or you can create an instance of the TaskDialog, configure its TaskDialogPage and then show it.
Features of the proposed Task Dialog:
- Supports all of the native Task Dialog elements (like custom buttons/command links, progress bar, radio buttons, checkbox, expanded area, footer)
- Some dialog elements can be updated while the dialog is displayed, and the dialog can be closed from code
- Additionally to standard icons, supports shield icons that show a green, yellow, red, gray or blue bar
- Can navigate to a new page (by reconstructing the dialog from current properties) by calling
TaskDialogPage.Navigate(TaskDialogPage)while the dialog is displayed - Can be shown modal or non-modal (when showing modal, can be centered to the parent)
- Exposes its window handle (
hWnd) through theHandleproperty so that the dialog window can be further manipulated (or used as owner for another window)
See also the Task Dialog Demo App for examples.
Show a simple Task Dialog
TaskDialogButton resultButton = TaskDialog.ShowDialog(new TaskDialogPage()
{
Text = "Hello World!",
Heading = "Hello Task Dialog! 👍",
Caption = "Dialog Title",
Buttons = {
TaskDialogButton.Yes,
TaskDialogButton.Cancel
},
Icon = TaskDialogIcon.ShieldSuccessGreenBar
});
if (resultButton == TaskDialogButton.Yes)
{
// Do something...
}Dialog similar to the Visual Studio dialog
TaskDialogCommandLinkButton buttonRestart = new TaskDialogCommandLinkButton()
{
Text = "&Restart under different credentials",
DescriptionText = "This text will be shown in the second line.",
ShowShieldIcon = true
};
TaskDialogCommandLinkButton buttonCancelTask = new TaskDialogCommandLinkButton()
{
Text = "&Cancel the Task and return"
};
var page = new TaskDialogPage()
{
Icon = TaskDialogIcon.Shield,
Heading = "This task requires the application to have elevated permissions.",
// TODO - Hyperlinks will be possible in a future version
Text = "Why is using the Administrator or other account necessary?",
// TODO - will be possible in a future version
//EnableHyperlinks = true,
Buttons =
{
TaskDialogButton.Cancel,
buttonRestart,
buttonCancelTask
},
DefaultButton = buttonCancelTask,
// Show a expander.
Expander = new TaskDialogExpander()
{
Text = "Some expanded Text",
CollapsedButtonText = "View error information",
ExpandedButtonText = "Hide error information",
Position = TaskDialogExpanderPosition.AfterFootnote
}
};
// Show the dialog and check the result.
TaskDialogButton result = TaskDialog.ShowDialog(page);
if (result == buttonRestart)
{
Console.WriteLine("Restarting...");
}Show a multi-page dialog that shows current progress, then navigates to a result
See also: Multi-page dialog boxes
// Disable the "Yes" button and only enable it when the check box is checked.
// Also, don't close the dialog when this button is clicked.
var initialButtonYes = TaskDialogButton.Yes;
initialButtonYes.Enabled = false;
initialButtonYes.AllowCloseDialog = false;
var initialPage = new TaskDialogPage()
{
Caption = "My Application",
Heading = "Clean up database?",
Text = "Do you really want to do a clean-up?\nThis action is irreversible!",
Icon = TaskDialogIcon.ShieldWarningYellowBar,
AllowCancel = true,
Verification = new TaskDialogVerificationCheckBox()
{
Text = "I know what I'm doing"
},
Buttons =
{
TaskDialogButton.No,
initialButtonYes
},
DefaultButton = TaskDialogButton.No
};
// For the "In Progress" page, don't allow the dialog to close, by adding
// a disabled button (if no button was specified, the task dialog would
// get an (enabled) 'OK' button).
var inProgressCloseButton = TaskDialogButton.Close;
inProgressCloseButton.Enabled = false;
var inProgressPage = new TaskDialogPage()
{
Caption = "My Application",
Heading = "Operation in progress...",
Text = "Please wait while the operation is in progress.",
Icon = TaskDialogIcon.Information,
ProgressBar = new TaskDialogProgressBar()
{
State = TaskDialogProgressBarState.Marquee
},
Expander = new TaskDialogExpander()
{
Text = "Initializing...",
Position = TaskDialogExpanderPosition.AfterFootnote
},
Buttons =
{
inProgressCloseButton
}
};
var finishedPage = new TaskDialogPage()
{
Caption = "My Application",
Heading = "Success!",
Text = "The operation finished.",
Icon = TaskDialogIcon.ShieldSuccessGreenBar,
Buttons =
{
TaskDialogButton.Close
}
};
TaskDialogButton showResultsButton = new TaskDialogCommandLinkButton("Show &Results");
finishedPage.Buttons.Add(showResultsButton);
// Enable the "Yes" button only when the checkbox is checked.
TaskDialogVerificationCheckBox checkBox = initialPage.Verification;
checkBox.CheckedChanged += (sender, e) =>
{
initialButtonYes.Enabled = checkBox.Checked;
};
// When the user clicks "Yes", navigate to the second page.
initialButtonYes.Click += (sender, e) =>
{
// Navigate to the "In Progress" page that displays the
// current progress of the background work.
initialPage.Navigate(inProgressPage);
// NOTE: When you implement a "In Progress" page that represents
// background work that is done e.g. by a separate thread/task,
// which eventually calls Control.Invoke()/BeginInvoke() when
// its work is finished in order to navigate or update the dialog,
// then DO NOT start that work here already (directly after
// setting the Page property). Instead, start the work in the
// TaskDialogPage.Created event of the new page.
//
// This is because if you started it here, then when that other
// thread/task finishes and calls BeginInvoke() to call a method in
// the GUI thread to update or navigate the dialog, there is a chance
// that the callback might be called before the dialog completed
// navigation (*) (as indicated by the Created event of the
// new page), and the dialog might not be updatable in that case.
// (The dialog can be closed or navigated again, but you cannot
// change e.g. text properties of the page).
//
// If that's not possible for some reason, you need to ensure
// that you delay the call to update the dialog until the Created
// event of the next page has occured.
//
//
// (*) Background info: Although the WinForms implementation of
// Control.Invoke()/BeginInvoke() posts a new message in the
// control's owning thread's message queue every time it is
// called (so that the callback can be called later by the
// message loop), when processing the posted message in the
// control's window procedure, it calls ALL stored callbacks
// instead of only the next one.
//
// This means that even if you start the work after setting
// the Page property (which means BeginInvoke() can only be
// called AFTER starting navigation), the callback specified
// by BeginInvoke might still be called BEFORE the task dialog
// can process its posted navigation message.
};
// Simulate work by starting an async operation from which we are updating the
// progress bar and the expander with the current status.
inProgressPage.Created += async (s, e) =>
{
// Run the background operation and iterate over the streamed values to update
// the progress. Because we call the async method from the GUI thread,
// it will use this thread's synchronization context to run the continuations,
// so we don't need to use Control.[Begin]Invoke() to schedule the callbacks.
var progressBar = inProgressPage.ProgressBar;
await foreach (int progressValue in StreamBackgroundOperationProgressAsync())
{
// When we display the first progress, switch the marquee progress bar
// to a regular one.
if (progressBar.State == TaskDialogProgressBarState.Marquee)
progressBar.State = TaskDialogProgressBarState.Normal;
progressBar.Value = progressValue;
inProgressPage.Expander.Text = $"Progress: {progressValue} %";
}
// Work is finished, so navigate to the third page.
inProgressPage.Navigate(finishedPage);
};
// Show the dialog (modeless).
TaskDialogButton result = TaskDialog.ShowDialog(initialPage);
if (result == showResultsButton)
{
Console.WriteLine("Showing Results!");
}
static async IAsyncEnumerable<int> StreamBackgroundOperationProgressAsync()
{
// Note: The code here will run in the GUI thread - use
// "await Task.Run(...)" to schedule CPU-intensive operations in a
// worker thread.
// Wait a bit before reporting the first progress.
await Task.Delay(2800);
for (int i = 0; i <= 100; i += 4)
{
// Report the progress.
yield return i;
// Wait a bit to simulate work.
await Task.Delay(200);
}
}Other examples from existing applications
"Save document" dialog from Notepad/Paint/WordPad
TaskDialogButton btnCancel = TaskDialogButton.Cancel;
TaskDialogButton btnSave = new TaskDialogButton("&Save");
TaskDialogButton btnDontSave = new TaskDialogButton("Do&n't save");
var page = new TaskDialogPage()
{
Caption = "My Application",
Heading = "Do you want to save changes to Untitled?",
Buttons =
{
btnCancel,
btnSave,
btnDontSave
}
};
// Show a modal dialog, then check the result.
TaskDialogButton result = TaskDialog.ShowDialog(this, page);
if (result == btnSave)
Console.WriteLine("Saving");
else if (result == btnDontSave)
Console.WriteLine("Not saving");
else
Console.WriteLine("Canceling");Windows 7 Minesweeper Difficulty Selection
var page = new TaskDialogPage()
{
Caption = "Minesweeper",
Heading = "What level of difficulty do you want to play?",
AllowCancel = true,
Footnote = new TaskDialogFootnote()
{
Text = "Note: You can change the difficulty level later " +
"by clicking Options on the Game menu.",
},
Buttons =
{
new TaskDialogCommandLinkButton("&Beginner", "10 mines, 9 x 9 tile grid")
{
Tag = 10
},
new TaskDialogCommandLinkButton("&Intermediate", "40 mines, 16 x 16 tile grid")
{
Tag = 40
},
new TaskDialogCommandLinkButton("&Advanced", "99 mines, 16 x 30 tile grid")
{
Tag = 99
}
}
};
TaskDialogButton result = TaskDialog.ShowDialog(this, page);
if (result.Tag is int resultingMines)
Console.WriteLine($"Playing with {resultingMines} mines...");
else
Console.WriteLine("User canceled.");Windows Security dialog when trying to access network files
var page = new TaskDialogPage()
{
Caption = "My App Security",
Heading = "Opening these files might be harmful to your computer",
Text = "Your Internet security settings blocked one or more files from " +
"being opened. Do you want to open these files anyway?",
Icon = TaskDialogIcon.ShieldWarningYellowBar,
// TODO - will be possible in a future version
//EnableHyperlinks = true,
Expander = new TaskDialogExpander("My-File-Sample.exe"),
Footnote = new TaskDialogFootnote()
{
// TODO - Hyperlinks will be possible in a future version
Text = "How do I decide whether to open these files?",
},
Buttons =
{
TaskDialogButton.OK,
TaskDialogButton.Cancel
},
DefaultButton = TaskDialogButton.Cancel
};
TaskDialogButton result = TaskDialog.ShowDialog(this, page);
if (result == TaskDialogButton.OK)
{
Console.WriteLine("OK selected");
}Auto-closing Dialog (closes after 5 seconds)
const string textFormat = "Reconnecting in {0} seconds...";
int remainingTenthSeconds = 50;
var reconnectButton = new TaskDialogButton("&Reconnect now");
var cancelButton = TaskDialogButton.Cancel;
var page = new TaskDialogPage()
{
Heading = "Connection lost; reconnecting...",
Text = string.Format(textFormat, (remainingTenthSeconds + 9) / 10),
ProgressBar = new TaskDialogProgressBar()
{
State = TaskDialogProgressBarState.Paused
},
Buttons =
{
reconnectButton,
cancelButton
}
};
// Create a WinForms timer that raises the Tick event every tenth second.
using var timer = new System.Windows.Forms.Timer()
{
Enabled = true,
Interval = 100
};
timer.Tick += (s, e) =>
{
remainingTenthSeconds--;
if (remainingTenthSeconds > 0)
{
// Update the remaining time and progress bar.
page.Text = string.Format(textFormat, (remainingTenthSeconds + 9) / 10);
page.ProgressBar.Value = 100 - remainingTenthSeconds * 2;
}
else
{
// Stop the timer and click the "Reconnect" button - this will
// close the dialog.
timer.Enabled = false;
reconnectButton.PerformClick();
}
};
TaskDialogButton result = TaskDialog.ShowDialog(this, page);
if (result == reconnectButton)
Console.WriteLine("Reconnecting.");
else
Console.WriteLine("Not reconnecting.");Proposed API
TODO: Which namespace to use for the types? In the PR I used System.Windows.Forms for now.
public class TaskDialog : IWin32Window
{
// Returns the window handle while the dialog is shown, otherwise returns IntPtr.Zero.
public IntPtr Handle { get; }
// Note: The ShowDialog() methods do not return until the
// dialog is closed (similar to MessageBox.Show()), regardless of whether the
// dialog is shown modal or non-modal.
public static TaskDialogButton ShowDialog(TaskDialogPage page, TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterOwner);
public static TaskDialogButton ShowDialog(IWin32Window owner, TaskDialogPage page, TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterOwner);
public static TaskDialogButton ShowDialog(IntPtr hwndOwner, TaskDialogPage page, TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterOwner);
// Close() will simulate a click on the "Cancel" button (but you don't
// have to add a "Cancel" button for this).
public void Close();
}public class TaskDialogPage
{
public TaskDialogPage();
public TaskDialog? BoundDialog { get; }
// Properties "Caption", "MainInstruction", "Text", "Icon" can be set
// set while the dialog is shown, to update the dialog.
public string? Caption { get; set; }
public string? Heading { get; set; }
public string? Text { get; set; }
// Icon can either be a standard icon or a icon handle.
// (In the future, we could allow to show a resource icon.)
// Note that while the dialog is shown, you cannot switch between a
// handle icon type and a non-handle icon type.
// The same applies to the footer icon.
public TaskDialogIcon? Icon { get; set; }
public bool AllowCancel { get; set; }
public bool AllowMinimize { get; set; }
public bool RightToLeftLayout { get; set; }
public bool SizeToContent { get; set; }
public TaskDialogButtonCollection Buttons { get; set; }
public TaskDialogRadioButtonCollection RadioButtons { get; set; }
public TaskDialogButton? DefaultButton { get; set; }
// Note: When creating a TaskDialogPage instance, these four properties
// contain default/empty control instances (for better Designer support) that
// do not show up in the dialog unless you modify their properties
// (because their initial "Text" is null and initial ProgressBarState is "None" -
// however when you create a new ProgressBar instance, its default State
// is "Normal"), but you can also set them to null.
public TaskDialogVerificationCheckBox? Verification { get; set; }
public TaskDialogExpander? Expander { get; set; }
public TaskDialogFootnote? Footnote { get; set; }
public TaskDialogProgressBar? ProgressBar { get; set; }
// See section "Event Cycle" for a diagram illustrating the events.
// Raised after this TaskDialogPage is bound to a TaskDialog (and therefore
// the controls have been created): after the dialog was created (directly after event
// TaskDialog.Opened/TDN_CREATED) or navigated (in the TDN_NAVIGATED handler).
public event EventHandler? Created;
// Raised when this TaskDialogPage is about to be unbound from a TaskDialog
// (and therefore the controls are about to be destroyed): when the dialog is
// about to be destroyed (directly before event TaskDialog.Closed) or about
// to navigate.
public event EventHandler? Destroyed;
// Raised when the user presses F1 or clicks the "Help" standard button
public event EventHandler? HelpRequest;
protected internal void OnCreated(EventArgs e);
protected internal void OnDestroyed(EventArgs e);
protected internal void OnHelpRequest(EventArgs e);
}public class TaskDialogIcon : IDisposable
{
// "Standard" icons
public static readonly TaskDialogIcon None;
public static readonly TaskDialogIcon Information;
public static readonly TaskDialogIcon Warning;
public static readonly TaskDialogIcon Error;
public static readonly TaskDialogIcon Shield;
public static readonly TaskDialogIcon ShieldBlueBar;
public static readonly TaskDialogIcon ShieldGrayBar;
public static readonly TaskDialogIcon ShieldWarningYellowBar;
public static readonly TaskDialogIcon ShieldErrorRedBar;
public static readonly TaskDialogIcon ShieldSuccessGreenBar;
public TaskDialogIcon(Bitmap image);
public TaskDialogIcon(Icon icon);
public TaskDialogIcon(IntPtr iconHandle);
// Note: This will throw (InvalidOperationException) if this is an
// instance representing a standard icon.
public IntPtr IconHandle { get; }
}public abstract class TaskDialogControl
{
public TaskDialogPage? BoundPage { get; }
public object? Tag { get; set; }
}public class TaskDialogButton : TaskDialogControl
{
public TaskDialogButton();
public TaskDialogButton(string? text, bool enabled = true, bool allowCloseDialog = true);
public static TaskDialogButton OK { get; }
public static TaskDialogButton Cancel { get; }
public static TaskDialogButton Abort { get; }
public static TaskDialogButton Retry { get; }
public static TaskDialogButton Ignore { get; }
public static TaskDialogButton Yes { get; }
public static TaskDialogButton No { get; }
public static TaskDialogButton Close { get; }
// Note: Clicking the "Help" button will not close the dialog, but will
// raise the TaskDialogPage.Help event.
public static TaskDialogButton Help { get; }
public static TaskDialogButton TryAgain { get; }
public static TaskDialogButton Continue { get; }
// Properties "Enabled" and "ShowShieldIcon" can be set while
// the dialog is shown.
public string? Text { get; set; }
public bool AllowCloseDialog { get; set; }
public bool Enabled { get; set; }
public bool ShowShieldIcon { get; set; }
// Setting "Visible" to false means the button will not be shown in the task dialog
// (the property cannot be set while the dialog is shown). This allows you
// to intercept button click events, e.g. "Cancel" when "AllowCancel" is true
// and the user presses ESC, without having to actually show a "Cancel" button.
public bool Visible { get; set; }
// Raised when this button is clicked while the dialog is shown (either because the
// user clicked it, or by calling PerformClick() or TaskDialog.Close()).
public event EventHandler? Click;
public override bool Equals(object? obj);
public override int GetHashCode();
public void PerformClick();
public override string ToString();
public static bool operator ==(TaskDialogButton? b1, TaskDialogButton? b2);
public static bool operator !=(TaskDialogButton? b1, TaskDialogButton? b2);
}public sealed class TaskDialogCommandLinkButton : TaskDialogButton
{
public TaskDialogCommandLinkButton();
public TaskDialogCommandLinkButton(string? text, string? descriptionText = null, bool enabled = true, bool allowCloseDialog = true);
public string? DescriptionText { get; set; }
}public sealed class TaskDialogRadioButton : TaskDialogControl
{
public TaskDialogRadioButton();
public TaskDialogRadioButton(string? text);
public string? Text { get; set; }
// Properties "Enabled" and "Checked" can be set while the dialog is shown
// (but "Checked" can then only be set to "true").
public bool Checked { get; set; }
public bool Enabled { get; set; }
// Raised when the "Checked" property changes while the dialog is shown (because
// the user clicked one of the radio buttons, or due to setting the "Checked"
// property of one of the radio buttons to "true").
public event EventHandler? CheckedChanged;
public override string ToString();
}public sealed class TaskDialogVerificationCheckBox : TaskDialogControl
{
public TaskDialogVerificationCheckBox();
public TaskDialogVerificationCheckBox(string? text, bool isChecked = false);
public string? Text { get; set; }
// "Checked" can be set while the dialog is shown.
public bool Checked { get; set; }
// Raised when the "Checked" property changes while the dialog is shown (because
// the user clicked it or due to setting the "Checked" property).
public event EventHandler? CheckedChanged;
public override string ToString();
public static implicit operator TaskDialogVerificationCheckBox(string verificationText);
}public sealed class TaskDialogExpander : TaskDialogControl
{
public TaskDialogExpander();
public TaskDialogExpander(string? text);
// "Text" can be set while the dialog is shown.
public string? Text { get; set; }
public string? ExpandedButtonText { get; set; }
public string? CollapsedButtonText { get; set; }
// Note: "Expanded" can NOT be set while the dialog is shown.
public bool Expanded { get; set; }
public TaskDialogExpanderPosition Position { get; set; }
// Raised when the "Expanded" property changes while the dialog is shown (because
// the user clicked the expando button).
public event EventHandler? ExpandedChanged;
public override string ToString();
}public sealed class TaskDialogFootnote : TaskDialogControl
{
public TaskDialogFootnote();
public TaskDialogFootnote(string? text);
// Properties "Text", "Icon" can be set while
// the dialog is shown (see comments for TaskDialogPage.Icon).
public string? Text { get; set; }
public TaskDialogIcon? Icon { get; set; }
public override string ToString();
public static implicit operator TaskDialogFootnote(string footnoteText);
}public sealed class TaskDialogProgressBar : TaskDialogControl
{
public TaskDialogProgressBar();
public TaskDialogProgressBar(TaskDialogProgressBarState state);
// Properties "State", "Minimum", "Maximum", "Value", "MarqueeSpeed" can
// be set while the dialog is shown.
public TaskDialogProgressBarState State { get; set; } // "Style"?
public int Minimum { get; set; }
public int Maximum { get; set; }
public int Value { get; set; }
public int MarqueeSpeed { get; set; }
}// Note: The button order in this collection is not necessarily the same as the actual
// order of buttons displayed in the dialog. See:
// https://github.com/kpreisser/winforms/issues/5#issuecomment-584318609
public class TaskDialogButtonCollection : Collection<TaskDialogButton>
{
public TaskDialogButtonCollection();
public TaskDialogButton Add(string? text, bool enabled = true, bool allowCloseDialog = true);
protected override void ClearItems();
protected override void InsertItem(int index, TaskDialogButton item);
protected override void RemoveItem(int index);
protected override void SetItem(int index, TaskDialogButton item);
}public class TaskDialogRadioButtonCollection : System.Collections.ObjectModel.Collection<TaskDialogRadioButton>
{
public TaskDialogRadioButtonCollection();
public TaskDialogRadioButton Add(string? text);
protected override void ClearItems();
protected override void InsertItem(int index, TaskDialogRadioButton item);
protected override void RemoveItem(int index);
protected override void SetItem(int index, TaskDialogRadioButton item);
}public enum TaskDialogStartupLocation
{
CenterScreen = 0,
CenterOwner = 1
}// Rename to "TaskDialogProgressBarStyle"?
public enum TaskDialogProgressBarState
{
Normal = 0,
Paused = 1,
Error = 2,
Marquee = 3,
MarqueePaused = 4,
// "None" is used for the default ProgressBar instance in the TaskDialogPage so
// that you need to set the State to a different value (or create a new ProgressBar
// instance) to actually show a progress bar in the dialog.
None = 5
}public enum TaskDialogExpanderPosition
{
AfterText = 0,
AfterFootnote = 1
}Event Cycle
The events in the proposed API currently have the folowing cycle at runtime (the diagram illustrates navigating the dialog in the TaskDialogButton.Click event):
Caller Events
TaskDialog.ShowDialog();
↓
(Calls TaskDialogIndirect())
────────────>
↓ (Window handle available now)
Callback(TDN_CREATED) ─────────> TaskDialogPage[1].Created
↓ (Window becomes visible)
↓
(...)
↓
Callback(TDN_BUTTON_CLICKED) ──> TaskDialogButton.Click
↓
TaskDialogPage.Navigate() <───────
↓
─────────────────> TaskDialogPage[1].Destroyed
↓
<──────────
↓
Callback(TDN_NAVIGATED) ───────> TaskDialogPage[2].Created
↓
(...)
↓
Callback(TDN_BUTTON_CLICKED) ──> TaskDialogButton.Click
↓ (Window closes; Dialog no longer navigable as it set a result button)
↓
Callback(TDN_DESTROYED) ───────> TaskDialogPage[2].Destroyed
↓ (Window handle no longer available)
<────────────
(TaskDialogIndirect() returns)
↓
(TaskDialog.ShowDialog() returns)
Implementation
The proposed API is implemented with PR #1133.
API Updates
Removed propertyTaskDialogContents.DoNotSetForegroundas it doesn't seem to have an effect- Removed base classes
TaskDialogControlCollectionandTaskDialogButtonCollection TaskDialogCustomButtonCollectionandTaskDialogRadioButtonCollectioninherit fromCollectioninstead ofKeyedCollection- Added an implicit cast operator from
TaskDialogButtonstoTaskDialogCommonButtonCollection - Removed property
ResultVerificationFlagChangedfromTaskDialog - Renamed property
ExpandedByDefaulttoExpanded(TaskDialogExpander) (so its value will be updated when the user clicks the expando button) - Removed non-predefined icons (that were used from
imageres.dll) - Class
TaskDialogextendsSystem.ComponentsModel.Component(and is disposable) - Added
Tagproperty toTaskDialogControl - Class
TaskDialogCommonButtonnow has a default constructor (like other control classes) - Renamed properties/events (e.g.
MainInstruction->Instruction,Content->Text,ButtonClicked->Click) - Properties and events of
TaskDialogRadioButtonandTaskDialogVerificationCheckboxhas been aligned with WinForms concepts (propertyChecked, eventCheckedChanged). - Renamed class
TaskDialogVerificationCheckboxtoTaskDialogCheckBox(along with properties) - Created struct
TaskDialogProgressBarRangeto be used for theTaskDialogProgressBar.Rangeproperty instead of(int min, int max)for better designer support - Restored property
TaskDialogContents.DoNotSetForegroundas it is actually working. - Removed
TaskDialogBooleanStatusEventArgs. - Remaned
TaskDialogProgressBarStateenum valueMarqueeDisabledtoMarqueePaused - Made class
TaskDialogControlabstract - Renamend enum value
TaskDialogIcon.StoptoError - Removed the
TaskDialogProgressBar.Rangeproperty (along with theTaskDialogProgressBarRangestruct) and instead added propertiesMinimumandMaximumdirectly on theTaskDialogProgressBarand also renamed propertyPositiontoValue, to simplify the API and align with the WinForms ProgressBar - Removed the WPF types
- Extracted the footer-specific properties on
TaskDialogContents(FooterText,FooterIcon,FooterIconHandle) into their ownTaskDialogFooterclass. The reasoning for this is that a separate dialog area is added for the footer when it is used (as shown in the below image), similar to the expander (and it reduces the number of properties inTaskDialogContents).
Also, when you intially don't create a footer when showing the task dialog, you cannot later add one by updating theFooterTextproperty, similar to theTextproperty of theExpander(which is different from the other text properties likeTaskDialogContents.TextandInstructionthat can be added later).
A separateTaskDialogFooterclass that inherits fromTaskDialogControlcan thus share the behavior withtaskDialogExpanderto throw anInvalidOperationExceptionwhen trying to update itsTextproperty but the control wasn't created because it was initiallynull(orstring.Empty).
- Renamed events
TaskDialog.ClosingtoClosedandTaskDialogContents.DestroyingtoDestroyed, and added a newTaskDialog.Closingevent that is called directly after aTaskDialogButton.Clickevent if the button would close the dialog, and it allows to cancel the close (similar toForm.FormClosingevent in WinForms) - see this comment (Option B). - Renamed property
TaskDialogExpander.ExpandoButtonClickedtoExpandedChanged - Renamed class
TaskDialogContentstoTaskDialogPageand propertyTaskDialog.CurrentContentstoTaskDialog.Page. This is because the documentation also talks about "navigating to a new page" - for example see Multi-page dialog boxes. - Removed the
TimerTickevent onTaskDialogPage:
This event represents theTDN_TIMERnotification that is called every 200 ms if theTDF_CALLBACK_TIMERflag was set in the task dialog config. The previous implementation specified the flag if the event handler has been set (like the implementation in the Windows API Code Pack did), but this means you could not add an event handler to theTimerTickevent after the dialog is displayed/navigated. Also, the interval of200is fixed (and a user might get the impression that the dialog can only be updated every 200 ms, which is not the case).
Instead, the user can use one of the already existing UI timer implementations likeSystem.Windows.Forms.Timer. Both the Task Dialog timer and the WinForms Timer use a Windows timer (WM_TIMERmessages), so using the WinForms timer should have the same behavior as the TaskDialog timer but with more flexibility. - Moved property
StartupLocationfromTaskDialogPagetoTaskDialogbecause it only has an effect when showing the dialog (but not when navigating it) and therefore isn't related to the page (which represents the contents of the dialog). Added eventsEdit: Removed these again because of an unresolved issue when closing the dialog.TaskDialog.ActivatedandTaskDialog.Deactivated.- Added event
TaskDialog.Shown(similar toForm.Shown). - Renamed class
TaskDialogCommonButtontoTaskDialogStandardButton(along with collections and property names). - Moved property
TaskDialogPage.DoNotSetForegroundtoTaskDialogbecause it only has an effect when showing the dialog, but not when navigating it. - Unified mutually exclusive properties
Icon+IconHandleonTaskDialogPageandTaskDialogFooterinto a singleIconproperty use subclasses to differentiate between icon types (see Windows Task Dialog #146 (comment)).
This should avoid confusion about having two mutually exclusive properties (and it allows to initially not showing an icon but then updating it to one using a handle (without using navigation)).
Additionally, it will allow us in the future to add an additional icon type that represents integer/string resource icons (e.g. fromimageres.dllor the application's executable), which could also be shown using a colored bar (which is not possible when using a handle). - Renamed property
TaskDialogPage.CommandLinkModetoCustomButtonStyle(along with the enum). TaskDialogno longer inherits fromSystem.ComponentModel.Componentwhich was used for trying to implement designer support, but that would require additional work. It be revisited for a future version.- Renamed event
TaskDialogPage.HelptoHelpRequest(and methodOnHelptoOnHelpRequest) as discussed in Implement the Windows Task Dialog #1133. - Renamed property
TaskDialog.DoNotSetForegroundtoTaskDialog.SetToForeground(the default value is still false), as per the feedback in Implement the Windows Task Dialog #1133. - Enabled nullable reference types.
- Made events nullable (see Make all event types nullable coreclr#25752).
- API Review Feedback:
- Renamed method group
TaskDialog.ShowtoShowDialog. - Renamed property
TaskDialogPage.InstructiontoMainInstruction(same with parameter names for the staticTaskDialog.Showmethods). - Renamed property
TaskDialogPage.TitletoCaption(same with parameter names for the staticTaskDialog.Showmethods). - Removed class
TaskDialogButtonClickedEventArgsand instead added boolean propertyTaskDialogButton.ShouldCloseDialogthat allows to specify whether clicking the button should close the task dialog. - Removed types
TaskDialogStandardIconandTaskDialogIconHandle, and instead added static fields onTaskDialogIconfor the standard icons, and added a constructor taking an icon or icon handle.
- Renamed method group
- Added an
intindexer toTaskDialogStandardButtonCollectionto avoid an overload resolution in the C# compiler for expressions likepage.StandardButtons[0]. See Implement the Windows Task Dialog #1133 (comment) - Changes from Streamlime footer control kpreisser/winforms#1:
- Added implicit cast operator from
stringtoTaskDialogFooter.
- Added implicit cast operator from
- Replaced property
TaskDialogExpander.ExpandFooterAreawithPosition(using enum typeTaskDialogExpanderPosition). - Added properties
TaskDialogPage.BoundDialogandTaskDialogControl.BoundPage, so that it is possible e.g. to access the currentTaskDialoginstance in a button click handler. See discussion here. - Renamed icons
SecurityShield,SecurityShieldBlueBar,SecurityShieldGrayBar,SecurityWarningYellowBar,SecurityErrorRedBar,SecuritySuccessGreenBartoShield,ShieldBlueBar,ShieldGrayBar,ShieldWarningYellowBar,ShieldErrorRedBar,ShieldSuccessGreenBar; as the term "security" would imply that such icons will/must be used for security purposes. - Changes from Tweak api kpreisser/winforms#3:
- Renamed
TaskDialogPage.CanBeMinimizedtoAllowMinimize. - Renamed
TaskDialogButton.ShouldCloseDialogtoAllowCloseDialog. - Add optional parameters to the
TaskDialogStandardButtonandTaskDialogCustomButtonconstructors and to theTaskDialogStandardButtonCollection.AddandTaskDialogCustomButtonCollection.Addmethods.
- Renamed
- Simplified type of event
TaskDialogButton.ClickfromEventHandler<EventArgs>toEventHandler. - Removed hyperlink functionality for now - see Disable TaskDialog "hyperlink" functionality kpreisser/winforms#4.
- Refactored Button API - see Rework task dialog button API kpreisser/winforms#12
- Streamlined single/multipage API - see Streamline single/multipage API kpreisser/winforms#14
- Move instance property
StartupLocationto a parameter ofShowDialog()- see Set dialog properties viaShowDialogkpreisser/winforms#16 - Allow to create
TaskDialogIconfrom aBitmap- see Simplify setting dialog icon from bitmaps kpreisser/winforms#15 - Renamed property
TaskDialogPage.MainInstructiontoHeading- see [minor]MainInstructionis confusing kpreisser/winforms#6 - Renamed class
TaskDialogFooter(and corresponding properties) toTaskDialogFootnote- see Consider renamingFootertoFootnotekpreisser/winforms#8 - Renamed
TaskDialogStartupLocation.CenterParenttoCenterOwner - Removed method
TaskDialogCheckBox.Focus() - Renamed property
TaskDialogButton.ElevationRequiredtoShowShieldIcon - Renamed class
TaskDialogCheckBox(and corresponding properties) to `TaskDialogVerificationCheckBox´ - see Streamlime verification control kpreisser/winforms#18 - Removed property
TaskDialogPage.Width
Possible API TODOs
- Maybe rename
TaskDialogProgressBarStatetoTaskDialogProgressBarStyle - Maybe add property
TagonTaskDialogPage(which is already present onTaskDialogControl) - Check how method
ShowDialog()should behave. Currently, it either shows the dialog modal (when specifying an owner) or non-modal, but in both cases the method does not return until the dialog has closed (similar toForm.ShowDialog()), which is the behavior of the nativeTaskDialogIndirectfunction.
This is the same as withMessageBox.Show(); however, theMessageBoxautomatically uses the current foreground window as owner when you don't specify the owner. For the Task Dialog however, it definitely should be possible to show it non-modal.
Note that this means you can show multiple non-modal dialogs at once, but each open dialog will add aTaskDialog.Show()entry in the call stack.
API Usage Notes
- Because some parts of the Task Dialog can be updated while it is shown (while others cannot), properties that cannot be updated while the dialog is shown will throw an
InvalidOperationException(this was also the behavior of the task dialog implementation in the Windows API Code Pack).
For example, radio buttons cannot be unselected while the dialog is shown (but they can be selected). This means that assigningfalseto theTaskDialogRadioButton.Checkedproperty (while the radio button is shown in a task dialog) will throw. - The button order in the
TaskDialogButtonCollectiondoes not necessarily reflect the order in which the task dialog actually displays the buttons (since common buttons are specified by flags in theTASKDIALOGCONFIGstructure, whereas custom buttons are stored in an array).
The native task dialog displays buttons from the collection in the following order:- Custom Buttons/Command Links in their relative order from the collection
- Standard Buttons in an OS-defined order:
OKYesNoAbortRetryCancelIgnoreTryAgainContinueCloseHelp
TaskDialog.ShowDialog()can return aTaskDialogButtonwhich was not added to the dialog in the following cases (which results from the native task dialog implementation):- No button has been added to the dialog, in which case it automatically gets an OK button, and so the
TaskDialogButton.OKbutton is returned when the user clicks it. - No Cancel button has been added to the dialog, but the dialog is programmatically closed by calling the
Close()method, orTaskDialogPage.AllowCancelhas been set and the dialog is closed by the user by pressing ESC or Alt+F4 or clicking the X button in the title bar. In these cases theTaskDialogButton.Cancelbutton will be returned.
This can also happen when a non-modal task dialog is shown but the main window of the app is closed, in which case the task dialog is also closed and returns aCancelresult.
- No button has been added to the dialog, in which case it automatically gets an OK button, and so the
- It is possible to use a color bar with a different icon (one of the predefined icons or an icon resource from
imageres.dll), by initially specifying one of theShield...Baricons, and when the dialog is shown, update it to a different icon:

However, it isn't possible to use a color bar with a icon handle, because after showing the dialog you can only update the icon member that was initially used to show a dialog, and specifying a color bar requires to use the non-handle icon member.
This means currently you can only use one of the standard icons with a color bar, but in a later version we could add support for showing icons from integer/string resources of DLLs/EXEs (e.g. fromimageres.dll) (by specifying thehInstancefield of the nativeTASKDIALOGCONFIGstruct), which would then allow you to show a custom icon with a colored bar.






