Windows Task Dialog
See original GitHub issueHi,
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 theHandle
property 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
- <del>Removed property
TaskDialogContents.DoNotSetForeground
as it doesn’t seem to have an effect</del> - Removed base classes
TaskDialogControlCollection
andTaskDialogButtonCollection
TaskDialogCustomButtonCollection
andTaskDialogRadioButtonCollection
inherit fromCollection
instead ofKeyedCollection
- Added an implicit cast operator from
TaskDialogButtons
toTaskDialogCommonButtonCollection
- Removed property
ResultVerificationFlagChanged
fromTaskDialog
- Renamed property
ExpandedByDefault
toExpanded
(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
TaskDialog
extendsSystem.ComponentsModel.Component
(and is disposable) - Added
Tag
property toTaskDialogControl
- Class
TaskDialogCommonButton
now has a default constructor (like other control classes) - Renamed properties/events (e.g.
MainInstruction
->Instruction
,Content
->Text
,ButtonClicked
->Click
) - Properties and events of
TaskDialogRadioButton
andTaskDialogVerificationCheckbox
has been aligned with WinForms concepts (propertyChecked
, eventCheckedChanged
). - Renamed class
TaskDialogVerificationCheckbox
toTaskDialogCheckBox
(along with properties) - Created struct
TaskDialogProgressBarRange
to be used for theTaskDialogProgressBar.Range
property instead of(int min, int max)
for better designer support - Restored property
TaskDialogContents.DoNotSetForeground
as it is actually working. - Removed
TaskDialogBooleanStatusEventArgs
. - Remaned
TaskDialogProgressBarState
enum valueMarqueeDisabled
toMarqueePaused
- Made class
TaskDialogControl
abstract
- Renamend enum value
TaskDialogIcon.Stop
toError
- Removed the
TaskDialogProgressBar.Range
property (along with theTaskDialogProgressBarRange
struct) and instead added propertiesMinimum
andMaximum
directly on theTaskDialogProgressBar
and also renamed propertyPosition
toValue
, 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 ownTaskDialogFooter
class. 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 theFooterText
property, similar to theText
property of theExpander
(which is different from the other text properties likeTaskDialogContents.Text
andInstruction
that can be added later).
A separateTaskDialogFooter
class that inherits fromTaskDialogControl
can thus share the behavior withtaskDialogExpander
to throw anInvalidOperationException
when trying to update itsText
property but the control wasn’t created because it was initiallynull
(orstring.Empty
). - Renamed events
TaskDialog.Closing
toClosed
andTaskDialogContents.Destroying
toDestroyed
, and added a newTaskDialog.Closing
event that is called directly after aTaskDialogButton.Click
event if the button would close the dialog, and it allows to cancel the close (similar toForm.FormClosing
event in WinForms) - see this comment (Option B). - Renamed property
TaskDialogExpander.ExpandoButtonClicked
toExpandedChanged
- Renamed class
TaskDialogContents
toTaskDialogPage
and propertyTaskDialog.CurrentContents
toTaskDialog.Page
. This is because the documentation also talks about “navigating to a new page” - for example see Multi-page dialog boxes. - Removed the
TimerTick
event onTaskDialogPage
: This event represents theTDN_TIMER
notification that is called every 200 ms if theTDF_CALLBACK_TIMER
flag 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 theTimerTick
event after the dialog is displayed/navigated. Also, the interval of200
is 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_TIMER
messages), so using the WinForms timer should have the same behavior as the TaskDialog timer but with more flexibility. - Moved property
StartupLocation
fromTaskDialogPage
toTaskDialog
because 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). - <del>Added events
TaskDialog.Activated
andTaskDialog.Deactivated
.</del> Edit: Removed these again because of an unresolved issue when closing the dialog. - Added event
TaskDialog.Shown
(similar toForm.Shown)
. - Renamed class
TaskDialogCommonButton
toTaskDialogStandardButton
(along with collections and property names). - Moved property
TaskDialogPage.DoNotSetForeground
toTaskDialog
because it only has an effect when showing the dialog, but not when navigating it. - Unified mutually exclusive properties
Icon
+IconHandle
onTaskDialogPage
andTaskDialogFooter
into a singleIcon
property use subclasses to differentiate between icon types (see https://github.com/dotnet/winforms/issues/146#issuecomment-467032370). 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.dll
or the application’s executable), which could also be shown using a colored bar (which is not possible when using a handle). - Renamed property
TaskDialogPage.CommandLinkMode
toCustomButtonStyle
(along with the enum). TaskDialog
no longer inherits fromSystem.ComponentModel.Component
which was used for trying to implement designer support, but that would require additional work. It be revisited for a future version.- Renamed event
TaskDialogPage.Help
toHelpRequest
(and methodOnHelp
toOnHelpRequest
) as discussed in #1133. - Renamed property
TaskDialog.DoNotSetForeground
toTaskDialog.SetToForeground
(the default value is still false), as per the feedback in #1133. - Enabled nullable reference types.
- Made events nullable (see dotnet/coreclr#25752).
- API Review Feedback:
- Renamed method group
TaskDialog.Show
toShowDialog
. - Renamed property
TaskDialogPage.Instruction
toMainInstruction
(same with parameter names for the staticTaskDialog.Show
methods). - Renamed property
TaskDialogPage.Title
toCaption
(same with parameter names for the staticTaskDialog.Show
methods). - Removed class
TaskDialogButtonClickedEventArgs
and instead added boolean propertyTaskDialogButton.ShouldCloseDialog
that allows to specify whether clicking the button should close the task dialog. - Removed types
TaskDialogStandardIcon
andTaskDialogIconHandle
, and instead added static fields onTaskDialogIcon
for the standard icons, and added a constructor taking an icon or icon handle.
- Renamed method group
- Added an
int
indexer toTaskDialogStandardButtonCollection
to avoid an overload resolution in the C# compiler for expressions likepage.StandardButtons[0]
. See https://github.com/dotnet/winforms/pull/1133#issuecomment-557483834 - Changes from kpreisser/winforms#1:
- Added implicit cast operator from
string
toTaskDialogFooter
.
- Added implicit cast operator from
- Replaced property
TaskDialogExpander.ExpandFooterArea
withPosition
(using enum typeTaskDialogExpanderPosition
). - Added properties
TaskDialogPage.BoundDialog
andTaskDialogControl.BoundPage
, so that it is possible e.g. to access the currentTaskDialog
instance in a button click handler. See discussion here. - Renamed icons
SecurityShield
,SecurityShieldBlueBar
,SecurityShieldGrayBar
,SecurityWarningYellowBar
,SecurityErrorRedBar
,SecuritySuccessGreenBar
toShield
,ShieldBlueBar
,ShieldGrayBar
,ShieldWarningYellowBar
,ShieldErrorRedBar
,ShieldSuccessGreenBar
; as the term “security” would imply that such icons will/must be used for security purposes. - Changes from kpreisser/winforms#3:
- Renamed
TaskDialogPage.CanBeMinimized
toAllowMinimize
. - Renamed
TaskDialogButton.ShouldCloseDialog
toAllowCloseDialog
. - Add optional parameters to the
TaskDialogStandardButton
andTaskDialogCustomButton
constructors and to theTaskDialogStandardButtonCollection.Add
andTaskDialogCustomButtonCollection.Add
methods.
- Renamed
- Simplified type of event
TaskDialogButton.Click
fromEventHandler<EventArgs>
toEventHandler
. - Removed hyperlink functionality for now - see kpreisser#4.
- Refactored Button API - see kpreisser/winforms#12
- Streamlined single/multipage API - see kpreisser/winforms#14
- Move instance property
StartupLocation
to a parameter ofShowDialog()
- see kpreisser/winforms#16 - Allow to create
TaskDialogIcon
from aBitmap
- see kpreisser/winforms#15 - Renamed property
TaskDialogPage.MainInstruction
toHeading
- see kpreisser/winforms#6 - Renamed class
TaskDialogFooter
(and corresponding properties) toTaskDialogFootnote
- see kpreisser/winforms#8 - Renamed
TaskDialogStartupLocation.CenterParent
toCenterOwner
- Removed method
TaskDialogCheckBox.Focus()
- Renamed property
TaskDialogButton.ElevationRequired
toShowShieldIcon
- Renamed class
TaskDialogCheckBox
(and corresponding properties) to `TaskDialogVerificationCheckBox´ - see kpreisser/winforms#18 - Removed property
TaskDialogPage.Width
Possible API TODOs
- Maybe rename
TaskDialogProgressBarState
toTaskDialogProgressBarStyle
- Maybe add property
Tag
onTaskDialogPage
(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 nativeTaskDialogIndirect
function.
This is the same as withMessageBox.Show()
; however, theMessageBox
automatically 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 assigningfalse
to theTaskDialogRadioButton.Checked
property (while the radio button is shown in a task dialog) will throw. - The button order in the
TaskDialogButtonCollection
does not necessarily reflect the order in which the task dialog actually displays the buttons (since common buttons are specified by flags in theTASKDIALOGCONFIG
structure, 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:
OK
Yes
No
Abort
Retry
Cancel
Ignore
TryAgain
Continue
Close
Help
TaskDialog.ShowDialog()
can return aTaskDialogButton
which 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.OK
button 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.AllowCancel
has 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.Cancel
button 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 aCancel
result.
- 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...Bar
icons, 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. from
imageres.dll
) (by specifying thehInstance
field of the nativeTASKDIALOGCONFIG
struct), which would then allow you to show a custom icon with a colored bar.
Issue Analytics
- State:
- Created 5 years ago
- Reactions:170
- Comments:94 (80 by maintainers)
Checking in to let you know that we’ve filed the Windows bug. We’re also trying to determine the best ship vehicle for this code. We’re evaluating the possibility of an optional ‘pack’ of components that are usable in both WinForms and WPF so that both technologies could use them. Stay tuned for updates, we haven’t forgotten this proposal!
Hi @AraHaan,
sorry, I’m not sure if I understand correctly what you mean with MessageBox using the application’s icon. Can you give an example?
Hi @KlausLoeffelmann,
do you mean screenshots of existing Windows applications using the Task Dialog from both Windows 7 and Windows 10, like these?
App not responding (Windows 7)
App not responding (Windows 10)
PC needs to be restarted (Windows 7)
PC needs to be restarted (Windows 10)
Network Access Warning (Windows 7) (when right-clicking on a .zip file within an UNC path)
Network Access Warning (Windows 10)
Network Access Error (Windows 7)
Network Access Error (Windows 10)
TortoiseGit (Windows 7)
TortoiseGit (Windows 10)
Visual Studio when app needs admin rights (Windows 7)
Visual Studio when app needs admin rights (Windows 10)
Custom Task Dialog (Windows 7)
Custom Task Dialog (Windows 10)
Custom Task Dialog (Windows Server 2019 Core with Server Core App Compatibility Feature on Demand)
Custom Task Dialog (Windows 7, Classic Theme)
Custom Task Dialog, 200% DPI (Windows 7)
Custom Task Dialog, 200% DPI (Windows 10)
(Windows 10 did some improvements for the Task Dialog layout with higher DPI settings.)
Edit: Added screenshots of TortoiseGit, and of the custom task dialog with Windows 7 classic theme and Windows Server 2019 Core with App Compatibility FoD.
Thanks!