在web application的表单提交过程中显示“please wait”信息或者是gif动画图片通常是很有用的,特别是提交过程比较久的情况。我最近开发了一个调查提交程序,在程序里内部用户通过一个网页上传excel电子表格。程序将上传的电子表格数据插入到数据库中。这个过程只需要几秒钟,但即便是几秒钟,在网页是看来却是非常明显的等待过程。在程序测试的时候,一些用户重复地点击上传按钮。因此,提供一个视觉的信息来告诉人们上传正在进行中是很有用的。并同时把上传按钮一起隐藏掉,以防止多次点击。这里介绍的控件是Button控件的子类,它演示了如何把客户端javascript代码封装在asp.net服务器控件中来提供便利的功能。 虽然外面已经有很多javascript的例子来完成这件事情,但当我试图把这些功能封装到asp.net控件中时我发现了一些问题。我最开始尝试通过javascript的onclick句柄来使button无效,并用另外的文本取代。但我发现很棘手,这样会妨碍到asp.net服务器端的click事件的功能。而最终行得通的,并且对不同浏览器也有很好支持的方法是,让button在div标记中呈现。div可以隐藏并且不妨碍asp.net的click事件。 Using the control 作为正常的button控件的派生,PleaseWaitButton的功能与它基本一样。它通过三个附加的属性来管理当按钮被点击后"please Wait"信息或图片的显示。 PleaseWaitText 这是显示的客户端文本信息,如果存在,当按钮被点击它将取代按钮。 PleaseWaitImage 这是显示的图像文件(比如gif动画图像),如果存在,当按钮被点击它将取代按钮。这个属性将变成<img>标记中的src属性。 PleaseWaitType PleaseWaitTypeEnum枚举值之一:TextOnly,ImageOnly,TextThenImage,或者ImageThenText。它控制消息和图片的布局。 下面是一个.aspx文件示例,它演示了一个设置了PleaseWaitText和PleaseWaitImage的PleastWaitButton。 <%@ Page language="C#" %> <%@ Register TagPrefix="cc1" Namespace="JavaScriptControls" Assembly="PleaseWaitButton" %> <script runat="server"> private void PleaseWaitButton1_Click(object sender, System.EventArgs e) { // Server-side Click event handler; // simulate something that could take a long time, // like a file upload or time-consuming server processing DateTime dt = DateTime.Now.AddSeconds(5); while (DateTime.Now < dt) { // do nothing; simulate a 5-second pause } // at the end of the loop display a success message // and hide the submit form panelSuccess.Visible = true; PleaseWaitButton1.Visible = false; } </script> <html> <head> <title>Testing PleaseWaitButton</title> </head> <body> <form id="Form1" method="post" runat="server"> <P>Testing the PleaseWaitButton control.</p> <cc1:PleaseWaitButton id="PleaseWaitButton1" runat="server" Text="Click me to start a time-consuming process" PleaseWaitText="Please Wait " PleaseWaitImage="pleaseWait.gif" OnClick="PleaseWaitButton1_Click" /> <asp:Panel id="panelSuccess" runat="server" visible="false"> Thank you for submitting this form. You are truly the coolest user I''ve ever had the pleasure of serving. No, really, I mean it. There have been others, sure, but you are really in a class by yourself. </asp:Panel> </form> </body> </html> How It Works
PleaseWaitButton控件在<div>标记中呈现了一个标准的asp.net Button。它也呈现了一个空的<div>标记给 信息/图像。在点击按钮时,由Javascript函数(见下面的客户端函数)控制按钮的隐藏和信息的显示。为了方便起见,由PleaseWaitButton服务器控件处理所有必需的javascript客户端代码的实施。 由于PleaseWaitButton实施它自己的javascript onclick句柄,所以我们必须采取一些额外的措施来保持原有的onclick句柄,并且允许控件清晰地运行一些客户端验证代码。为了达到此目的,我们首先把Button基类还原为一个字符串缓冲,然后巧妙地处理它,把我们定义的onclick代码包含进去。 protected override void Render(HtmlTextWriter output) { // Output the button''s html (with attributes) // to a dummy HtmlTextWriter StringWriter sw = new StringWriter(); HtmlTextWriter wr = new HtmlTextWriter(sw); base.Render(wr); string sButtonHtml = sw.ToString(); wr.Close(); sw.Close(); // now modify the code to include an "onclick" handler // with our PleaseWait() function called appropriately // after any client-side validation. sButtonHtml = ModifyJavaScriptOnClick(sButtonHtml); // before rendering the button, output an empty <div> // that will be populated client-side via javascript // with a "please wait" message" output.Write(string.Format("<div id=''pleaseWaitButtonDiv2_{0}''>", this.ClientID)); output.Write("</div>");
// render the button in an encapsulating <div> tag of its own output.Write(string.Format("<div id=''pleaseWaitButtonDiv_{0}''>", this.ClientID)); output.Write(sButtonHtml); output.Write("</div>"); } 这种把button还原成一个字符串缓冲然后处理它的onclick内容的技术是一件很危险的事情(is certainly a hack). 但它可以让我们在父button类中实施标准的验证代码,然后再实现我们的PleaseWait() Javascript函数调用。如果不这样做,我们只能在验证代码之前就在onclick属性中实施我们的PleaseWait()函数调用,除非我们愿意完全重写父Button类的属性的呈现。这样就算页面上有输入错误也会产生我们并不希望的按钮隐藏和显示"please wait"信息的效果。因此,我们必须在onclick句柄中强行令我们的客户端PleaseWait()函数出现在客户端页面验证之后。 onclick属性的修改发生在ModifyJavaScriptOnClick()函数中。这个函数获取按钮呈现的HTML字符串,并检查看是否存在onclick属性。如果是,这个函数会检查是否有使用客户端验证代码。如果是这种情况的话,我们定义的PleaseWait()函数会加在已经存在的onclick代码的最后面,紧跟在客户端检查的boolin变量Page_IsValid后面。这个变量代表是否使用了验证控件。如果Page_IsValid的值是false,"Please wait"信息将不显示。如果为True则显示。 private string ModifyJavaScriptOnClick(string sHtml) { // Thanks to CodeProject member KJELLSJ (Kjell-Sverre Jerijaervi) // for code ideas to allow the button to work with client-side validation string sReturn = ""; string sPleaseWaitCode = GeneratePleaseWaitJavascript(); // is there an existing onclick attribute? Regex rOnclick = new Regex("onclick=\"(?<onclick>[^\"]*)"); Match mOnclick = rOnclick.Match(sHtml); if (mOnclick.Success) { // there is an existing onclick attribute; // add our code to the end of it; if client-side // validation has been rendered, make sure // we check to see if the page is valid; string sExisting = mOnclick.Groups["onclick"].Value; string sReplace = sExisting + (sExisting.Trim().EndsWith(";") ? "" : "; "); if (IsValidatorIncludeScript() && this.CausesValidation) { // include code to check if the page is valid string sCode = "if (Page_IsValid) " + sPleaseWaitCode + " return Page_IsValid;"; // add our code to the end of the existing onclick code; sReplace = sReplace + sCode; } else { // don''t worry about the page being valid; sReplace = sReplace + sPleaseWaitCode; } // now substitute our onclick code sReplace = "onclick=\"" + sReplace; sReturn = rOnclick.Replace(sHtml, sReplace); } else { // there isn''t an existing onclick attribute; // add ours int i = sHtml.Trim().Length - 2; string sInsert = " onclick=\"" + sPleaseWaitCode + "\" "; sReturn = sHtml.Insert(i, sInsert); } return sReturn; } 这个IsValidatorIncludeScript() 利用上面的检查来查看是否有使用页面注册的asp.net验证控件的标准Javascript代码块。下面则用一个简单的方法测试了是否有验证代码和像Page_IsValid的变量存在。 private bool IsValidatorIncludeScript() { // return TRUE if this page has registered javascript // for client-side validation; this code may not be registered // if ASP.NET detects what it thinks (correctly or incorrectly) // is a down-level browser. return this.Page.IsStartupScriptRegistered("ValidatorIncludeScript"); } 下面这个GeneratePleaseWaitJavascript()构建了包含在onclick属性中的PleaseWait() Javascript函数。我们可以通过检查控件的属性来决定想要的布局。 private string GeneratePleaseWaitJavascript() { // create a JavaScript "PleaseWait()" function call // suitable for use in an onclick event handler string sMessage = ""; string sText = _pleaseWaitText; string sImage = (_pleaseWaitImage != String.Empty ? string.Format( "<img src=\"{0}\" align=\"absmiddle\" alt=\"{1}\"/>" , _pleaseWaitImage, _pleaseWaitText ) : String.Empty); // establish the layout based on PleaseWaitType switch (_pleaseWaitType) { case PleaseWaitTypeEnum.TextThenImage: sMessage = sText + sImage; break; case PleaseWaitTypeEnum.ImageThenText: sMessage = sImage + sText; break; case PleaseWaitTypeEnum.TextOnly: sMessage = sText; break; case PleaseWaitTypeEnum.ImageOnly: sMessage = sImage; break; } // return the final code chunk string sCode = string.Format( "PleaseWait(''pleaseWaitButtonDiv_{0}'', ''pleaseWaitButtonDiv2_{1}'', ''{2}'');" , this.ClientID, this.ClientID, sMessage); sCode = sCode.Replace("\"", """); return sCode; } 如果指定了一个PleaseWaitImage,就必须包含额外的一段Javascript代码来通知客户端预载该图像。这段脚本的注册应该出现在重写的OnPreRender方法中。注册的键是图像的名称;如果多个按钮都使用同一图像,预载脚本只需要实施一次。这里使用了一个正则表达式来创建Javascript图像变量,以保证特殊字字符(比如文件路径中的斜线)转化成下划线。 protected override void OnPreRender(EventArgs e) { base.OnPreRender (e); // If we''re using an image, register some javascript // for client-side image preloading if (_pleaseWaitImage != String.Empty && _pleaseWaitType != PleaseWaitTypeEnum.TextOnly) RegisterJavascriptPreloadImage(_pleaseWaitImage); } private void RegisterJavascriptPreloadImage(string sImage) { Regex rex = new Regex("[^a-zA-Z0-9]"); string sImgName = "img_" + rex.Replace(sImage, "_"); StringBuilder sb = new StringBuilder(); sb.Append("<script language=''JavaScript''>"); sb.Append("if (document.images) { "); sb.AppendFormat("{0} = new Image();", sImgName); sb.AppendFormat("{0}.src = \"{1}\";", sImgName, sImage); sb.Append(" } "); sb.Append("</script>"); this.Page.RegisterClientScriptBlock(sImgName + "_PreloadScript", sb.ToString()); } Client-side functions 嵌入的文本文件javascript.txt包含了隐藏按钮的<div>和显示"please wait"信息或图像的客户端代码。这些代码在重写的OnInit()方法中调用的私有方法RegisterJavascriptFromResource()加载。这个方法调用泛型方法GetEmbeddedTextFile() ,在这个泛型方法中把文件做为源加载而把内容返回成字符串。 protected override void OnInit(EventArgs e) { base.OnInit(e); // the client-side javascript code is kept // in an embedded resource; load the script // and register it with the page. RegisterJavascriptFromResource(); }
private void RegisterJavascriptFromResource() { // load the embedded text file "javascript.txt" // and register its contents as client-side script string sScript = GetEmbeddedTextFile("javascript.txt"); this.Page.RegisterClientScriptBlock("PleaseWaitButtonScript", sScript); }
private string GetEmbeddedTextFile(string sTextFile) { // generic function for retrieving the contents // of an embedded text file resource as a string
// we''ll get the executing assembly, and derive // the namespace using the first type in the assembly Assembly a = Assembly.GetExecutingAssembly(); String sNamespace = a.GetTypes()[0].Namespace; // with the assembly and namespace, we''ll get the // embedded resource as a stream Stream s = a.GetManifestResourceStream( string.Format("{0}.{1}", sNamespace, sTextFile) ); // read the contents of the stream into a string StreamReader sr = new StreamReader(s); String sContents = sr.ReadToEnd(); sr.Close(); s.Close(); return sContents; } javascript.txt嵌入资源包含了按钮在Javascript的onclick句柄中执行的客户端方法PleaseWait()。这段代码也调用了一个客户端方法HideDiv()以隐藏按钮的容器<div>,然后通过设置innerHTML属性把信息或图像组装进之前空的<div>标记中。辅助函数GetDiv()则是通过检查document.getElementById, document.all, 和 document.layers用id返回一个<div>对象,保证了不同浏览器的兼容性。下面是javascript.txt的全部代码: <script language="JavaScript"> function GetDiv(sDiv) { var div; if (document.getElementById) div = document.getElementById(sDiv); else if (document.all) div = eval("window." + sDiv); else if (document.layers) div = document.layers[sDiv]; else div = null; return div; } function HideDiv(sDiv) { d = GetDiv(sDiv); if (d) { if (document.layers) d.visibility = "hide"; else d.style.visibility = "hidden"; } } function PleaseWait(sDivButton, sDivMessage, sInnerHtml) { HideDiv(sDivButton); var d = GetDiv(sDivMessage); if (d) d.innerHTML = sInnerHtml; } </script> |