Making the most out of the AjaxControlToolkit’s ModalPopupExtender Part 7

In this post well look at the some Trace.axd and Fiddler/FireBug output to see what kind of work we are doing to generate the dynamic content inside of the modal update panel. Additionally the some modifications to the code listed here are listed below to integrate the WebMethod dynamic contend within the scope of a ModalPopupExtender.
Right of the bat, I have to admit that using the WebMethod version's Trace.axd output compared to the UpdatePanel's Trace output is entirely unfair. The issue is that the Control creation work handled by the AjaxControlRender is not listed in the output, as it's not directly called by the request. I've added some trace logging to get a general comparison, but I' as I have Fiddler/FireBug to let me look at the wire side of things, but that said it's definitely not a good head on comparison without some better outline of the work being performed on the server. In subsequent parts of this series I'll be looking at ways to work around this so the final head to head comparisons are really worth looking at.

One other issue I want to cover before we look at some comparison result is the issues I have with building a Control for a specific Page/Pattern/Host etc. I really like the idea of truly agnostic controls. Obviously thought I'm nowhere close to that with the UserControls I'm creating for this example. Arguably the UserControls I'm creating for the WebMethod version of this example are even more offensive in this respect as the Implement a non-standard Interface IRenderable that is very specific to the WebMethod Usage, in defense of this though I'll offer up the challenge to think about this pattern in terms of the MVP pattern.

Okay So Here's the unfair Trace.axd output; mostly I'm looking at the timings.

This is the UpdatePanel:
Category
Message
From First(s)
From Last(s)
aspx.page
Begin PreInit
aspx.page
End PreInit
1.65969652878819E-05
0.000017
aspx.page
Begin Init
3.00660985242153E-05
0.000013
aspx.page
End Init
0.000164551652463105
0.000134
aspx.page
Begin InitComplete
0.000178510912492205
0.000014
aspx.page
End InitComplete
0.000189424651839534
0.000011
aspx.page
Begin LoadState
0.000199253377676159
0.000010
aspx.page
End LoadState
0.000386672625233839
0.000187
aspx.page
Begin ProcessPostData
0.000400530866763667
0.000014
aspx.page
End ProcessPostData
0.000635454998960715
0.000235
aspx.page
Begin PreLoad
0.000649343171897734
0.000014
aspx.page
End PreLoad
0.000659785491581792
0.000010
aspx.page
Begin Load
0.000669621700270214
0.000010
aspx.page
End Load
0.00131647682394513
0.000647
aspx.page
Begin ProcessPostData Second Try
0.0013312554562461
0.000015
aspx.page
End ProcessPostData Second Try
0.00134366202452713
0.000012
aspx.page
Begin Raise ChangedEvents
0.00135389482436084
0.000010
aspx.page
End Raise ChangedEvents
0.00137432675119518
0.000020
aspx.page
Begin Raise PostBackEvent
0.00138573435876117
0.000011
aspx.page
End Raise PostBackEvent
0.00222819496986074
0.000842
aspx.page
Begin LoadComplete
0.0022437593016005
0.000016
aspx.page
End LoadComplete
0.00226010559135315
0.000016
aspx.page
Begin PreRender
0.00227047308251923
0.000010
aspx.page
End PreRender
0.0101874949075036
0.007917
aspx.page
Begin PreRenderComplete
0.0102070064435668
0.000020
aspx.page
End PreRenderComplete
0.0103499214300561
0.000143
aspx.page
Begin SaveState
0.0110321629598836
0.000682
aspx.page
End SaveState
0.0111056483059655
0.000073
aspx.page
Begin SaveStateComplete
0.0111185749324465
0.000013
aspx.page
End SaveStateComplete
0.0111282502598212
0.000010
aspx.page
Begin Render
0.0111427931822906
0.000015
aspx.page
End Render
0.0164754138432758
0.005333

This is the WebMethod:
Category
Message
From First(s)
From Last(s)
WebMethod
In GetControlTwo
AjaxControlRender
Create Page
0.00665580507840431
0.006656
AjaxControlRender
Create Header
0.00667671982307066
0.000021
AjaxControlRender
Create Form
0.00672034147030946
0.000044
AjaxControlRender
Create ScriptManager
0.0067555223905416
0.000035
AjaxControlRender
Load Control: ControlTwo.ascx
0.00679069208640271
0.000035
AjaxControlRender
Add Header
0.0142921113457605
0.007501
AjaxControlRender
Add Form
0.0143148818531021
0.000023
AjaxControlRender
Add ScriptManager
0.0143265065600213
0.000012
AjaxControlRender
Add Control: ControlTwo.ascx
0.0143376822920997
0.000011
AjaxControlRender
Bind Control: ControlTwo.ascx
0.0143483866005953
0.000011
AjaxControlRender
Render Page
0.0145353172755542
0.000187
AjaxControlRender
Parse Output
0.0320549216788333
0.017520
AjaxControlRender
Render Complete
0.0320997405923142
0.000045

Okay, so it looks like in this very simple case the WebMethod is twice as slow as using an update panel. This certainly doesn't lend itself to recommending the additional complexity of using WebMethod's vs. UpdatePanels to add dynamic UI Elements to the client. True these timings don't indicate what's happening in the browser, but when we look at that later on, we'll see there's very little difference in what actually happens on the client. Running these tests several times, this appears to be pretty consistent. On this very simple example using a WebMethod is between 1.5 and 2 times as costly as using an UpdatePanel.

Let's open up FireBug (or Fiddler if you're using IE) and look at the request/response overhead for these 2 implementations.

UpdatePanel
:
Post:
DataFromTwoCtldr
FirstLoaderTgtCtl
MainContentDynCtl$DataFromOne
Some Text For Two
MainContentDynCtl$Submit
Go Get ControlTwo
ScriptManager1
Content|MainContentDynCtl$Submit
__EVENTARGUMENT
__ EVENTTARGET
__EVENTVALIDATION
/wEWCQKm0JmOAwL6poiACwKYl8StBAKrg7q+DALpna34AwL3vdXbBwLy/PKFBwLn1IhgAvSRrqIN5a3XffAoph03Vng2j9DDynitk7s=
__VIEWSTATE
/wEPDwULLTE2MjQyODE1NDIPZBYCAgMPZBYCAgsPZBYCAgUPZBYCZg9kFgICAQ9kFgJmD2QWAgIFDw8WAh4EVGV4dGRkZGRDEEbPW8ezdjRSo6G5VB/62faV+g==

The Response:
798|updatePanel|Content|
This is the Content of ControlTwo:<br />
Put some Text Here for ControlOne:<input name="MainContentDynCtl$DataFromTwo" type="text" id="MainContentDynCtl_DataFromTwo"
/><br />
<input type="submit" name="MainContentDynCtl$Submit" value="Go Get ControlOne" id="MainContentDynCtl_Submit"
/><br />
<span id="MainContentDynCtl_TextFromControlOne">Some Text For Two</span><br />
<input type="submit" name="MainContentDynCtl$OKButton" value="OK" id="MainContentDynCtl_OKButton" />
<br />
<input type="submit" name="MainContentDynCtl$CancelButton" value="Cancel" id="MainContentDynCtl_CancelButton"
/><br />
<input type="submit" name="MainContentDynCtl$MinimizeButton" value="Minimize" id="MainContentDynCtl_MinimizeButton"
/><br />
|0|hiddenField|__EVENTTARGET||0|hiddenField|__EVENTARGUMENT||148|hiddenField
|__VIEWSTATE|/wEPDwULLTE2MjQyODE1NDIPZBYCAgMPZBYCAgsPZBYCAgUPZBYCZg9kFgICAQ9kFgJmD2QWAgIFDw8WAh4EVGV4dAURU29tZSBUZXh0IEZvciBUd29kZGTNCkz
/0eO77l1wnH0tC/vDoxkAIw==|104|hiddenField|__EVENTVALIDATION|/wEWCQL9kqD7BALqq/axDgL3vdXbBwLy/PKFBwLn1IhgAvSRrqINAvqmiIALApiXxK0EAquDur4MNSxugn064UiDt6p
//HN9TGNg+wM=|0|asyncPostBackControlIDs|||0|postBackControlIDs|||8|updatePanelIDs||tContent|0|childUpdatePanelIDs
|||7|panelsToRefreshIDs||Content|2|asyncPostBackTimeout||90|12|formAction||default.aspx|11|pageTitle
||ModalSample|247|scriptBlock|ScriptContentNoTags|function wireUpHandlers(){var extenderID = MODAL_EXTENDER_ID
;addCancelHandler('MainContentDynCtl_CancelButton', extenderID);addCancelHandler('MainContentDynCtl_MinimizeButton'
, extenderID);addOkayHandler('MainContentDynCtl_OKButton', extenderID);}|65|scriptBlock|ScriptContentNoTags
|MODAL_EXTENDER_ID = 'ModalPopup'; Sys.Application.add_load(init);|0|hiddenField|DataFromTwoCtldr||17
|hiddenField|DataFromOneCtldr|Some Text For Two|1|hiddenField|FirstLoaderTgtCtl|2|

WebMethod:
Post:
 
{"data":{"__type":"WebMethodWork.TextObject","TextField":"Some Text For Two"}}

Response:
 
\u003c!-- START --\u003e\u003cscript type=\"text/javascript\"\u003e\r\nfunction buildTextObject(){var
 
dataObj = new WebMethodWork.TextObject();dataObj.TextField = document.getElementById(\u0027ctl03_DataFromTwo
 
\u0027).value; return dataObj; }\r\n\u003c/script\u003e\r\n\r\n\r\n\r\nThis is the Content of ControlTwo
 
:\u003cbr /\u003e\r\nPut some Text Here for ControlOne:\u003cinput name=\"ctl03$DataFromTwo\" type=\"text
 
\" id=\"ctl03_DataFromTwo\" /\u003e\u003cbr /\u003e\r\n\u003cscript type=\"text/javascript\"\u003e\r
 
\nfunction regJS(html){\r\nwhile (html.indexOf(\u0027\u003cscript\u0027) != -1){\r\nvar scriptStart
 
= html.indexOf(\u0027\u003e\u0027, (html.indexOf(\u0027\u003c\u0027 + \u0027script\u0027)));\r\nvar scriptEnd
 
= html.indexOf(\u0027\u003c\u0027 +\u0027/script\u003e\u0027);\r\nvar script = html.substring(scriptStart
 
+1, scriptEnd);\r\nhtml = html.substring(0,html.indexOf(\u0027\u003c\u0027 + \u0027script\u0027)) + html
 
.substring(scriptEnd+9,html.length-1);\r\nvar scriptElement = document.createElement(\u0027SCRIPT\u0027
 
);\r\nscriptElement.type = \u0027text/javascript\u0027;\r\ndocument.getElementsByTagName(\u0027HEAD\u0027
 
)[0].appendChild(scriptElement);\r\nscriptElement.innerHTML = script;\r\n}\r\nreturn html;}\r\nvar SRC_TGT
 
= SRC_TGT;function dynControlTwoLinkResponseHandler(result){result = regJS(result);\r\nvar el = document
 
.getElementById(SRC_TGT); el.innerHTML = result; if(el.style.display == \u0027none\u0027) { el.style
 
.display = \u0027\u0027;}}\r\n\r\nfunction dynControlTwoLinkInitRequest(){PageMethods.GetControlOne(buildTextObject
 
(),dynControlTwoLinkResponseHandler);}\r\n\u003c/script\u003e\r\n\r\n\u003ca onclick=\"dynControlTwoLinkInitRequest
 
(); return false;\" id=\"ctl03_dynControlTwoLink\" href=\"javascript:__doPostBack(\u0027ctl03$dynControlTwoLink
 
\u0027,\u0027\u0027)\"\u003eGet Control One\u003c/a\u003e\u003cbr /\u003e\r\n\u003cspan id=\"ctl03_TextFromControlOne
 
\"\u003eSome Text For Two\u003c/span\u003e\u003cbr /\u003e\r\n\u003cinput type=\"submit\" name=\"ctl03$OKButton
 
\" value=\"OK\" id=\"ctl03_OKButton\" /\u003e\u003cbr /\u003e\r\n\u003cinput type=\"submit\" name=\"ctl03$CancelButton
 
\" value=\"Cancel\" id=\"ctl03_CancelButton\" /\u003e\u003cbr /\u003e\r\n\u003cinput type=\"submit\"
 
name=\"ctl03$MinimizeButton\" value=\"Minimize\" id=\"ctl03_MinimizeButton\" /\u003e\u003cbr /\u003e
 
\r\n\r\n\r\n\r\n\r\n\r\n\u003c!-- END --\u003e\r\n

Once again, we really don't see much of a benefit by using the WebMethod. The Post data is much smaller, but the Response is larger, although not by much so again, there's really not much motivation to incur the higher level of effort need to build and maintain a WebMethod driven dynamic UI.

The reality is though, that you are unlikely to ever have a page with a single control. Later in this series we're going to get a more realistic comparison between the UpdatePanel vs. WebMethod approach to managing the UI. The more I look into the dark heart that is the desktop-ing of the Web, the more I realize that the problems that developers currently face, are going to pale in comparison to the challenges they will face.

The key thing I think that one could abstract from this posting series so far is that the standard response of it's not "one size fits all" is extremely accurate when it comes to Ajax driven websites. Contrary to what most developers and development organizations like there's not an apparent de-facto standard way of approaching the UI. Each time a portion of a WebPage or WebSite is moved into the world of Ajax, the decisions of how to achieve a successful migration will need to be evaluated all over again, leading to a diverse, and potentially un- maintainable mesh of custom UI and JavaScript Code.
Most people that have done Asp.Net programming for any amount of time will not find this surprising. It's not that different than the realities faced in traditional web development. The added work here is the increase in the number of potential development languages, and platforms. Choices Like PageMethods, WebServices, using Third party webservices proxied through your own host. These aren't new concerns, only now the need to be discussed at the UI Level, and the UI is really the focus of much of design process.

Some strange hybridization of the feature focused XP approach to web and BUFD will need to be used to allow the UI to flex in the way's it needs without re-inventing the technology stack every single time… but I digress.
More to come next time, but here's the modified code for the WebMethod approach the Dynamic Modal Dialog.

Default.aspx.WebMethod.cs
public partial class _Default
{
[WebMethod]
public static string GetControlOne(TextObject data)
{
HttpContext.Current.Trace.Write("WebMethod", "In GetControlOne");
return AjaxControlRenderer.RenderAjaxControl<ControlOne, TextObject>(true, "ControlOne.ascx", data);
}
[WebMethod]
public static string GetControlTwo(TextObject data)
{
HttpContext.Current.Trace.Write("WebMethod", "In GetControlTwo");
return AjaxControlRenderer.RenderAjaxControl<ControlTwo, TextObject>(true, "ControlTwo.ascx", data);
}
}

AjaxControlRenderer
public class AjaxControlRenderer
{
public static string RenderAjaxControl<T, D>(bool enableViewState, string path, D data) where T : System.Web.UI.Control, IRenderable<D>, new()
{
TraceContext trace = HttpContext.Current.Trace;
trace.Write(category, "Create Page");
Page renderPage = new Page();
renderPage.EnableViewState = enableViewState;
trace.Write(category, "Create Header");
HtmlHead header = renderPage.LoadControl(typeof(HtmlHead), null) as HtmlHead;
header.EnableViewState = enableViewState;
trace.Write(category, "Create Form");
HtmlForm form = renderPage.LoadControl(typeof(HtmlForm), null) as HtmlForm;
form.EnableViewState = enableViewState;
trace.Write(category, "Create ScriptManager");
ScriptManager scriptManager = renderPage.LoadControl(typeof(ScriptManager), null) as ScriptManager;
trace.Write(category, "Load Control: " + path);
T controlToRender = renderPage.LoadControl(path) as T;
trace.Write(category, "Add Header");
renderPage.Controls.Add(header);
trace.Write(category, "Add Form");
renderPage.Controls.Add(form);
trace.Write(category, "Add ScriptManager");
form.Controls.Add(scriptManager);
form.Controls.Add(new LiteralControl("<!-- START -->"));
trace.Write(category, "Add Control: " + path);
form.Controls.Add(controlToRender);
form.Controls.Add(new LiteralControl("<!-- END -->"));
if (null != data)
{
trace.Write(category, "Bind Control: " + path);
controlToRender.PopulateData(data);
}
StringWriter htmlOutput = new StringWriter();
trace.Write(category, "Render Page");
HttpContext.Current.Server.Execute(renderPage, htmlOutput, false);
trace.Write(category, "Parse Output");
string result = htmlOutput.ToString();
result = result.Substring(result.IndexOf("<!-- START -->"));
result = result.Substring(0, result.IndexOf("<!-- END -->") + 14);
trace.Write(category, "Render Complete");
return result;
}
static readonly string category = "AjaxControlRenderer";
}

Default.aspx
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>WebMethod Page</title>
<style type="text/css">
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>
<script type="text/javascript">
function OnCanceledHandler(){
alert("Cancel Completed");
}
function OnOkayedHandler(){
alert("Okay Completed");
}
function addOkayHandler(clientID, extenderID){
var comp = Sys.Application.findComponent(extenderID);
$addHandler($get(clientID), 'click', Function.createDelegate(comp, comp._onOk));
}
function addCancelHandler(clientID, extenderID){
var comp = Sys.Application.findComponent(extenderID);
$addHandler($get(clientID), 'click', Function.createDelegate(comp, comp._onCancel));
}
function init(){
var prm = Sys.WebForms.PageRequestManager.getInstance();
if (prm){
if (!prm.get_isInAsyncPostBack()){
prm.add_endRequest(endHandler);
if (wireUpHandlers){
wireUpHandlers();
}
}
}
}
function endHandler(sender,args){
wireUpHandlers();
}
</script>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="MainManager" runat="server" EnablePageMethods="true" />
<div>
<asp:Button ID="ShowModal" runat="server" Text="Show The Modal" />
<asp:Button ID="DummyOkayControl" runat="server" style="display:none;" />
<ajaxToolkit:ModalPopupExtender
ID="ModalPopup"
runat="server"
TargetControlID="ShowModal"
CancelControlID="CloseButton"
OkControlID="DummyOkayControl"
OnCancelScript="OnCanceledHandler();"
OnOkScript="OnOkayedHandler();"
PopupControlID="ModalContentWrapper"
PopupDragHandleControlID="DragHandle"
RepositionMode="repositionOnWindowResize"
DropShadow="false"
>
</ajaxToolkit:ModalPopupExtender>
<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;">
<asp:Panel ID="Target" runat="Server" />
</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>
</div>
</form>
</body>
</html>

Default.aspx.cs
public partial class _Default : System.Web.UI.Page
{
//Note this is only to make it mimic the UpdatePanel for this round
//of development
protected override void OnInit(EventArgs e)
{
if (!IsPostBack)
{
Target.Controls.Add(LoadControl("/ControlOne.ascx"));
ScriptManager.RegisterClientScriptBlock( Page, Page.GetType(), "regScript", "var SRC_TGT = '" + Target.ClientID + "';",true );
}
base.OnInit(e);
}
}

ControlOne.ascx
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ControlOne.ascx.cs" Inherits="WebMethodWork.ControlOne" %>
<%@ Register Assembly="Controls" Namespace="Controls" TagPrefix="cc1" %>
<cc1:WebMethodDataTypeMapping ID="DataMapping" runat="server" MappedType="WebMethodWork.TextObject, WebMethodWork" >
<Mappings>
<cc1:Mapping ControlID="DataFromOne" MappedPropertyName="TextField" />
</Mappings>
</cc1:WebMethodDataTypeMapping>
This is the Content of ControlOne:<br />
Put some Text Here for ControlTwo:<asp:TextBox ID="DataFromOne" runat="server" /><br />
<cc1:WebMethodUIUpdateLink
ID="dynGetControlOneLink"
runat="server"
Text="Get Control Two"
DataMapControlID="DataMapping"
WebMethod="GetControlTwo"
UseParentAsTarget="true" />
<br />
<asp:Label ID="TextFromControlTwo" runat="server" /><br />
<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 />

ControlOne.ascx.cs
public partial class ControlOne : System.Web.UI.UserControl, IRenderable<TextObject>
{
protected override void OnLoad(EventArgs e)
{
ScriptManager.RegisterClientScriptBlock(this, GetType(), "wireScripts", getWireScripts(), true);
base.OnLoad(e);
}
public void PopulateData(TextObject data)
{
TextFromControlTwo.Text = data.TextField;
}
string getWireScripts()
{
StringBuilder script = new StringBuilder();
script.Append("function wireUpHandlers(){");
script.Append("var extenderID = MODAL_EXTENDER_ID;");
script.AppendFormat("addCancelHandler('{0}', extenderID);", CancelButton.ClientID);
script.AppendFormat("addCancelHandler('{0}', extenderID);", MinimizeButton.ClientID);
script.AppendFormat("addOkayHandler('{0}', extenderID);", OKButton.ClientID);
script.Append("}");
return script.ToString();
}
}

ControlTwo.ascx
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ControlTwo.ascx.cs" Inherits="WebMethodWork.ControlTwo" %>
<%@ Register Assembly="Controls" Namespace="Controls" TagPrefix="cc1" %>
<cc1:WebMethodDataTypeMapping ID="DataMapping" runat="server" MappedType="WebMethodWork.TextObject, WebMethodWork" >
<Mappings>
<cc1:Mapping ControlID="DataFromTwo" MappedPropertyName="TextField" />
</Mappings>
</cc1:WebMethodDataTypeMapping>
This is the Content of ControlTwo:<br />
Put some Text Here for ControlOne:<asp:TextBox ID="DataFromTwo" runat="server" /><br />
<cc1:WebMethodUIUpdateLink
ID="dynControlTwoLink"
runat="server" Text="Get Control One"
DataMapControlID="DataMapping"
WebMethod="GetControlOne"
UseParentAsTarget="true" /><br />
<asp:Label ID="TextFromControlOne" runat="server" /><br />
<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 />

ControlTwo.ascx.cs
public partial class ControlTwo : System.Web.UI.UserControl, IRenderable<TextObject>
{
protected override void OnLoad(EventArgs e)
{
ScriptManager.RegisterClientScriptBlock(Page, Page.GetType(), "wireScripts", getWireScripts(), true);
base.OnLoad(e);
}
public void PopulateData(TextObject data)
{
TextFromControlOne.Text = data.TextField;
}
string getWireScripts()
{
StringBuilder script = new StringBuilder();
script.Append("function wireUpHandlers(){");
script.Append("var extenderID = MODAL_EXTENDER_ID;");
script.AppendFormat("addCancelHandler('{0}', extenderID);", CancelButton.ClientID);
script.AppendFormat("addCancelHandler('{0}', extenderID);", MinimizeButton.ClientID);
script.AppendFormat("addOkayHandler('{0}', extenderID);", OKButton.ClientID);
script.Append("}");
return script.ToString();
}
}
Print | posted on Saturday, October 13, 2007 1:19 PM
Comments have been closed on this topic.