前几天Bēniaǒ 同学MSN询问我关于confirm在Web中的用法,据说,Bēniaǒ 长期做WinForm程序,不太熟悉Web上类似WinForm上的MessageBox.Show方法,当时很困,也不是太理解Bēniaǒ 同学要表达什么意思,就决定写一个小示例给Bēniaǒ 同学直接看看是不是,后来Bēniaǒ 同学去睡觉了,留下我一个人奋战~。Bēniaǒ 同学刚下线不久我就搞完了,把核心代码直接用MSN给他发过去了,后来Bēniaǒ 同学写了一篇《由var js = confirm("确认操作?");引发的技术难题 》,其中感谢了我,矣,只可惜依Bēniaǒ 同学所述,好像没有收到我的留言。
当时有跟Bēniaǒ 同学说了一下思路,不知道他是不是理解了。不过今天看了Bēniaǒ 同学文章的观点,又看了他对别的同学的留言的回复,我也说不清楚是Bēniaǒ 同学没理解我的思路还是我没有理解Bēniaǒ 同学的思路……(汗死)
其实Bēniaǒ 同学问我的时候就跟我讲他打算发篇文章问问,Bēniaǒ 同学发这篇《由var js = confirm("确认操作?");引发的技术难题 》的时候,我估计还在睡觉,名字也太隐秘了,我也没有在意,直到下午代码乱了 同学发了一篇《Asp.net中模仿Winform的MessageBox 》,其中提到了Bēniaǒ 同学的问题,我才想到Bēniaǒ 同学那天问过同样的问题。
经过仔细考量两位同学的代码和文字后(代码乱了 同学的文章算是很容易理解的,Bēniaǒ 同学的语言太深奥,有点绕,不是太理解~不过看了各种留言后,也了解了大概),觉得二位同学都很执着和努力,向二位同学的努力表示敬意。但是有一点没有想明白,很多留言的朋友也没有想太明白,就是“为什么非要在Web上实现Windows.Forms中的MessageBox.Show()方法呢?”。
为什么非要在Web上实现Windows.Forms中的MessageBox.Show()方法呢?
经过一些简单的分析,发现可能有这么几点理由:
1、认为MessageBox.Show的这种使用方式是一种约定俗成的必然。 Web的发展相对于WinForm的发展来说,还是很新的,大家在写第一个VB/C++/C#/Java的代码的时候,很可能就是用一个MessageBox.Show方式来写HelloWorld的,就算后来JavaScript代码盛行的时候,最常写的也是alert(msg);由于这些展示对话框的方法简单直接,很容易被人感性地理解为是一个“原子方法”,它也就自然被人们认为是一种必然。
2、认为Web是WinForm的一种延伸。 ASP.NET和很多的Web技术不同,它秉承了微软一贯的观点,就是入门容易。我们知道微软是怎么降低这个门槛的。之前大部分程序员都在做Windows程序,Windows程序的使用编写方式都被公认为一种优秀的实践,而微软期待将这种实现移植到Web上,正如微软也封装足够多的代码使一大堆的Ajax脚本变成了对程序员几乎透明的方式。这种价值观和运作方式固然带来了革命,我们可以说,没有这样的包装,没有ASP.NET,Web至今仍然不够普及或者无法胜任更多更复杂的应用。但也由此带给了程序员依赖性。就我所见,很多程序员甚至不能区分WebForm和WinForm的本质区别。这也不能怪他们,Visual Studio作为几乎是最优秀的一款IDE,让程序员省去了不少粗枝大叶,再加上微软团队的辛勤和智慧,让编程也变得普及起来,但是门槛的降低并不能解决许多高端的问题,作为一名资深的程序员,我们不可能要求自己停留在拖控件的水平。这也是很多人常说的微软程序员贬值的原因,但是观察那一堆设计的成果,所有的方便不是因为编程本身变成一件很简单的事,而是因为微软为我们思考了更多,这是很多其他软件公司和团队做不到的。也就是这种精益求精的精神,让这种包装变成了透明状。(好像跑题了)因此很多程序员认为Web应该和WinForm一样,应该用同样的编程方式去实现。但是这里面涉及到了很多复杂的东西,可能从设计上来讲是不美观的,从实现上来讲是低效的。因此如果你认为为什么这么简单的功能微软都不为你做好的时候,你就要想清楚,到底是不是自己应该换一个思路了。因为就我个人觉得,那个号称拥有全世界最精良大脑的团队,肯定不会连我都能想到的东西给遗漏了。所以80%以上的几率是我自己本身出了问题(当然了,按照2:8原理,有些东西我们应该自己做)。
现在回到那个问题,我们该如何去完成这个“确认执行”的操作呢?
从本质上讲,Web是客户从浏览器发送HTTP请求给Web服务器,然后Web服务器返回指定的页面给浏览器,然后断开连接!注意最后一步是断开连接。在PostBack模型中,我们将一些数据或者变量,存放到页面的一些变量中,文档被整体发送给了服务器,服务器根据得到的数据,存取那些他们认识的变量,看看这些变量是否发生了变化,如果发生了变化就执行相应的更新,将这些更新整合成新的Web页面,然后再将这些内容发送回客户端,如此反复,这也就是为什么有ViewState的原因。(请参考ViewState的相关文档了解具体的细节)
有的同学可能要问了,那Ajax不会刷新,那是不是就不是这样了呢?这个问题要回到Ajax的本质上来,Ajax通过一个异步的机制能够向服务器请求数据,这个机制通常是由XMLHttpRequest对象来实现的,这个对象最早由Microsoft Internet Explorer实现,但是那个时候用的人很少,总之还没有形成规模也没有Ajax这个词。它的存在完全只是因为微软要在Microsoft Outlook中使用该组件,但是诸多原因,现在它红透了,导致很多人又被扑面而来的一大堆概念弄昏了头脑。从本质上,我们还是发送了一个HTTP请求,然后对回复的文本进行了处理。但可能因为Ajax涉及到了比较多的JavaScript,因此很多人反倒弄清了一些WebForm上的概念,但有些人可能越来越糊涂了。
那么对一个静态HTML无交互的页面,我们通常怎么做呢?(保存这段代码到<filename>.html文件,打开即可)
静态HTML页面confirm
1 <! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
2 < html xmlns ="http://www.w3.org/1999/xhtml" >
3 < head >< title >
4 StaticConfirm
5 </ title ></ head >
6 < body >
7 < form name ="form1" method ="post" action ="StaticConfirm.aspx" id ="form1" >
8 < div >
9 < input type ="hidden" name ="__VIEWSTATE" id ="__VIEWSTATE" value ="/wEPDwUJNzgzNDMwNTMzZGTpzEzLFWePmfM+hko0f46EJgnVkw==" />
10 </ div >
11
12 < div >
13 < span id ="label1" ></ span >
14
15 < input type ="button" onclick ="custom_confirm(yes1,no1);" value ="execute" />
16 < div id ="result" style ="background-color: Yellow;" >
17 </ div >
18 </ div >
19
20 < script language ="javascript" type ="text/javascript" >
21 function yes1() {
22 try
23 {
24 document.getElementById(" label1 " ).innerHTML = " Have execute!<br /> " ;
25 }catch (e) {}
26 }
27 function no1() {
28 try {
29 document.getElementById(" label1 " ).innerHTML = "" ;
30 }catch (e) {}
31 }
32 function custom_confirm(yes, no) {
33 if (window.confirm( ' Are you sure? ' )) {
34 // yes
35 if (yes)
36 yes();
37 document.getElementById(' result ' ).innerHTML = " Completed! " ;
38 }
39 else {
40 // no
41 if (no)
42 no();
43 document.getElementById(' result ' ).innerHTML = " You cancel the operate! " ;
44 }
45 document.getElementById(' result ' ).style.display = "" ;
46 setTimeout(function () {
47 document.getElementById(' result ' ).style.display = " none " ;
48 }, 2000 );
49 }
50 </ script >
51
52 </ form >
53 </ body >
54 </ html >
是的,这几乎是Web中最简单且唯一的处理这种“确认执行”的形式了。它利用了JavaScript中的window.confirm方法,弹出阻塞浏览器进程的对话框以实现征求用户同意的方法。这种方式相信大家一定都耳熟能详了。
下面我们将它移植到ASP.NET中,似乎这种说法并不准确,按照Bēniaǒ 同学的说法,就是要在后台代码(CodeBehind)中编写上述逻辑。以实现确认以及取消的操作。Bēniaǒ 同学的思路核心是像在WinForm中所惯用的,也就是从MessageBox.Show的返回值中得知用户的确认情况,究竟是前进还是后退。但是这一点Bēniaǒ 同学最后认为不可能,好像是无法解决的,但其实要把是否确认的信息返回服务端是完全可能的,随便找一个隐藏域就可以把该数据传回服务端了。但这里只解决了将“确认状态”传回的功能,但是却没有完成后续逻辑的执行。
论:在一次回发中不可能完成Winform中的相似代码!
什么是Winform中使用MessageBox.Show的相似代码?这些代码是什么?下面的代码片段展示了基本的MessageBox.Show的代码:
DialogResult result = MessageBox.Show( " Are you sure? " , " Warning " , MessageBoxButtons.YesNo, MessageBoxIcon.Information);
if (result == DialogResult.Yes)
{
// TODO
}
else
{
// TODO
}
相信不需要任何解释大家就可以看明白这段代码,现在我们将它们放到一个WebForm的按钮中(假设在VS中我们在页面拖放一个Button,双击Button在相应的Button1_Click方法内插入以上代码)。
这个时候我们需要确定我们的MessageBox.Show方法应该具备哪些输入输出,可以看出,我们需要传入一些提示信息,返回用户的选择(通常是“是”/“否”)。因此,我们在代码第一行中则需要“等待用户输入”,而我们知道我们在Web中,这种等待是不切实际的,假设有一种等待机制的话,那么假设我们的用户在弹出框后关闭浏览器进程了(这里都不想说离开去吃饭了),那么结果就是这个等待将会无限期地等待下去,当然有的同学会说可以设置过期时间,但是就服务器有限的连接数一条原则也不允许我们这么做。这个方法必须枪毙,相信大家也都会认同。
在上述代码中TODO中的内容则为实现逻辑,我们按下按钮后执行该方法(Button1_Click),要么是,要么否,但是既然客户的确认信息无法传回来,我们是不能臆断客户的选择。所以我们这个判断将无法执行下去。
深入点讲,当我们按下一个按钮的时候,我们必然引起一次回发,回发表明我们应该执行该方法(Button1_Click),该方法则要求客户端告诉我们用户的选择是什么。这必然导致用户的选择信息需要再次传到服务端,这样我们综合这两个信息,才能完成这个判断。但是这时候就产生了一个死锁。用户在等服务端,服务端在等用户。
可能有同学说了,代码乱了 同学的那篇《Asp.net中模仿Winform的MessageBox 》中,就是这么做的,确实可以啊!没错,但是我们需要看清楚,代码乱了 同学用了两次回发的方式。可能你执行了代码乱了 同学提供的代码后并看不出回发,但是你要注意到他采用了一个UpdatePanel,也就是它的回发对你隐藏了而已。两次回发必然带来性能的严重损耗,当然你非得说这个严重程度强烈到跑不动那也不至于,我只是提醒您一下,那个方法有一点固然的BUG。即便你不在乎Ajax下的性能损耗,你也应该去掉Ajax UpdatePanel看看提示效果。在你看到确认框的时候页面已经回发了,你看到的是空白的页面。对于示例可能你看不出什么问题,假想一下博客园的首页一个按钮点击后一片空白会是什么场景。也就是它受限于Ajax UpdatePanel(或者类似的机制)。另外我还是需要强调一下性能,为什么我们用Ajax?因为我们期待更好的用户体验和性能提升,然而很多朋友只在乎用户体验却不在乎性能,这将成为一种隐患,也是一个不好的习惯,程序员虽然不应该吹毛求疵,但也要有基本的精益求精的精神嘛。
既然我们需要在一次回发中把这些都搞定,那我们该怎么做呢?
其实针对这个问题,代码乱了 同学的代码看上去确实很酷,只可惜多了一次回发,而且在无UpdatePanel时的效果欠佳。简单的分析过后发现这几乎是个不可能改进的缺陷。我们只能寻找固有的方式去完成这个任务。
这个任务既有它的局限性,我们是否真的应该用WinForm的那种方式来实现呢?
其实代码乱了 同学的代码稍微改一下就可以避免二次回发的毛病了,不过很可惜的是,那样的修改同样需要改动调用方式,结果就不是那么美了。
下面是我提供的两个简单的思路,希望能够抛砖引玉,我的目的绝不是在宣扬这样两种或者其他类似的方式,我旨在告诫人们Web不是WinForm,不同的东西为什么总要扯到一起呢?
思路1:
既然我们点击按钮,那么我们在页面中通过confrim必然产生一个true/false,纯脚本的方式,我们对true执行一段代码,对false的时候也执行一段代码。而现在,我们对true的时候放行,它将按照正常模式回到服务端执行Button1_Click中的代码,对于false的时候,如果我们允许执行代码,为了避免控件事件验证的诸多麻烦,我们直接向页面添加一个隐藏的按钮,将我们的执行逻辑分配给这个隐藏按钮,通过得到该隐藏按钮再模拟点击,即可完成任务。
思路1调用代码
public partial class Confirm1 : System.Web.UI.Page
{
protected void Page_Load( object sender, EventArgs e)
{
btnUpdate.RegisterShow( " Are you sure? " , cancel_Update);
lbtnUpdtate.RegisterShow( " Are you sure? " , (obj, args) =>
{
// TODO:Your logical here.
Response.Write( " cancel_Update: " + txtText.Text);
// TODO:Your logical here.
});
btnUpdateWithoutCancelCode.RegisterShow( " Are you sure? " );
}
protected void btnUpdate_Click( object sender, EventArgs e)
{
// TODO:Your logical here.
Response.Write( " btnUpdate_Click: " + txtText.Text);
// TODO:Your logical here.
}
protected void lbtnUpdtate_Click( object sender, EventArgs e)
{
// TODO:Your logical here.
Response.Write( " lbtnUpdate_Click: " + txtText.Text);
// TODO:Your logical here.
}
protected void btnUpdateWithoutCancelCode_Click( object sender, EventArgs e)
{
// TODO:Your logical here.
Response.Write( " btnUpdateWithoutCancelCode_Click: " + txtText.Text);
// TODO:Your logical here.
}
protected void cancel_Update( object sender, EventArgs e)
{
// TODO:Your logical here.
Response.Write( " cancel_Update: " + txtText.Text);
// TODO:Your logical here.
}
}
思路1核心代码
public static class MessageBox
{
public static void RegisterShow( this WebControl registerControl, string message)
{
RegisterShowControl(registerControl, message, " onclick " , null );
}
public static void RegisterShow( this WebControl registerControl, string message, EventHandler wrongLogical)
{
RegisterShowControl(registerControl, message, " onclick " , wrongLogical);
}
public static void RegisterShowControl(WebControl registerControl, string message, string clientEventName, EventHandler wrongLogical)
{
System.Web.UI.Page page = registerControl.Page;
string rightLogicalCode = page.ClientScript.GetPostBackEventReference(registerControl, string .Empty);
string wrongLogicalCode = string .Empty;
if (wrongLogical != null )
{
Button btnCancel = new Button();
btnCancel.Attributes[ " style " ] = " display:none; " ;
btnCancel.Click += new EventHandler(wrongLogical);
page.Form.Controls.Add(btnCancel);
wrongLogicalCode = " var _confrim_cancel_control_ = window.document.getElementById(' " + btnCancel.ClientID + " '); if(_confrim_cancel_control_){_confrim_cancel_control_.click();} " ;
}
string jscodeA = " javascript:if(confirm(' " + message + " ')){function _confirm_(){ " + rightLogicalCode + " ; " ;
string jscodeB = " } _confirm_();}else{ " + (wrongLogical != null ? wrongLogicalCode : " return false; " ) + " ; return false;} " ;
if (registerControl.Attributes[clientEventName] != null && registerControl.Attributes[clientEventName].Contains( " _confirm_() " ))
registerControl.Attributes[clientEventName] = string .Empty;
registerControl.Attributes[clientEventName] = jscodeA + registerControl.Attributes[clientEventName] + jscodeB;
}
}
思路2:
既然我们有了confirm,那么confirm必然有结果。既然我们的点击势必要回发,我们就可以将我们confirm的结果存在页面上,然后在回发到服务端后找回这个confirm的结果,然后执行指定的逻辑。
思路2调用代码
public partial class Confirm2 : System.Web.UI.Page
{
// MessageBoxHelper msg = new MessageBoxHelper();
protected void Page_Load( object sender, EventArgs e)
{
// msg.RegistorConfirmContorl(btnUpdate1, "Are you sure?");
// msg.RegistorConfirmContorl(btnUpdate2, "Surely?");
btnUpdate1.BeginConfirm(" Are you sure? " );
btnUpdate2.BeginConfirm( " Surely? " , false );
lbtnUpdate1.BeginConfirm( " Are you sure? " );
}
protected void btnUpdate1_Click( object sender, EventArgs e)
{
// if (msg.GetConfirmStatus(btnUpdate1))
if (btnUpdate1.EndConfirm())
{
Response.Write( " You have confirm1! " );
}
else
{
Response.Write(" You clicked cancel1! " );
}
}
protected void btnUpdate2_Click( object sender, EventArgs e)
{
// if (msg.GetConfirmStatus(btnUpdate2))
if (btnUpdate2.EndConfirm())
{
Response.Write( " You have confirm2! " );
}
else
{
Response.Write(" You clicked cancel2! " );
}
}
protected void lbtnUpdate1_Click( object sender, EventArgs e)
{
if (lbtnUpdate1.EndConfirm())
{
Response.Write( " You have confirm3! " );
}
else
{
Response.Write(" You clicked cancel3! " );
}
}
}
思路2核心代码
public static class MessageBoxS
{
/// <summary>
/// Begin the Confirm, you must use this method to let the control has the ability to confrim message.
/// </summary>
/// <param name="control"></param>
/// <param name="message"></param>
public static void BeginConfirm( this WebControl control, string message)
{
MessageBoxHelper.RegisterConfirmContorl(control, message, true );
}
/// <summary>
/// Begin the Confirm, you must use this method to let the control has the ability to confrim message.
/// </summary>
/// <param name="control"></param>
/// <param name="message"></param>
/// <param name="hasCancelHandler"> To indicate if the confirm method have the cancel method. </param>
public static void BeginConfirm( this WebControl control, string message, bool hasCancelHandler)
{
MessageBoxHelper.RegisterConfirmContorl(control, message, hasCancelHandler);
}
/// <summary>
/// By the method, you can get the result of confirm
/// </summary>
/// <param name="control"></param>
/// <returns></returns>
public static bool EndConfirm( this WebControl control)
{
return MessageBoxHelper.GetConfirmStatus(control);
}
}
public class MessageBoxHelper
{
/// <summary>
/// Retrieve the HiddenField
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private static HiddenField RetrieveTempStorage(Page page)
{
string tempStorageName = CreateHiddenName();
foreach (Control ctl in page.Form.Controls)
{
if (ctl.ClientID == tempStorageName)
return ctl as HiddenField;
}
return null ;
}
/// <summary>
/// Get the HiddenField's Name/ClientID
/// </summary>
/// <returns></returns>
private static string CreateHiddenName()
{
return " _Confirm " ;
}
private const char CONTROL_SEPARATOR = ' | ' ;
private const char VALUE_SEPARATOR = ' : ' ;
/// <summary>
/// Register the control as the confirm control.
/// </summary>
/// <param name="control"></param>
/// <param name="message"></param>
/// <param name="hasCancelHandler"></param>
public static void RegisterConfirmContorl(WebControl control, string message, bool hasCancelHandler)
{
string scriptBlockKey = " _confirm " ;
if ( ! control.Page.ClientScript.IsClientScriptBlockRegistered(scriptBlockKey))
{
// Create hidden area
if (RetrieveTempStorage(control.Page) == null )
{
HiddenField tempStorage = new HiddenField();
tempStorage.ID = CreateHiddenName();
control.Page.Form.Controls.Add(tempStorage);
}
// Export the _confirm javascript code.
StringBuilder _confirm_js = new StringBuilder();
_confirm_js.Append( " <script language=\ " javascript\ " type=\ " text / javascript\ " > " );
_confirm_js.Append( " function _confirm(targetId, message) { " );
_confirm_js.Append( " var result = window.confirm(message); " );
// only IE(there is a method to extend the event in firefox.)
_confirm_js.Append( " var hidden = document.getElementById(' " + RetrieveTempStorage(control.Page).ClientID + " '); " );
_confirm_js.Append( " if (hidden) { " );
_confirm_js.Append( " hidden.value = targetId + \ "" + VALUE_SEPARATOR.ToString() + " \ " + result + \ "" + CONTROL_SEPARATOR.ToString() + " \ " ; " );
_confirm_js.Append( " } " );
_confirm_js.Append( " return result; " );
_confirm_js.Append( " } " );
_confirm_js.Append( " </script> " );
control.Page.ClientScript.RegisterClientScriptBlock(control.GetType(), scriptBlockKey, _confirm_js.ToString());
}
if (control.Attributes[ " onclick " ] != null && control.Attributes[ " onclick " ].Contains( " _confirm( " ))
control.Attributes[ " onclick " ] = string .Empty;
if ( ! hasCancelHandler)
control.Attributes[ " onclick " ] += " var result= " ;
control.Attributes[ " onclick " ] += " _confirm(' " + control.ClientID + " ',' " + message + " '); " ;
if ( ! hasCancelHandler)
control.Attributes[ " onclick " ] += " if(!result) return false; " ;
}
/// <summary>
/// Analyse the HiddenField to findout the result of the confirm form.
/// </summary>
/// <param name="control"></param>
/// <returns></returns>
public static bool GetConfirmStatus(WebControl control)
{
HiddenField tempStorage = RetrieveTempStorage(control.Page);
if (tempStorage != null )
{
string [] controls = tempStorage.Value.Split(CONTROL_SEPARATOR);
foreach ( string ctl in controls)
{
if (ctl.Trim() != string .Empty)
{
string [] ctlPair = ctl.Split(VALUE_SEPARATOR);
if (ctlPair == null || ctlPair.Length != 2 )
{
throw new ArgumentException( " The hidden code is error! " );
}
else
{
if (ctlPair[ 0 ] == control.ClientID)
{
tempStorage.Value = string .Empty;
return ctlPair[ 1 ].Trim().ToLower() == " true " ;
}
}
}
}
}
return false ;
}
}
这两个思路都很淳朴和原始,所以不会引来多次回发这样的性能问题,而且不同于WinForm的调用方式,旨在告诉WinForm用户Web与WinForm不是相同的。
软件下载
# Non-members may check out a read-only working copy anonymously over HTTP.
svn checkout http://v-labs.googlecode.com/svn/trunk/ v-labs-read-only
download http://code.google.com/p/v-labs/downloads/list
点此下载:
http://v-labs.googlecode.com/files/WebAppConfirmControl0.1.zip