The basic challenge here is how to give the ModalPopuExtender all the flexibility of a popup window. A lot of the functionality is build in using the DynamicControlID, or DynamicServiceMethod attributes, but unfortunately that approach didn't serve the needs of the project. The main constraint was to "skin" the dialog once, and replace the center div's content only dynamically based on the requested function.
I'm going to make 2 passes at this, the first using an UpdatePanel nested in the Extended Panel, that will allow me to swap content on the fly. I'll cover the shortfalls of this, and how they lead to, buyer-beware general concerns when using the Ajax.asp.net framework. The second pass will replace the UpdatePanel with a call to a WebMethod and some client manipulation of the HTML. Again I'll cover the pitfalls, and potential advantages to this approach.
For Starters, let's make a simple Dialog box, and then look at how we can wire up Modal Cancel and OK buttons in a non-declarative manner. One of the key things to do when using the AjaxControlToolkit (I'm going to be using the 10920 release) and review both the C# and even more crucially the JavaScript Files. Although VS2008 beta2 has some really nice JavaScript intellisense, it still is up to the developer to really explore how the toolkit does its magic, because inevitably you're going to be asked to make it work just like it does, only different.
First a note about the JavaScript Public vs. Private syntax, generally when a var or function is declared with a preceding underscore (var _myPrivate = 1;) it is meant to be treated as a private, and just like ANY private methods or fields you tickle using Reflection in C#, you are playing off the beaten track, and are subject to the whims of the JavaScript's developer. That said, I'm going to be doing just that. The ModalPopupBehavior.js file defines three methods that are interesting to us in the next section, hide, _onOk, _onCancel. Both _onOk and _onCancel delegate the actual hiding of the modal dialog to the hide function, but they also will handle their respective OnCancelScript and OnOkScript values if they are specified. It is not much extra work to use the get_onCancelScript function and manage the process ourselves, but I don't want to provide production ready script, only a way to encourage people to know just how the tools they are going to use function.
That said let's look at the Panel we are going to extend:
<asp:Panel ID="ModalContentWrapper" runat="Server">
<table width="600px" border="0" cellpadding="0" cellspacing="0" class="Modal">
<tr>
<td style="width:580px" colspan="2" class="Modal" id="DragHandle" runat="server">
<img src="images/spacer.gif" width="580px" height="20px" alt=""/>
</td>
<td style="width:20px"><asp:Linkbutton ID="CloseButton" runat="server" CssClass="CloseButtonStyle">X</asp:Linkbutton></td>
</tr>
<tr>
<td style="width:20px">
<img src="images/spacer.gif" width="20px" height="360px" alt=""/>
</td>
<td style="width:560px;">
<div id="eventualHomeOfDynamicContent>
<asp:Button ID="OKButton" runat="server" Text="OK" /><br />
<asp:Button ID="CancelButton" runat="server" Text="Cancel" /><br />
<asp:Button ID="MinimizeButton" runat="server" Text="Minimize"/><br />
</div>
</td>
<td style="width:20px">
<img src="images/spacer.gif" width="20px" height="360px" alt=""/>
</td>
</tr>
<tr>
<td style="width:600px" colspan="3">
<img src="images/spacer.gif" width="600px" height="20px" alt=""/>
</td>
</tr>
</table>
</asp:Panel>
There's nothing fancy here, just need a control we can extend, and some buttons we can wire up to close the dialog. One thing I'd like to point out is the eventualHomeOfDynamicContent div. The idea here is that we can have a consistent wrapper around our eventual content, without needing each control to be hosted in the Modal to re-implement it. You can see that this might be a case where we would implement an ITemplate in a base ModalControl that allows for a really consistent look and feel (ed. Note: The UI I'm creating is ugly, very ugly as I'm not a UI guy, but the idea is that someone with some skill could skin this well in a base class and we could consume it.).
Let's look at the ModalPopupExtender:
<asp:Button ID="ShowModal" runat="server" Text="Show The Modal" OnClientClick="if (!wiredHanlders){wireUpHandlers();}" />
<ajaxToolkit:ModalPopupExtender
ID="ModalPopup"
runat="server"
TargetControlID="ShowModal"
CancelControlID="CloseButton"
OnCancelScript="OnCanceledHandler();"
OnOkScript="OnOkayedHandler();"
PopupControlID="ModalContentWrapper"
PopupDragHandleControlID="DragHandle"
RepositionMode="repositionOnWindowResize"
DropShadow="false"
>
There's some cheating here. First, I'm hard wiring my "Cancel" "X" button. Because this is part of what I'm calling the skin, and will always appear in the modal. Second, there's the JavaScript in the "TargetControl" OnClientClick that wires up the closing handlers. In reality you would want something more stable than this. Maybe something along the lines of adding a event handler with Sys.Application.add_load that would handle the wiring, but again this isn't about finished code, we'll get there by the end though.
Besides from the cheats, there's not too much to look at here, using these extenders is pretty straight forward. I'll admit sometimes things just don't work like I'd expect them too, for instance the "OnCancelScript" is a very misleading attribute name, it really should be "OnCanceledScript" as there's no way to prevent the modal from closing by returning false. But that's a nit, and as well see in part to there's a pretty smooth way to deal with canceling cancels.
Here's the non dynamic JavaScript:
<script type="text/javascript">
var wiredHanlders = false;
function OnCanceledHandler(){
alert("Cancel Completed");
}
function OnOkayedHandler(){
alert("Okay Completed");
}
function addOkayHandler(clientID, extenderID){
var comp = Sys.Application.findComponent(extenderID);
var okHandler = Function.createDelegate(comp, comp._onOk);
$addHandler($get(clientID), 'click', okHandler);
}
function addCancelHandler(clientID, extenderID){
var comp = Sys.Application.findComponent(extenderID);
var cancelHandler = Function.createDelegate(comp, comp._onCancel);
$addHandler($get(clientID), 'click', cancelHandler);
}
</script>
There's a script to let us know when Ok and Cancel have been triggered, and then 2 more interesting functions, the addOkayHandler and addCancelHandler. These are where we start to poke around in the Ajax Framework world, so let's get some look at this in some detail.
Sys and Sys.Application – Microsoft chose to create a JavaScript feel that closely mimicked the .Net C# world when they creating their Ajax Framework. Sys is the primary "namespace" within the framework. One of the big challenges with JavaScript is that functions are global and the eachtime a function is defined it overrides the previous definition without warning, unlike helpful complaints from a compiler. However it's perfectly fine to use the "." In the name of a JavaScript function, so in addition to aiding in the grouping of functions in logical areas, using these namespaces helps reduce issues with naming collisions. It's a nice pattern to follow.
findComponent – This does exactly what it sounds like it will do, Give an ComponentID it will return a Sys.Component if it can find one with a matching ID.
Sys.Component – Is a prototype JS function that encapsulates "Control" like functionality. If you view source on a page the has a Control Extender you will likely see something that looks like this:
<script type="text/javascript">
<!--
MODAL_EXTENDER_ID = 'ModalPopup';Sys.Application.initialize();
Sys.Application.add_init(function() {
$create(AjaxControlToolkit.ModalPopupBehavior, {"CancelControlID":"CloseButton","OnCancelScript":"OnCanceledHandler();","OnOkScript":"OnOkayedHandler();","PopupControlID":"ModalContentWrapper","PopupDragHandleControlID":"DragHandle","dynamicServicePath":"/default.aspx","id":"ModalPopup","repositionMode":1}, null, null, $get("ShowModal"));
});
// -->
</script>
This call the $create function defines creates a new instance of the Component function, that allows for a single entity to track manage the Extended control. Additional just like the Asp.Net page has a lifecycle with Event you can subscribe to or override in a C# UserControl, the Sys.Component has set of events that provide a consistent predictable process to code against.
Fuction.CreateDelegate – Is a nifty way to properly scope and event handler, essentially it binds the defined handler method to the "instance object" that it will handle events for. This is where we are using those private functions, an we're saying please give a pointer to this the _onOk function scoped to the component. For a really in depth explanation of how the apply() function works check out this blog.
$addHandler – A nifty function that allows chained event handlers, you can add a handler to any event on any element without having to (MOST OF THE TIME* more on this later in this series) worry about what other handlers are already bound to an element.
$get – nothing special here just a wrapper on document.getElementById, although it's slightly optimized to skip right to a parent Element if provided.
So the net effect of all of this is that we can Find our ModalPopupExtender component, and add some event triggers for Ok and Cancel.
The Code Behind:
protected void Page_Load(object sender, EventArgs e)
{
ScriptManager.RegisterClientScriptBlock(Page, Page.GetType(), "wireScripts", getWireScripts(), true);
}
string getWireScripts(){
StringBuilder script = new StringBuilder();
script.Append("function wireUpHandlers(){");
script.AppendFormat("var extenderID = '{0}';", ModalPopup.ClientID);
script.AppendFormat("addCancelHandler('{0}', extenderID);", CancelButton.ClientID);
script.AppendFormat("addCancelHandler('{0}', extenderID);", MinimizeButton.ClientID);
script.AppendFormat("addOkayHandler('{0}', extenderID);", OKButton.ClientID);
script.Append("wiredHanlders = true;}");
return script.ToString();
}
Here we are just dynamically creating the function that will wire up our buttons on the client. We need to know what Component we want to muck with on the client, and what button we want to wire up.
Some styles to be reach the really ugly that we need:
body
{
font-family: Verdana, Arial;
font-size:medium;
color:#000000;
background-color:#fdfdfd;
}
.Modal
{
background-color:#AADDDD;
color:#ffffff;
}
.Modal td
{
white-space:nowrap;
text-align:center;
}
.CloseButtonStyle
{
font-size:large;
font-weight:bold;
color:#ffffff;
text-align:center;
text-decoration:none;
}
</style>
That's it put this all together and you should be able to close the Modal Dialog using the"X" button, the OKButton, the CancelButton and the MinimizeButton. To be continued…
Print | posted on Saturday, October 06, 2007 1:10 PM