Introduction
A common requirement in all but the most trivial browser based applications is to show popup dialogues and this is often achieved by displaying a new window. If the application is being written for an intranet where a great deal of control over browser type and configuration is possible, then the new window approach works well, but not otherwise.
A more up to date alternative is to use AJAX and if you hunt around on CodeProject and elsewhere, you'll find some good examples of AJAX based popup dialogues. I'm not aware of any significant drawbacks to AJAX, but it does require you to have an AJAX toolset installed.
Late last summer, I found myself needing a popup dialogue, I didn't want to use a popup window and I didn't have an AJAX toolkit. What I did have was a context (popup) menu class derived from the context menu control described by Dino Esposito in MSDN Magazine, see Adding a Context Menu to ASP.NET Controls. It occurred to me that it might be possible to provide simple embedded dialogues for ASPX pages using a hidden <div>
as an alternative to opening a new window or using an AJAX based solution.
This isn't an especially novel technique, if you poke around, you'll find other examples of the same basic idea; such as Custom Javascript Dialog in ASP.NET.
Overview
To reiterate a point made above, as presented, the code is only suitable for simple dialogues. If you can achieve what you want with the following control types, then it may be of use to you.
asp:TextBox
asp:RadioButtonList
asp:Checkbox
asp:DropDownList
There are two main sections to this solution:
- The
DialogueBase
class to render the dialogue and an...
- ...associated handful of JavaScript methods to extract the data.
DialogueBase
is based on the
ContextMenu
class in the MSDN article with some changes to suit my preferred way of working together with some additional methods to provide for the addition of controls other than "menu items" (
ContextMenuButtons
).
The main steps required to create a popup dialogue are:
- Derive a class from
DialogueBase
- Create an instance of the derived class in code
- Write code to handle returned values. Either:
- In the
DialogueClose
event handler or...
- ...in the
OnLoad
event handler.
Class: DialogueBase
The main public
and protected
methods of the class are:
Method |
Visibility |
Returns |
Comment |
AddDialogueButtons |
protected virtual |
Panel |
If necessary, the derived class can setup custom buttons and button handling. |
AddDialogueControls |
protected abstract |
Panel |
To be implemented by the derived class |
AddDialogueTitle |
protected virtual |
Panel |
If desired, the derived class can create a custom title layout. |
CustomCancel |
protected |
string |
Provides an alternative postback call to the default ContextMenuButton postback for the Cancel button |
CustomOK |
protected |
string |
Provides an alternative postback call to the default ContextMenuButton postback for the OK button |
DialogueData |
public static |
NameValueCollection |
The data returned by the dialogue. |
IsCancelled |
public static |
bool |
Returns true if the dialogue's cancel button was clicked |
IsDialogue |
public static |
bool |
Returns true if HTTPRequest contains data for a named dialogue |
Example: Password Confirmation Dialogue
For the sake of an example, we'll define a password confirmation dialogue which is to be opened up when a user clicks on a button to request a change to account settings. Something like this...
Creating the Derived Class
The object model is extremely simple.
Because the DialogueBase
owes its existence to a ContextMenu
class and also because I'm a lazy so and so the buttons on the dialogue are instances of ContextMenuButton
.
Step 1 - Define the dialogue class
All the derived class has to provide is a constructor and an implementation for the AddDialogueControls
method.
public class ConfirmPassword : DialogueBase
{
public ConfirmPassword(String dialogueCaption,
String dialogueKey):base()
{
DialogueKey = dialogueKey;
DialogueTitle = dialogueCaption;
}
protected override Panel AddDialogueControls(Panel controlContainer)
{
Label legend = new Label();
legend.Text = "Password";
controlContainer.Controls.Add(legend);
controlContainer.Controls.Add(new LiteralControl("<br />"));
TextBox password = new TextBox();
password.TextMode = TextBoxMode.Password;
password.ID = "myPassword";
password.AutoPostBack = false;
controlContainer.Controls.Add(password);
return controlContainer;
}
}
This example shows how to create a single purpose dialogue. However, it's fairly obvious that a general purpose dialogue class, such as might be needed for searches, can be written. Instead of defining its own controls as shown, a search dialogue class would have to accept a suitably configured collection of controls at instantiation.
Step 2 - Add the dialogue to the page
Create an instance of the dialogue as part of the page load sequence and add it to the form's control collection.
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (!preservePageData(Request))
resetPageData();
loadPasswordDialogue();
}
private void loadPasswordDialogue()
{
ConfirmPassword settings = new ConfirmPassword("Confirm Password",
"confirmPassword");
settings.Height = Unit.Pixel(120);
settings.Width = Unit.Pixel(200);
Page.Form.Controls.Add(settings);
settings.BoundControl = accountSettings;
settings.DialogueClose +=new CommandEventHandler(onSettingsClose);
}
I generally prefer to work with as little as possible in markup and add controls in code. However if you wanted to add the dialogue to markup rather than add it in code, you would have to provide the ConfirmPassword
class with an argumentless constructor. If you do, then be aware that the BoundControl
property cannot be set in markup.
Step 3 - Add the code to retrieve returned dialogue data
There are two ways of retrieving the data. The first is to attach an event handler to the dialogue's DialogueClose
event as shown above. The second is to extract it directly from the HTTPRequest
object using static
methods supplied in the DialogueBase
class.
Dialogue Close Handling
This is best suited to situations where the processing to be carried out has no effect on data binding or can otherwise be deferred to a point after page load.
void onSettingsClose(object sender, CommandEventArgs e)
{
NameValueCollection data = (NameValueCollection)e.CommandArgument;
if (!DialogueBase.IsCancelled(data))
{
String password = data["myPassword"];
Label1.Text = string.Format("Your password is : {0}",password);
}
}
Direct Extraction
This is best suited to situations where the data returned from the dialogue will affect data binding, say the filtering of GridView
data, or if there is processing driven by the dialogue return values that must be dealt with during page load.
The DialogueBase
methods that meet this need are:
Method |
Purpose |
IsDialogue |
Check to see if the postback is for a named dialogue |
DialogueData |
Retrieve any data returned by the dialogue |
IsCancelled |
Was the dialogue cancelled? |
Example:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (!preservePageData(Request))
resetPageData();
loadSearchInstruction(Request);
loadSearchDialogue();
}
private void loadSearchInstruction(HttpRequest Request)
{
NameValueCollection data;
if (DialogueBase.IsDialogue("searchDialogue", Request))
{
data = DialogueBase.DialogueData(Request);
if (DialogueBase.IsCancelled(data))
Session.Remove("searchParams");
else
Session.Add("searchParams", data);
}
}
Operation
The creation and rendering of a dialogue is fairly standard and rather than waste space here, I suggest you read the MDSN article. The only difference worth noting is the addition of two custom attributes to each supported control type.
Custom Attribute |
Purpose |
DialogueTag |
Identifies the control as belonging to a particular dialogue. This is why each dialogue must have a unique (to the page) key. |
UnadornedName |
Simplifies the retrieval of the parameter name, especially where Master Pages are in use. |
Identifying the controls this way makes it easier to write the JavaScript to retrieve the dialogue values.
The names assigned to these attributes are not important so long as they do not clash with existing official attributes.
...from DialogueBase::buildDialogue
...
foreach (object ctl in childControls.Controls)
{
try
{
if (ctl.GetType() == typeof(TextBox) ||
ctl.GetType() == typeof(CheckBox) ||
ctl.GetType() == typeof(RadioButtonList) ||
ctl.GetType() == typeof(DropDownList))
{
WebControl c = (WebControl)ctl;
c.Attributes.Add(DialogueTag, this.DialogueKey);
c.Attributes.Add(UnadornedName, c.ID);
}
}
catch {}
}
...and a key method in the JavaScript is...
function __getValues(controlID, dialogueKey, dialogueID, unadornedName)
{
var nullValue = String.fromCharCode(2);
var GS = String.fromCharCode(29);
var RS = String.fromCharCode(30);
var returnVal = '';
for (i=0; i<document.all.length; i++)
{
if (null != document.all(i).getAttribute(dialogueKey)
&& dialogueID == document.all(i).getAttribute(dialogueKey))
{
if (returnVal.length > 0)
{
returnVal = returnVal + RS;
}
var v = nullValue;
var id = document.all(i).getAttribute(unadornedName);
var tagName = document.all(i).tagName.toLowerCase();
switch (tagName)
{
case 'select':
v = getValueSelect(i);
break;
default:
v = getValueInput(i);
break;
}
returnVal = returnVal + id +'=' + v;
}
}
returnVal = dialogueID + GS + returnVal;
__doPostBack(controlID, returnVal);
}
These are the methods to modify if you want to extend the range of supported control types for dialogues.
Points of Interest
An amusing little bug that I tripped over whilst writing this article and which hadn't shown up in the project for which the solution was originally developed involved the generation of the script for the onclick
attribute of the dialogue's bound control.
The relevant code was:
From DialogueBase
:
private const string attachDialogue = "return __showDialogue({0}) ";
and from the JavaScript:
function __showDialogue(dialogue)
{
var offsetY = 5;
var offsetX = 40;
dialogue.style.left = window.event.x + offsetX;
dialogue.style.top = window.event.y + offsetY;
dialogue.style.display = "";
window.event.cancelBubble = true;
return false;
}
which would render the dialogue's bound control as:
<input type="submit"
name="accountSettings"
value="Account Settings"
onclick="return __showDialogue(3e64d51970824b73afedbb0c84490821);"
id="accountSettings" />
The __showDialogue
call was handed a reference to the bound control. In the project for which it was originally developed, this caused no problems whatsoever, but in the demonstration project, it gave rise to intermittent "expected ) at line ... char ..." errors when the browser (Internet Explorer 8) tried to render the page.
I have no idea why this should be the case. I can only speculate that it is down to the order in which the document elements are built and that sometimes the dialogue element wasn't quite ready.
Once I'd worked out what might be going on the solution proved quite straightforward. Hand in the ID as a string and recover the element in JavaScript.
private const string attachDialogue = "return __showDialogue('{0}') ";
function __showDialogue(dialogue)
{
dialogue = document.all(dialogue);
var offsetY = 5;
var offsetX = 40;
dialogue.style.left = window.event.x + offsetX;
dialogue.style.top = window.event.y + offsetY;
dialogue.style.display = "";
window.event.cancelBubble = true;
return false;
}
The dialogue's bound control is now rendered as:
<input type="submit"
name="accountSettings"
value="Account Settings"
onclick="return __showDialogue('3e64d51970824b73afedbb0c84490821');"
id="accountSettings" />
and everything appears to work as I would want.
Limitations
Limitations that I'm aware of include:
- Dialogues have to be bound to a "clickable" control such as a
Button
or ImageButton
- Dialogues only support input from:
asp:TextBox
asp:RadioButtonList
asp:Checkbox
asp:DropDownList
- The JavaScript that retrieves the dialogue data may break if changes are made to the rendering of controls.
- Controls on the dialogue must have
Autopostback
set false
- Only simple name/value pairs are returned
- JavaScript has to be enabled
- As written, you're stuck with an OK and a Cancel button
- You have to validate entered data on postback
The autopostback restriction may become a problem as it rules out the use of the more sophisticated controls such as the calendar. The other limitations are less worrying as the current range of supported input controls do all that I require at the moment and it's always possible to change the DialogueBase
and its associated JavaScript values should additional control types be required.
A more serious potential drawback is the use of the custom attributes. At the moment, Internet Explorer 8 is happy to ignore them. This may not always be the case and it may be that other browsers and future versions of Internet Explorer will not accept document elements with custom attributes. We shall see.
The JavaScript that retrieves the dialogue data won't win any prizes for elegance either, but I might get around to tidying it up. One day.
Advantages
Provided you can live with the acknowledged limitations, this solution has a number of things in its favour:
- No need to configure the browser to allow popup windows
- Dialogue data available in page on postback
- Minimal additional infrastructure required
- No need for AJAX
History
- Summer 2010 - Initial investigations
- Xmas 2010 - Sort of working
- Spring 2011 - Tidied up (a bit) for CodeProject article