Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Multipurpose Message Dialog

0.00/5 (No votes)
17 Apr 2018 2  
A C# class for a multipurpose Windows message dialog

Introduction

This article will describe a C# class for a multipurpose Windows message dialog with the ability for custom buttons, textboxes, checkboxes, and a progress bar indicator. The .NET provided MessageBox is quite versatile for general use, but there was a need to have buttons like “Continue” or “Skip”, and rather than make up a custom dialog for each type needed, this class was developed. Over time, additions to the class were added for a non-modal progress bar indicator, with the option to cancel or continue, and the ability to define and get input from checkboxes and textboxes. Controls are automatically positioned, with the window expanding if more room is needed, and defaults for buttons, checkboxes and textboxes can be defined.

Using the Code

The class CMsgDlg is derived from the Windows Form. A few member variables may require some explanation. Recall that the Windows Form MessageBox is made to use the usual “OK”, “Cancel”, “Yes”, “No”, buttons and the clicked button result is returned in a MessageBoxButtons object. Since this new class has buttons that can be customized, an alternative method of returning which button was clicked must be used. Buttons added to the form are defined by a string, and the button that was clicked is returned as this string. The dialog may be defined with multiple buttons and the default button can be set by passing the first character of the button text as an escape character (‘\xd’). Dialogs that are called using checkboxes or textboxes are returned as a List<string>. To set checkboxes to the checked state (default is not checked), the escape character ‘\xc’ can be used when defining the checkbox. There’s nothing special about the escape values used here, but they need to be something that would not be likely to be used as the button text, so they can be identified as such, and stripped off from the string before being used in the form.

private List<Button> _buttons = new List<Button>(); // Buttons list
private string _btnResult; // Button clicked
private List<string> _lResults = new List<string>(); // Return list

public const char escDef = '\xd'; // Default button
public const char escChk = '\xc'; // Default check

Custom Button Dialog

To display a dialog with customized buttons, this class member may be called:

public string ShowDialog(string text, string caption, string[] btnText) {...

where text is the message displayed in the dialog, caption becomes the form caption, and the array of strings btnText defines the button labels.

As an example, a dialog with three buttons "Filter", "Skip", and "Cancel" is called like this below. Each button will be placed in the form side by side, left to right, in the order as called. Note that the button that was clicked is just returned as a string. So for example, if the Cancel button was clicked, then the return string is “Cancel”.

string res = new CMsgDlg().ShowDialog("Warning - the chosen filter is non-standard.",
	"Filter Warning", new string[] { "Filter", "Skip", "Cancel" });

if (res == "Cancel") 
    break; // Exit loop
else if (res == "Skip") continue; // Don’t filter and continue to next iteration
// ...else just Filter it

Textbox Dialog

To display a textbox dialog, this method may be called:

public List<string> ShowDialogTextBox(string caption, string[] lbl_txtboxText,
            string[] btnText = null) {...

lbl_txtboxText is an array of strings, where the prompt string is the first element of lbl_txtboxText (“Enter units:” in example below), and the second element is the default for the textbox (in this case, a blank string). This second element also is used to define how wide the textbox should be, and the number of lines (by using '\n').

An example textbox dialog may be called like this:

List<string> astr = new CMsgDlg().ShowDialogTextBox("Units", 
    new string[] { "Enter units:", "                  " });

if (astr[0] == "Cancel") return;
string unit = astr[1].Trim(); // Entered string

The return List<string> astr has as its first element (astr[0]), the button that was clicked, and the second element (astr[1]), is the string that the user typed into the textbox. Note that lbl_txtboxText is an array of strings so that multiple textboxes could have been displayed onto the form, with the values of the array alternating between the textbox label, and the default string, textbox label, then default string, etc. The btnText is optional, and if not supplied, the buttons are defaulted to “OK” and “Cancel”, as is done in the example above.

A multi-line textbox will be produced by including a newline (‘\n’ for each extra line) in the textbox default (second element). This example code shown below will generate a two line textbox with default text defText. The extra blanks are added to generate a wider textbox.

List<string> lstr = new CMsgDlg().ShowDialogTextBox("Chart Edit", "Title:", defText + "        \n ");

Checkbox Dialog

The definition for the checkbox dialog is:

public List<string> ShowDialogCheckBox(string lblText, string caption, string[] chkBoxText,
    string[] btnText = null) {...

A dialog with checkboxes may be called like this:

string chk_distr = (_bDistr ? CMsgDlg.escChk.ToString() : "") + "Distribution statement";
string chk_Sign = (_bSign ? CMsgDlg.escChk.ToString() : "") + "Sign document";
string chk_Show = (_bShow ? CMsgDlg.escChk.ToString() : "") + "Show report updates";

List<string> astr = new CMsgDlg().ShowDialogCheckBox("Check all that apply.", "Report Options", 
    new string[] { chk_distr, chk_Sign, chk_Show }, new string[] { "\xdOK", "Cancel" });

if (astr[0] == "Cancel") return;

_bDistr = (astr[1][0] == CMsgDlg.escChk);  // Distr. statement
_bSign = (astr[2][0] == CMsgDlg.escChk);   // Sign
_bShow = (astr[3][0] == CMsgDlg.escChk);   // Show updates

The first three statements above define the checkbox labels and whether they should be defaulted to the “checked” state. For example, the first statement says if the bool _bDistr is true, then the escChk (escape character defined above in the class) is inserted as the first character in the text label “Distribution statement”. The member function checks for this to know if it should set the checkbox state to “checked”. If the escape character is not present, then the checkbox defaults to not checked. The 1st element astr[0] of the returned List<string> astr is the button clicked. If not the “Cancel” button, then the checkbox states are retrieved (escape characters) from the first character of these remaining astr elements; astr[1][0], astr[2][0], etc. So looking at the last three statements above: _bDistr is true if the first character of astr[1] is the escChk character, _bSign is true if the first character of astr[2] is the escChk character, etc. The strings are returned in the same order that they were passed to the method.

Progress Bar Dialog

The progress bar dialog method definition is:

public CMsgDlg ShowProgress(Form parent, string text, string caption, string btnText = "Cancel") {...

This will initialize and display a non-modal dialog for use in a loop, with a progress bar that gets updated with each iteration of the loop. Note that the parent Form is passed to this function. This is done strictly for positioning the dialog to default to the center of the parent form, because non-modal dialogs are not centered automatically. The btnText is optional and defaults to just the “Cancel” button.

An example call to this method:

CMsgDlg dlg = new CMsgDlg().ShowProgress(f1, "Running the Full Report script...", "Script");

int i = 0;
foreach (TreeNode node in chkNodes) { // For each checked node
    if (dlg.Result((double)++i / chkNodes.Count) == "Cancel") goto Exit;
    ...
}

If (dlg != null) dlg.Close();

After the instantiating call to CMsgDlg().ShowProgress, the dlg object is retained to be used in the foreach loop to have access to the class’s Result method to check if the user clicked the “Cancel” button, and to update the progress bar at each iteration of the loop.

The Result method is shown below:

//--------------------------------------------------------------------------
// Returns button result (for non-modal use) and updates progress by perc %
//--------------------------------------------------------------------------
public string Result(double perc, string text = "") {
    try {
        if (_prgBar != null) {
           if (perc <= 1.0) perc *= 100; // Convert to percentage
               _prgBar.Value = (int)(perc + 0.5);
        }
        if (text != "") _lbl.Text = text;
    }
    catch (Exception) { // Keep from crashing if passed invalid value
    }

    return _btnResult;
}

Note that an optional text string can be passed to change the text from its initial text (as called from ShowProgress) to, for example, “% completed”. The perc value is converted to a percentage if less than 1 and is used to show the progress bar’s percent complete. In the foreach loop above, this perc value is passed as a counter “i” divided by the total number of iterations chkNodes.Count. The counter “i” is incremented with each pass of the loop. The try block with the “null” catch ensures that even some erroneous call to the dialog that throws an exception doesn’t halt the caller’s loop. Note that it’s the responsibility of the calling function to close the dialog, e.g., after the calling loop is exited.

Even though this is a non-modal dialog, a button click can be responded to by the dialog. If the user clicks the Cancel button, the private member variable _btnResult is set in the btn_Click method. The btn_Click method will then invoke the Cancel method to generate a modal dialog which prompts the user to cancel or continue.

The Cancel method (shown below) can be called to just close the dialog, or (if bAsk = true) to prompt before closing. Following the prompt mode, it first makes the non-modal dialog non-visible, which effectively pauses the application. It calls the modal ShowDialog, adds a Continue button and changes the text message, asking the user if they want to cancel the operation. If Continue is chosen, then the Continue button is removed from the form, the original message text is restored, and the non-modal dialog is made visible again, thereby permitting the application to continue and the loop to resume. If Cancel is chosen, then “Cancel” is returned in _btnResult, which is returned by the Result method to the caller in the loop, and the loop is exited.

Note that after the form’s visible attribute is set to false and ShowDialog is called, the modal dialog has the same appearance as the progress bar dialog that was made non-visible (it has the Cancel button and progress bar), so just the Continue button needs to be added. Since the class hasn’t been re-instantiated, "this" is still refering to the current form, the same form which was made non-visible. So it effectively acts as if the non-modal dialog was changed to a modal dialog. When the form is made visible again, this causes it to continue, as it was, as a non-modal dialog.

//--------------------------------------------------------------------------
// Cancel handler for non-modal
//--------------------------------------------------------------------------
public void Cancel(bool bAsk = false) {
    if (bAsk) { // Ask 1st
        this.Visible = false;   // Disables non-modal dlg
        string lbl = _lbl.Text; // Save in case continue

        if (ShowDialog("Cancel the current operation?", this.Text, "Continue") 
		== "Continue") { // Already has Cancel btn
            _lbl.Text = lbl; // Restore
            _buttons.Last().Dispose(); // Continue btn
            _buttons.Remove(_buttons.Last());
            this.Visible = true; // Enable non-modal again
            Form1.TheForm().MsgDlg = this; // Restore for form - Close() nulls

            return;
        }
    }

    _lbl.Text = "Closing...";
    _btnResult = "Cancel"; // Set to close on next Result() check
}

When the form is non-modal, it doesn’t always have the focus, so depending on how much computation is done in the loop, it may not respond right away. The class also has a KeyDown handler looking for the Esc key, which is often responded to more promptly than the Cancel button click. The main application can also have a KeyDown event looking for the Esc key, and so that’s why the Cancel method is declared public, so that the main application key handler can call Cancel as well. The main application (Form1) needs to have access to the CMsgDlg object (to call Cancel), so when ShowProgress is first called, a public global variable in Form1 is assigned the “this” object so it will have access to the Cancel method. This Form1 global is also restored here in the Cancel method because closing the modal dialog (after the button click) sets it to null. Note: If the Form1 code is not used, it can, of course, be commented out or removed.

The class also contains a ShowDialog method (shown declared below) that can utilize checkboxes and textboxes on the same form, although it is currently designed to put the checkboxes above the textboxes only.

public List<string> ShowDialog(string text, string caption, string[] btnText, 
    string[] chkBoxText = null, string[] txtboxText = null) {

Using the Class with Pre-designed Forms

Although this class was developed to handle most kinds of simple user dialogs on its own, there often is still a requirement for designing a more complicated input form, perhaps with dynamic requirements (i.e., not known at design time). For example, there was a need to have a dialog with dynamically added textboxes based on how many bookmarks were found in a Word template document. So a form was defined through the designer (not by this class) and the AddTextBox method was made public so it could be called externally multiple times to add a textbox for each bookmark onto the designed form. In this case, since the class’s default form was not used, AddTextBox needs to know what form to add the textbox onto, and so this is why there’s an alternate constructor to the class declared as: public CMsgDlg(Control form). So for more general usage, the methods AddTextBox and AddCheckBox can be defined as public so that these methods can be called with the alternate constructor (with the form passed) so the controls can be added from an external pre-designed window’s form, and the positioning of the controls is still handled by the CMsgDlg class.

Conclusion

A multipurpose message dialog class was discussed which can have multiple custom labeled buttons, checkboxes, textboxes, or a progress bar. Some of the class methods may be used to add textboxes or checkboxes to external dialogs so that these components can be added dynamically (i.e., at run-time). The class is by no means exhaustive of all message dialog capabilities, but will hopefully offer the developer a good starting point for a more powerful general purpose message dialog.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here