question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Windows Task Dialog

See original GitHub issue

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: taskdialog-vs

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 the Handle 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...
    }

simpletaskdialog

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...");
    }

grafik

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);
        }
    }

wizarddialog

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");

taskdialog-savedocument

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.");

taskdialog-minesweeperdifficulty

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");
    }

taskdialog-windowssecurity

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.");

autoclosingdialog

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 and TaskDialogButtonCollection
  • TaskDialogCustomButtonCollection and TaskDialogRadioButtonCollection inherit from Collection instead of KeyedCollection
  • Added an implicit cast operator from TaskDialogButtons to TaskDialogCommonButtonCollection
  • Removed property ResultVerificationFlagChanged from TaskDialog
  • Renamed property ExpandedByDefault to Expanded (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 extends System.ComponentsModel.Component (and is disposable)
  • Added Tag property to TaskDialogControl
  • 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 and TaskDialogVerificationCheckbox has been aligned with WinForms concepts (property Checked, event CheckedChanged).
  • Renamed class TaskDialogVerificationCheckbox to TaskDialogCheckBox (along with properties)
  • Created struct TaskDialogProgressBarRange to be used for the TaskDialogProgressBar.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 value MarqueeDisabled to MarqueePaused
  • Made class TaskDialogControl abstract
  • Renamend enum value TaskDialogIcon.Stop to Error
  • Removed the TaskDialogProgressBar.Range property (along with the TaskDialogProgressBarRange struct) and instead added properties Minimum and Maximum directly on the TaskDialogProgressBar and also renamed property Position to Value, 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 own TaskDialogFooter 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 in TaskDialogContents).
    Also, when you intially don’t create a footer when showing the task dialog, you cannot later add one by updating the FooterText property, similar to the Text property of the Expander (which is different from the other text properties like TaskDialogContents.Text and Instruction that can be added later).
    A separate TaskDialogFooter class that inherits from TaskDialogControl can thus share the behavior with taskDialogExpander to throw an InvalidOperationException when trying to update its Text property but the control wasn’t created because it was initially null (or string.Empty).
    taskdialog-footer
  • Renamed events TaskDialog.Closing to Closed and TaskDialogContents.Destroying to Destroyed, and added a new TaskDialog.Closing event that is called directly after a TaskDialogButton.Click event if the button would close the dialog, and it allows to cancel the close (similar to Form.FormClosing event in WinForms) - see this comment (Option B).
  • Renamed property TaskDialogExpander.ExpandoButtonClicked to ExpandedChanged
  • Renamed class TaskDialogContents to TaskDialogPage and property TaskDialog.CurrentContents to TaskDialog.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 on TaskDialogPage: This event represents the TDN_TIMER notification that is called every 200 ms if the TDF_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 the TimerTick event after the dialog is displayed/navigated. Also, the interval of 200 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 like System.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 from TaskDialogPage to TaskDialog 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 and TaskDialog.Deactivated.</del> Edit: Removed these again because of an unresolved issue when closing the dialog.
  • Added event TaskDialog.Shown (similar to Form.Shown).
  • Renamed class TaskDialogCommonButton to TaskDialogStandardButton (along with collections and property names).
  • Moved property TaskDialogPage.DoNotSetForeground to TaskDialog because it only has an effect when showing the dialog, but not when navigating it.
  • Unified mutually exclusive properties Icon+IconHandle on TaskDialogPage and TaskDialogFooter into a single Icon 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. from imageres.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 to CustomButtonStyle (along with the enum).
  • TaskDialog no longer inherits from System.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 to HelpRequest (and method OnHelp to OnHelpRequest) as discussed in #1133.
  • Renamed property TaskDialog.DoNotSetForeground to TaskDialog.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 to ShowDialog.
    • Renamed property TaskDialogPage.Instruction to MainInstruction (same with parameter names for the static TaskDialog.Show methods).
    • Renamed property TaskDialogPage.Title to Caption (same with parameter names for the static TaskDialog.Show methods).
    • Removed class TaskDialogButtonClickedEventArgs and instead added boolean property TaskDialogButton.ShouldCloseDialog that allows to specify whether clicking the button should close the task dialog.
    • Removed types TaskDialogStandardIcon and TaskDialogIconHandle, and instead added static fields on TaskDialogIcon for the standard icons, and added a constructor taking an icon or icon handle.
  • Added an int indexer to TaskDialogStandardButtonCollection to avoid an overload resolution in the C# compiler for expressions like page.StandardButtons[0]. See https://github.com/dotnet/winforms/pull/1133#issuecomment-557483834
  • Changes from kpreisser/winforms#1:
    • Added implicit cast operator from string to TaskDialogFooter.
  • Replaced property TaskDialogExpander.ExpandFooterArea with Position (using enum type TaskDialogExpanderPosition).
  • Added properties TaskDialogPage.BoundDialog and TaskDialogControl.BoundPage, so that it is possible e.g. to access the current TaskDialog instance in a button click handler. See discussion here.
  • Renamed icons SecurityShield, SecurityShieldBlueBar, SecurityShieldGrayBar, SecurityWarningYellowBar, SecurityErrorRedBar, SecuritySuccessGreenBar to Shield, 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 to AllowMinimize.
    • Renamed TaskDialogButton.ShouldCloseDialog to AllowCloseDialog.
    • Add optional parameters to the TaskDialogStandardButton and TaskDialogCustomButton constructors and to the TaskDialogStandardButtonCollection.Add and TaskDialogCustomButtonCollection.Add methods.
  • Simplified type of event TaskDialogButton.Click from EventHandler<EventArgs> to EventHandler.
  • 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 of ShowDialog() - see kpreisser/winforms#16
  • Allow to create TaskDialogIcon from a Bitmap - see kpreisser/winforms#15
  • Renamed property TaskDialogPage.MainInstruction to Heading - see kpreisser/winforms#6
  • Renamed class TaskDialogFooter (and corresponding properties) to TaskDialogFootnote - see kpreisser/winforms#8
  • Renamed TaskDialogStartupLocation.CenterParent to CenterOwner
  • Removed method TaskDialogCheckBox.Focus()
  • Renamed property TaskDialogButton.ElevationRequired to ShowShieldIcon
  • Renamed class TaskDialogCheckBox (and corresponding properties) to `TaskDialogVerificationCheckBox´ - see kpreisser/winforms#18
  • Removed property TaskDialogPage.Width

Possible API TODOs

  • Maybe rename TaskDialogProgressBarState to TaskDialogProgressBarStyle
  • Maybe add property Tag on TaskDialogPage (which is already present on TaskDialogControl)
  • 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 to Form.ShowDialog()), which is the behavior of the native TaskDialogIndirect function.
    This is the same as with MessageBox.Show(); however, the MessageBox 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 a TaskDialog.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 assigning false to the TaskDialogRadioButton.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 the TASKDIALOGCONFIG structure, whereas custom buttons are stored in an array). The native task dialog displays buttons from the collection in the following order:
    1. Custom Buttons/Command Links in their relative order from the collection
    2. Standard Buttons in an OS-defined order:
      1. OK
      2. Yes
      3. No
      4. Abort
      5. Retry
      6. Cancel
      7. Ignore
      8. TryAgain
      9. Continue
      10. Close
      11. Help
  • TaskDialog.ShowDialog() can return a TaskDialogButton 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, or TaskDialogPage.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 the TaskDialogButton.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 a Cancel result.
  • 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 the Shield...Bar icons, and when the dialog is shown, update it to a different icon: taskdialog-colorbar-customicon 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 the hInstance field of the native TASKDIALOGCONFIG struct), which would then allow you to show a custom icon with a colored bar.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:170
  • Comments:94 (80 by maintainers)

github_iconTop GitHub Comments

9reactions
merriemcgawcommented, Mar 22, 2019

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!

9reactions
kpreissercommented, Feb 21, 2019

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) taskdialog-appnotresponding-win7

App not responding (Windows 10) taskdialog-appnotresponding-win10

PC needs to be restarted (Windows 7) taskdialog-mustrestart-win7

PC needs to be restarted (Windows 10) taskdialog-mustrestart-win10

Network Access Warning (Windows 7) (when right-clicking on a .zip file within an UNC path) taskdialog-networksecurity-win7

Network Access Warning (Windows 10) taskdialog-networksecurity-win10

Network Access Error (Windows 7) taskdialog-networkerror-win7

Network Access Error (Windows 10) taskdialog-networkerror-win10

TortoiseGit (Windows 7) taskdialog-tortoisegit-win7

TortoiseGit (Windows 10) taskdialog-tortoisegit-win10

Visual Studio when app needs admin rights (Windows 7) taskdialog-vs-win7

Visual Studio when app needs admin rights (Windows 10) taskdialog-vs-win10

Custom Task Dialog (Windows 7) taskdialog-allelements-win7

Custom Task Dialog (Windows 10) taskdialog-allelements-win10

Custom Task Dialog (Windows Server 2019 Core with Server Core App Compatibility Feature on Demand) taskdialog-allelements-svr2019appcompatfod

Custom Task Dialog (Windows 7, Classic Theme) taskdialog-allelements-win7classic

Custom Task Dialog, 200% DPI (Windows 7) taskdialog-allelements-200pct-win7

Custom Task Dialog, 200% DPI (Windows 10) taskdialog-allelements-200pct-win10

(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!

Read more comments on GitHub >

github_iconTop Results From Across the Web

About Task Dialogs - Win32 apps
In this article. A task dialog is a dialog box that can be used to display information and receive simple input from the...
Read more >
Task Dialog - Win32 apps
The task dialog contains application-defined icons, messages, title, verification check box, command links, push buttons, and radio buttons.
Read more >
TaskDialog function (commctrl.h) - Win32 apps
The TaskDialog function creates, displays, and operates a task dialog. The task dialog contains application-defined message text and title, ...
Read more >
TaskDialog Class (System.Windows.Forms)
A task dialog allows to display information and get simple input from the user. It is similar to a MessageBox (in that it...
Read more >
Using Task Dialogs - Win32 apps
A task dialog is created and shown by using either the TaskDialog function or the TaskDialogIndirect function.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found