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

Popup Dialogues in ASP.NET using DIV Tag

0.00/5 (No votes)
21 Mar 2011 1  
Embedding simple popup dialogues in an ASP.NET page

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...

sample screenshot

Creating the Derived Class

The object model is extremely simple.

class diagram

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)
    {
      // Prompt the user.
      Label legend = new Label();
      legend.Text = "Password";
      controlContainer.Controls.Add(legend);
      controlContainer.Controls.Add(new LiteralControl("<br />"));

      // Set up somewhere for the user to enter his or her password.
      TextBox password = new TextBox();
      password.TextMode = TextBoxMode.Password;
      // Make very sure that we give the control an ID otherwise we won't
      // be able to retrieve the value.
      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.

  // Our page_load code.
  protected override void OnLoad(EventArgs e)
  {
    base.OnLoad(e);

    // Manage postbacks and data binding of controls...
    if (!preservePageData(Request))
      resetPageData();

    // ...do whatever else is necessary for this page...

    // ...and (re)load our dialogue,
    loadPasswordDialogue();
  }

  // Setting up the dialogue.
  private void loadPasswordDialogue()
  {
    // Each dialogue on the page requires a key.
    // This should either be a constant or generated in a deterministic manner.
    ConfirmPassword settings = new ConfirmPassword("Confirm Password",
                                                   "confirmPassword");
    settings.Height = Unit.Pixel(120);
    settings.Width = Unit.Pixel(200);

    // The dialogue (which is just a DIV remember) needs to be added
    // to the form's control collection.
    Page.Form.Controls.Add(settings);

    // Tie the dialogue to the control (such as a button) that will be
    // used to display the dialogue.
    settings.BoundControl = accountSettings;

    // Set up the code that'll act on the returned data.
    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)
  {
    // Dialogue data is returned as nameValueCollection.
    NameValueCollection data = (NameValueCollection)e.CommandArgument;

    if (!DialogueBase.IsCancelled(data))
    {
      // The ID assigned to each control is used to name its returned value
      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:

    // A notional page load sequence for a filtered gridview
    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);

      if (!preservePageData(Request))
        resetPageData();

      loadSearchInstruction(Request);            // Retrieve any filtering info.

      // Use return values to filter data / do whatever.

      loadSearchDialogue();                      // (re)create the search dialogue.

   }

  // Extraction of filtering information.
  private void loadSearchInstruction(HttpRequest Request)
  {
    NameValueCollection data;

    // Has a search dialogue just posted back? If so it takes precedence
    // over any previously stored search instruction.

    // Dose the postback data contain search instructions?
    if (DialogueBase.IsDialogue("searchDialogue", Request))
    {
      // What are the search instructions?
      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...

  // ...and then each input control is marked as owned by this dialogue so
  // so we can find them again easily and fish the values out of them prior
  // to postback.
  foreach (object ctl in childControls.Controls)
  {
    try
    {
      // If the control type isn't on this list we ignore it.
      // It's either for layout or not supported.
      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 {/*NOP*/}
  }

...and a key method in the JavaScript is...

  // Walk the document looking for elements identified with a div based
  // dialogue and retrieve the values entered by the user.
  function __getValues(controlID, dialogueKey, dialogueID, unadornedName)
  {
    // Use non-printing chars to delimit return value so that we don't
    // have to restrict what a user may enter.
    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))
      {

        // Separate multiple return values.
        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;
      }
    }

    // Return value with header to identify the dialogue.
    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)
{
  // Pass in the ID of the DIV and get it from
  // the DOM.  Gets around the intermittent render problem.
  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

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