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>();
private string _btnResult;
private List<string> _lResults = new List<string>();
public const char escDef = '\xd';
public const char escChk = '\xc';
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 string
s 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;
else if (res == "Skip") continue;
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 string
s, 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();
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 string
s 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 checkbox
es 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);
_bSign = (astr[2][0] == CMsgDlg.escChk);
_bShow = (astr[3][0] == CMsgDlg.escChk);
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 string
s 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) {
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:
public string Result(double perc, string text = "") {
try {
if (_prgBar != null) {
if (perc <= 1.0) perc *= 100;
_prgBar.Value = (int)(perc + 0.5);
}
if (text != "") _lbl.Text = text;
}
catch (Exception) {
}
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.
public void Cancel(bool bAsk = false) {
if (bAsk) {
this.Visible = false;
string lbl = _lbl.Text;
if (ShowDialog("Cancel the current operation?", this.Text, "Continue")
== "Continue") {
_lbl.Text = lbl;
_buttons.Last().Dispose();
_buttons.Remove(_buttons.Last());
this.Visible = true;
Form1.TheForm().MsgDlg = this;
return;
}
}
_lbl.Text = "Closing...";
_btnResult = "Cancel";
}
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 checkbox
es above the textbox
es 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.