Cutting Edge
Modal Dialog Boxes with AJAX
Dino Esposito
Code download available at:CuttingEdge2008_Launch.exe(419 KB)
Contents
The ModalPopupExtender Control
Closing the Popup Using the Esc Key
Adding Animation on Display
Returning Data to the Server
It's Your Turn
Dialog boxes have been around in Windows® for a long time, and they do have their advantages. But if you want your Web application to have dialog boxes, you're basically stuck with popups, and, as you know, most users disable them with popup blockers. So what are you to do if you need a popup dialog?
With Microsoft® ASP.NET AJAX, dialog boxes are especially important for displaying context-sensitive information without a page reload or new page. It is therefore important to devise a different implementation of dialog boxes that are as effective as modal popups but hassle-free for users.
Neither ASP.NET nor AJAX extensions have built-in support for popups or Web dialog boxes; however, the AJAX Control Toolkit does. The AJAX Control Toolkit is a shared-source library of ASP.NET extender controls. One of its most useful controls is the ModalPopupExtender control that I briefly introduced in the January 2008 installment of Cutting Edge (msdn.microsoft.com/msdnmag/issues/08/01/CuttingEdge). The ModalPopup extender takes the markup generated by a server-side ASP.NET panel and shows or hides it as the user clicks on an associated HTML element. Initially styled as hidden, the dialog box is downloaded onto the client when the page is loaded and then shown or hidden on demand. How is modality guaranteed? Take a look at the code snippet in Figure 1.
Figure 1 Details of the ModalPopupExtender Control
// Code excerpted from modalpopupbehavior.js. // Method initialize() // // Download the source code of the AJAX Control Toolkit from // https://www.codeplex.com/atlascontroltoolkit. // // The panel defined as the popup is saved as the foreground element. this._foregroundElement = this._popupElement; // A new DIV tag is created as the background element for the panel. // The panel is invisible and placed at 0,0 coordinates (fixed position). // In addition, the background element is given a high z-index so that it // sits above everything else. this._backgroundElement = document.createElement('div'); this._backgroundElement.id = this.get_id() + '_backgroundElement'; this._backgroundElement.style.display = 'none'; this._backgroundElement.style.position = 'fixed'; this._backgroundElement.style.left = '0px'; this._backgroundElement.style.top = '0px'; this._backgroundElement.style.zIndex = 10000; // The background element is styled as specified. if (this._BackgroundCssClass) { this._backgroundElement.className = this._BackgroundCssClass; } // The background element is appended to the parent of the foreground // element. this._foregroundElement.parentNode.appendChild(this._backgroundElement); // The foreground element is programmatically hidden from view. In // addition, it is given absolute positioning and an higher z-index than // the background element. this._foregroundElement.style.display = 'none'; this._foregroundElement.style.position = 'fixed'; this._foregroundElement.style.zIndex = $common.getCurrentStyle( this._backgroundElement, 'zIndex', this._backgroundElement.style.zIndex) + 1; // A click handler is added to the target element of the extender. In // this case, It is the DOM element whose ID is passed on as the // TargetControlID property. this._showHandler = Function.createDelegate(this, this._onShow); $addHandler(this.get_element(), 'click', this._showHandler);
The code shows an excerpt from the script used to initialize the ModalPopup extender on the client. Note that an ASP.NET AJAX extender usually comprises a server control—the extender—and a client behavior class written in JavaScript. The code in Figure 1 comes from the ModalPopup extender's client behavior class. If you download the source code for the AJAX Control Toolkit from codeplex.com/atlascontroltoolkit, you'll find the aforementioned script code in the modalpopupbehavior.js file.
As you can see in Figure 1, the root element of the dialog's markup tree is designated as the foreground element and programmatically hidden from view. At the same time, a DIV tag is dynamically created and designated as the background element. The tag is given a fixed position at coordinates 0,0 and styled appropriately. The DIV tag is also given an extremely high (but arbitrary) z-index that virtually ensures it will sit on top of all other elements in the document, since the z-index property sets the stack order of an HTML element, and the element with the greatest stack order is always displayed on top of elements with lower stack order.
The DIV is then added to the Document Object Model (DOM) as a child of the foreground element's parent. Finally, the foreground element—the dialog box content—is given a z-index just higher than that of the background.
The net effect of this intermediate DIV is that a new transparent element is layered over all page elements except the popup panel. A click handler is also dynamically applied to the extender controls target in order to ensure that any user input performed outside the popup panel (on the foreground element) is lost and never reaches the intended target. This is the behavior that ensures modality.
The content of the server panel you define in the source ASPX page is popped up as a modal window, just like a classic Windows message box. At the same time, it delivers an unprecedented level of UI flexibility—after all, it's an ASP.NET panel, so you can fill it with any combination of controls you like, and you can style it however you like.
The ModalPopupExtender Control
Setting up a modal popup using the AJAX Control Toolkit library is a relatively simple task. You start by defining a panel to provide the UI, and then add a button control to trigger the display of the dialog:
<asp:Button runat="server" ID="btnEditCustomer" Text="Edit text" /> <asp:Panel runat="server" ID="pnlEditCustomer"> ... </asp:Panel>
Next, you set up the extender and specify the target control ID and the popup control ID:
<act:ModalPopupExtender ID="ModalPopupExtender1" runat="server" TargetControlID="btnEditCustomer" PopupControlID="pnlEditCustomer" BackgroundCssClass="modalBackground" OkControlID="editBox_OK" OnOkScript="yes()" />
The target control ID of a ModalPopup extender is the ID of the server control that, when clicked, causes the dialog box to pop up. The popup control ID is the ID of the server control that provides the content for the dialog box.
The AJAX Control Toolkit framework does not provide any default style for the dialog box. The user has to employ the visual elements in the panel to dismiss the dialog box. Figure 2 shows a sample dialog box in action. It allows confirmation and provides some information. But if you want context-sensitive dialogs where some further user interaction is required, you need to do some more work.
Figure 2** Sample Web Modal Dialog Box in Action **(Click the image for a larger view)
To make the ModalPopup extender more like that of a Windows dialog box, a number of features can be added. For instance, the ability to dismiss the dialog box by simply pressing the Esc key—a common functionality in Windows but not supported yet in the AJAX Control Toolkit—is a good idea.
Before I implement any of these features, let's briefly review the methods and properties of the ModalPopupExtender control. Figure 3 lists its properties but does not include all the properties automatically inherited from base classes.
Figure 3 Properties of the ModalPopupExtender Control
Property | Description |
---|---|
BackgroundCssClass | The CSS class linked to the host application to apply to any content underneath the modal popup when the popup is displayed. |
DropShadow | Boolean property that indicates whether the extender will automatically add a drop shadow to the modal popup. True by default. |
OkCancelID | ID of the DOM element in the displayed UI that dismisses the modal popup with a Cancel action. |
OkControlID | ID of the DOM element in the displayed UI that dismisses the modal popup with an OK action. |
OkCancelScript | The name of a JavaScript function linked to the page to run when the modal popup is dismissed with the Cancel action. |
OnOkScript | The name of a JavaScript function linked to the page to run when the modal popup is dismissed with the OK action. |
PopupControlID | ID of the root element in the DOM tree to display as the content of the modal popup. |
PopupDragHandleControlID | ID of the embedded element that contains the header/title that will be used as a drag handle for the whole popup. |
RepositionMode | Indicates whether the popup needs to be repositioned when the browser window is resized or scrolled. Feasible values belong to the ModalPopupRepositionMode enumerated type. |
TargetControlID | ID of the DOM element that, when clicked, activates the modal popup. |
X | X coordinate of the top-left corner of the modal popup. |
Y | Y coordinate of the top-left corner of the modal popup. |
The signature of the ModalPopupExtender control class is shown here:
public class ModalPopupExtender : DynamicPopulateExtenderControlBase
The base class here is a library-defined extender that provides DynamicPopulate support to multiple extenders. DynamicPopulate is another extender in the AJAX Control Toolkit that replaces the markup of a DOM element with text returned by a Web service call. The ModalPopup extender has dependencies on other AJAX Control Toolkit extenders including DropShadow and DragPanel.
The content of the dialog box is fully determined by a DOM sub-tree rooted in an element specified through the PopupControlID property. Quite often, this element will be an ASP.NET server control such as a Panel. The position of the popup is determined by the X and Y properties, but the popup will be centered horizontally if no coordinates are specified.
Just as in any classic Windows dialog box, the Web modal popup can be dragged around. To enable this, you specify in the PopupDragHandleControlID property the ID of the element to be used as the drag handle, and the embedded script does the rest. Note that you can drag the modal popup around, but only within the page area covered by the DOM. In other words, if your page area is defined by a DIV with a height of 100 pixels, and you display the page in a browser window of 1600 by 1024 pixels, you can only drag the popup vertically for 100 pixels, regardless of the physical height of the browser window.
By setting the RepositionMode property to any value but "None" (the default), you enable the behavior's script to update the position of the popup when the user scrolls or resizes the Web browser window (see Figure 4).
Figure 4** Repositioning the Modal Dialog **(Click the image for a larger view)
Closing the Popup Using the Esc Key
In Windows, the user can dismiss a message or dialog box by pressing the Esc key. In the AJAX Control Toolkit, this ability is not present natively in the modal popup behavior. As you'll see in a moment, though, detecting and handling the Esc key is not a big issue. Consider the following JavaScript code, which is attached to the KeyDown event of the page's DOM:
function OnKeyPress(args) { if(args.keyCode == Sys.UI.Key.esc) { $find("ModalPopupExtender1").hide(); } }
The code is triggered by keystrokes that affect the entire document. The handler receives a Sys.UI.DomEvent object through the args parameter. The object describes the mouse and keyboard status when a DOM-level event is caught. In particular, the keyCode property indicates the ASCII code of the key pressed. Additional information about the Ctrl, Shift, and Alt keys is available, as well as information about the mouse position and button status. The Sys.UI.Key enumeration lists some predefined constants for the code of the most commonly used keys, including Esc, Enter, and Del buttons. Using these tools, detecting if the Esc key is pressed is a breeze. Now let's see how you can programmatically hide the popup.
An AJAX Control Toolkit extender has a client and server programming interface. The server interface is an ASP.NET server control; the client interface is a JavaScript behavior class. This class exposes a JavaScript programming model that mirrors the server-side programming model, so each of the properties in Figure 3 has its JavaScript counterpart. The container of these properties is an object with the ModalPopup extender control ID. This control is not part of the DOM, as it is created by the infrastructure of the Microsoft AJAX Library. Hence, you can't use the $get function to get an instance of a behavior class; you must use the $find function instead. Both the server and client API of the modal popup have show and hide methods to control visibility.
Note that the OnKeyPress event handler will not work unless you register it with the Microsoft AJAX Library. The best place to run this registration code is the pageLoad function, like so:
function pageLoad(sender, args) { $addHandler(document, "keydown", OnKeyPress); }
The Microsoft AJAX Library invokes the pageLoad function when all of its startup tasks have completed and everything in the library and the page has been initialized. The library also automatically wires its loading stage to any JavaScript function with that name that can be found in the page. Now, any modal popup managed through the AJAX Control Toolkit's ModalPopup extender can be dismissed with the Esc key.
Adding Animation on Display
With this completed, we can progress to other things. Wouldn't you love to achieve the same kind of fade-in effect Windows Vista® uses for your Web applications? The effect is built-in for regular Windows-managed windows, such as the window that pops up when the window.alert method is invoked. On custom modal popups, like those I'm building here, you have to provide for it yourself.
Animation may become a native feature of the ModalPopup extender in the near future, but, for the time being, getting it to work is relatively easy thanks to the animation API already available in the AJAX Control Toolkit library. The following code refers to the Animation extender, which supports animation for a number of common predefined DOM events such as OnLoad, OnClick, OnMouseOver, and OnMouseOut. The TargetControlID points to the DOM element whose events will trigger the animation:
<act:AnimationExtender ID="popUpAnimation" runat="server" TargetControlID="btnViewMore"> <Animations> <OnClick> <Parallel AnimationTarget="pnlViewCustomer" Duration=".3" Fps="25"> <FadeIn /> </Parallel> </OnClick> </Animations> </act:AnimationExtender>
Animations can be combined in a sequence, and a few can even be played in parallel. The preceding code just fades in the panel associated with a modal popup for the specified duration and number of frames after the button is clicked. The code below shows a more sophisticated animation that executes multiple effects:
<act:AnimationExtender ID="popUpAnimation" runat="server" TargetControlID="btnViewMore"> <Animations> <OnClick> <Parallel AnimationTarget="pnlViewCustomer" Duration=".3" Fps="25"> <Move Horizontal="100" Vertical="100" /> <Resize Width="280" Height="180" /> <Color PropertyKey="backgroundColor" StartValue="#FFFFFF" EndValue="#FFFF00" /> </Parallel> </OnClick> </Animations> </act:AnimationExtender>
Here, the modal popup is first moved to a new relative position and then resized and colored. The gallery of examples at asp.net/AJAX/AjaxControlToolkit/Samples/Animation/Animation.aspx shows some demos; however, most of them can't be applied to a modal popup without modifying the source code. The main issue lies in the fact that the modal popup is displayed before the animation starts. While that might work for a simple fade-in effect, if you want to create explosions or wipe-out effects, you need to get clever.
The ModalPopup extender fires a few helpful events on the client, including the showing event. Here's how you can subscribe to it:
function pageLoad(sender, args) { $find("ModalPopupExtender1").add_showing(onModalShowing); }
Any code you associate with the showing event runs just before the popup is displayed. You can use this event to perform any client-side initialization tasks. Other client events to which you can bind include hiding, hidden, and shown.
Initializing the Elements of the Popup
On the client, the ModalPopup extender toggles the visibility of the DOM tree, as identified by the PopupControlID attribute. In the source code for the client behavior, you'll see additional code to intercept markup that was downloaded from the server as a constituent part of the host page, and you can style it for display.
For this reason you can't make changes to the template or content of the popup once the page has been served. Consider the following scenario. Your user chooses a customer from, say, a dropdown list and views some details. Next, he may want to edit some of the customer information. In a traditional Web application, you just redirect the user to a new page or, perhaps, make a postback to load a different view. In ASP.NET 2.0 and newer versions, this is what the DetailsView control does. In a desktop application, you would use a modal dialog box. For AJAX-enabled applications, a Web dialog box is now an option.
The dialog box used to edit a customer, however, has a fixed layout that is populated with new content every time it is going to pop up. Suppose that you originate a full postback when a customer is selected. In the selection-changed event, you update the customer view and optionally the controls in the modal popup. In this way, when the button is clicked to pop up the dialog box the latest information is already loaded.
But an AJAX application doesn't normally perform full postbacks. Figure 5 shows a fragment of an ASP.NET AJAX page that uses partial rendering to update a customer view instead. The updatable region is linked to the SelectedIndexChanged event on the child dropdown list control. The dropdown list, in turn, has the AutoPostBack property set to true. The net effect is that whenever the user changes the selection in the dropdown list, the table in Figure 5 is updated without a full page reload. So far, so good.
Figure 5 Using Partial Rendering to Update the Customer View
<table> <tr> <td valign="top"> <b>Customers</b><br /> <asp:DropDownList id="ddlCustomers" runat="server" DataSourceID="odsCustomers" DataTextField="CompanyName" DataValueField="ID" AutoPostBack="true" OnSelectedIndexChanged="ddlCustomers_SelectedIndexChanged" ondatabound="ddlCustomers_DataBound" /> <asp:ObjectDataSource ID="odsCustomers" runat="server" TypeName="IntroAjax.CustomerManager" SelectMethod="LoadAll"> </asp:ObjectDataSource> </td> <td valign="top"> <asp:UpdatePanel ID="UpdatePanel1" runat="server"> <ContentTemplate> <table> <tr> <td><b>Customer ID:</b></td> <td> <asp:Label runat="server" id="lblCustomerID" /> </td> </tr> <tr> <td><b>Company Name:</b></td> <td> <asp:Label runat="server" id="lblCompanyName" /> </td> </tr> <tr> <td><b>Contact Name:</b></td> <td> <asp:Label runat="server" id="lblContactName" /> </td> </tr> <tr> <td><b>Country:</b></td> <td><asp:Label runat="server" id="lblCountry" /></td> </tr> </table> </ContentTemplate> <Triggers> <asp:AsyncPostBackTrigger ControlID="ddlCustomers" EventName="SelectedIndexChanged" /> </Triggers> </asp:UpdatePanel> </td> </tr> </table>
The next step entails updating the panel in Figure 6, which represents the content of the modal popup. The panel must be in sync with the customer's view.
Figure 6 The Content of a Modal Popup Supports Partial Rendering
<asp:Panel ID="pnlEditCustomer" runat="server" CssClass="modalPopup" style="display:none"> <div style="margin:10px"> <asp:UpdatePanel runat="server" ID="ModalPanel1" RenderMode="Inline" UpdateMode="Conditional"> <ContentTemplate> <table> <tr> <td><b>Customer ID:</b></td> <td> <asp:Label runat="server" id="editCustomerID" /> </td> </tr> <tr> <td><b>Company Name:</b></td> <td> <asp:TextBox runat="server" id="editTxtCompanyName" /> </td> </tr> <tr> <td><b>Contact Name:</b></td> <td> <asp:TextBox runat="server" id="editTxtContactName" /> </td> </tr> <tr> <td><b>Country:</b></td> <td> <asp:TextBox runat="server" id="editTxtCountry" /> </td> </tr> </table> <hr /> <asp:Button ID="btnApply" runat="server" Text="Apply" OnClick="btnApply_Click" /> </ContentTemplate> </asp:UpdatePanel> <asp:Button ID="editBox_OK" runat="server" Text="OK" OnClick="editBox_OK_Click" /> <asp:Button ID="editBox_Cancel" runat="server" Text="Cancel" /> </div> </asp:Panel>
You can update the panel being displayed in the popup when a new customer is displayed or just before the popup is displayed:
<act:ModalPopupExtender ID="ModalPopupExtender1" runat="server" TargetControlID="hiddenTargetControlForModalPopup" PopupControlID="pnlEditCustomer" BackgroundCssClass="modalBackground" DropShadow="false" OkControlID="editBox_OK" OnOkScript="ok()" OnCancelScript="cancel()" CancelControlID="editBox_Cancel" />
If you update the popup content just before display (the approach I recommend), then your dialog initialization code needs to run on the server, but you won't have any postback event from any control that brings up the modal dialog. And even if you did, it would be too late for you to edit the controls in the dialog box. The ModalPopup extender, in fact, adds a client-side onclick event handler to the target control and prevents the default action—in this case, a postback—from taking place.
The idea, then, is to use a fake target control for the ModalPopup extender so that the extender will never be automatically kicked off out of your control. How would you then trigger the modal popup? Here's an example. When the button below is clicked, it executes its server-side OnClick handler over a partial rendering operation:
<asp:UpdatePanel runat="server" ID="DialogBoxUpdatePanel" UpdateMode="Conditional"> <ContentTemplate> <asp:Button runat="server" ID="btnEditText" Text="Edit text" OnClick="btnEditText_Click" /> </ContentTemplate> </asp:UpdatePanel>
This has two benefits. First, no full refresh affects the page. Second, the OnClick server code can be used to properly initialize the popup panel and then order the extender to show the popup:
protected void btnEditText_Click(object sender, EventArgs e) { InitDialog(); ModalPanel1.Update(); ModalPopupExtender1.Show(); }
The InitDialog method contains the internal code required to initialize all controls in the panel in Figure 6. This code is sufficient to change the state of involved controls but doesn't modify their markup. This is because you're executing the code over a partial rendering postback. So in the next step, you explicitly refresh the updatable panel. Finally, call the Show method on the ModalPopup extender. This last call ensures that the script to show the dialog as the page loads is properly downloaded to the browser. Figure 7 shows a sample page in action designed along these guidelines.
Figure 7** Modal Dialog Box with Data from Current Context **(Click the image for a larger view)
Do you really need all those partial rendering regions in the page? Well, if your only purpose is find out how to initialize the dialog box with server code, then you could perhaps save some updatable regions. However, if your dialog requires server-side initialization work, then it likely requires some work to update the underlying page with collected data. In that case you would need to return data to the server.
Returning Data to the Server
The ModalPopup extender allows you to identify controls that serve as the OK and Cancel buttons by using the properties OkControlID and CancelControlID for this purpose. When any of these buttons are clicked, the popup is dismissed and some JavaScript code optionally executes. You may define a JavaScript function to run when the OK and Cancel buttons are clicked by using the OnOkScript and OnCancelScript properties. The popup doesn't post back when any of the predefined OK and Cancel buttons are clicked. The following code excerpt from the modalpopupbehavior.js source file explains. This code belongs to the built-in handlers of the click event for both the OK and Cancel buttons:
var element = $get(this._OkControlID); if (element && !element.disabled) { if (this.hide() && this._OnOkScript) { window.setTimeout(this._OnOkScript, 0); } e.preventDefault(); return false; }
Here's a sample handler for the OK button:
function onOK(sender, e) { // refresh the UI // if you need to run server code, you // can invoke a Web service method }
It's Your Turn
To refresh, I didn't add code to the existing control or client behavior to complete this sample. I simply used the existing set of members (both client and server) to improve the initialization of the dialog and the data exchange with the host page. To avoid full page refreshes, I employed partial rendering whenever it made sense.
To replicate the tricks in the sample code discussed here in your own application, install the latest version of the AJAX Control Toolkit and then wrap the modal panel in an UpdatePanel region, paying attention to not include the OK and Cancel buttons in the partial region. Next, bind the ModalPopup extender to an invisible control and display and hide the popup programmatically. Finally, if you need additional buttons in the popup to post without leaving the dialog, all that you have to do is add these buttons to the UpdatePanel that wraps the popup. For more details, have a look at the source code; a line of code is worth a thousand words.
Send your questions and comments for Dino to cutting@microsoft.com.
Dino Esposito is the author of Programming ASP.NET 3.5 Core Reference (Microsoft Press, 2008). Based in Italy, Dino is a frequent speaker at industry events worldwide.