2012年8月22日 星期三

Asp.Net Ajax 生命週期

看到 Asithangae 把 ASP.Net 使用 Ajax 技術後的前端事件生命週期寫得這麼詳細,真是令人讚嘆。 閱讀後,大致上對於前端的事件,清楚許多。

先以一般的事件來說,大概可分為三種:

一、pageInit   (ref. Sys.Application.init 事件)
當頁面第一次被載入,或是整個頁面觸發 post back 之後會發生。但如果是非同步的請求,也就是僅有部份頁面被更新,則不會發生。
Sys.Application.add_init(MyInit);

function MyInit()
{
    alert("MyInit added to page init");
}

二、pageLoad  (ref. Sys.Application.load 事件)
他的觸發順序是在 pageInit 之後。他觸發的時機點跟 pageInit 相似,當頁面第一次被載入,或是整個頁面觸發 post back 之後會發生。唯一不同的地方,是非同步的請求,也就是僅有部份頁面被更新時,他也會發生。
Sys.Application.add_load(MyLoad);

function MyLoad()
{
    alert("MyLoad added to page load");
}

三、pageUnload  (ref. Sys.Application.unload 事件)
當要離開頁面的時候,這事件會被觸發。例如程式裡面使用了 Response.Redirect() 或是 Server.Transfer()。
Sys.Application.add_unload(MyUnload);

function MyUnload()
{
    alert("MyUnload added to page unload");
}


此外,針對非同步請求,則又可以分為以下五種:

一、initializeRequest (ref. Sys.WebForms.PageRequestManager initializeRequest 事件)
觸發的時機點,是使用者壓下頁面上的控制項,準備要執行非同步更新的請求。只是目前這個請求,尚未送到後端的伺服器。既然這請求尚未送到後端,表示在這時間點,我們有機會執行取消或放棄請求的動作。
Sys.WebForms.PageRequestManager.getInstance().add_initializeRequest( MyIntializeRequest );

function MyIntializeRequest(sender,args)
{
    alert("My Request is getting initalized");
}


二、beginRequest (ref. Sys.WebForms.PageRequestManager beginRequest 事件)
這事件是發生在 initializeRequest 之後,此時的非同步更新請求已經傳送到後端伺服器,並等待處理。通常在這時候,可以擺放「處理中」的動畫圖示。
Sys.WebForms.PageRequestManager.getInstance().add_beginRequest( MyBeginRequest );

function MyBeginRequest(sender,args)
{
    alert("My Request is ready about to sent to server");
}

三、pageLoading (ref. Sys.WebForms.PageRequestManager pageLoading 事件)
當後端伺服器處理完我們的非同步更新請求後,在尚未把處理結果交予前端之前,會觸發這個事件。
Sys.WebForms.PageRequestManager.getInstance().add_pageLoading( MyPageLoading );

function MyPageLoading(sender, args)
{
    alert("My page is started loading");
}

四、pageLoaded (ref. Sys.WebForms.PageRequestManager pageLoaded 事件)
當前端頁面完成所有非同步更新請求的結果後,會觸發這個事件。我們原先在 beginRequest 所擺放的「處理中」動畫圖示,在這個時間點就可以撤除了。
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded( MyPageLoaded );

function MyPageLoaded(sender, args)
{
    alert("My page is loaded");
}

五、endRequest (ref. Sys.WebForms.PageRequestManager endRequest 事件)
這個事件是整個非同步更新請求的最後一個,當他結束後,就會把控制權還給使用者。而再這個事件裡,也會回傳此次非同步更新請求的結果,例如:失敗或成功,如果有需要做紀錄(log),也是在這個事件裡面來撰寫。
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(MyEndRequest);

function MyEndRequest(sender, args)
{
    alert("My Request has end");
}



參考「Ajax 用戶端生命週期事件」一文,假設非同步回傳的狀況假設如下:

頁面包含一個 ScriptManager 控制項,且控制項的 SupportsPartialRendering 和 EnablePartialRendering 屬性皆為 true。

頁面包含一個 UpdatePanel 控制項,且控制項的 ChildrenAsTriggers 屬性為 true。

UpdatePanel 控制項內部的按鈕會啟始非同步回傳。

伺服器成功傳回回應。

下列用戶端事件會依此順序發生:

1.按一下 UpdatePanel 內部的按鈕,該按鈕會啟始非同步回傳。

2.PageRequestManager 執行個體會引發 initializeRequest 事件。

3.PageRequestManager 執行個體會引發 beginRequest 事件。

4.傳送要求至伺服器。

5.用戶端收到回應。

6.PageRequestManager 執行個體會引發 pageLoading 事件。

7.PageRequestManager 執行個體會引發 pageLoaded 事件。

8.Application 執行個體會引發 load 事件。

9.PageRequestManager 執行個體會引發 endRequest 事件。

請注意,Application 執行個體的 load 事件,是在 PageRequestManager pageLoaded 事件之後、及其 endRequest 事件之前引發的。



在上面所介紹的各種事件觸發裡,實際測試後,發現 pageUnload 其實並沒這麼單純。
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>    
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>
        <script>
            function MyUnload() {
                alert('this is my unload');              
            }

            Sys.Application.add_unload(MyUnload);
        </script>    
    </div>
    </form>
</body>
</html>

這個範例裡,期待當網頁關閉,或要切換到別的頁面時,會出現「this is my unload」的訊息。這需求在 IE 瀏覽器可以正常執行,但是在 Firefox、Chrome 則完全失效。以前 IE 獨大的年代或許可以不考慮這問題,但今非昔比,接下來就是要特別處理 unload 事件了。
參考先前 網頁離開前跳出確認視窗 的文章,改成去攔 window.onbeforeunload 事件。這時候,我把原先 Sys.Application.add_unload(MyUnload)  改成使用 Sys.UI.DomEvent.addHandler(window, "beforeunload", MyUnload)後,的確有些改善,Firefox 可以在離開網頁前跳出「this is my unload」訊息,但 chrome 還是不行。原因是 Chrome 在 onbeforeunload 事件,如果要在當下跳出訊息,不可以用 alert( ),而是要用 return( )。 也就是要寫成:return 'this is my unload'; (參考:unload not working in Chrome)。為了同時讓這三大主流瀏覽器同時能夠支援,所以我在 MyUnload( ) 事件裡,就同時寫了:  alert('this is my unload');   return 'this is my unload';  如此才讓三者相安無事,彼此共存。


<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>    
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>
        <script>
            function MyUnload() {
                alert('this is my unload');
                return 'this is my unload';
            }
          
            Sys.UI.DomEvent.addHandler(window, "beforeunload", MyUnload);
        </script>    
    </div>
    </form>
</body>
</html>


以上是針對 Unload 事件時跳出訊息( alert )的處理,如果是要產生確認 ( confirm )的效果,可以參考網頁離開前跳出確認視窗 的作法。

參考:
01:The Page Life Cycle of Client [Browser]
02:網頁離開前跳出確認視窗
03:unload not working in Chrome
04:Sys.Application.init 事件
05:Sys.Application.load 事件
06:Sys.Application.unload 事件
07:Sys.WebForms.PageRequestManager initializeRequest 事件
08:Sys.WebForms.PageRequestManager beginRequest 事件
09:Sys.WebForms.PageRequestManager pageLoading 事件
10:Sys.WebForms.PageRequestManager pageLoaded 事件
11:Sys.WebForms.PageRequestManager endRequest 事件
12:Ajax 用戶端生命週期事件

2012年8月16日 星期四

在 UpdatePanel 裡面使用 javascript

近日在使用 Asp.Net 所提供的 Ajax 功能時,對於 UpdatePanel 與 JavaScript 之間的愛恨糾葛,還是跳出來說幾句話好了。

從自己最常使用的功能說起。在網頁上放個 Button,壓下去後,跳出一個 alert 視窗,程式如下:
<form id="form1" runat="server">
<div>
    <asp:button id="Button1" runat="server" onclick="Button1_Click" text="Button" />
</div>
</form>
以及 .cs 程式
protected void Button1_Click(object sender, EventArgs e)
{
    ClientScript.RegisterStartupScript(this.GetType(), "JustAlert", 
        "<script> alert('Hello');</script>");
}
但當我將這個 button 移到 UpdatePanel 裡,就開始不正常了。我的 javascript 完全沒有反應,也沒有錯誤訊息。詳閱了「RegisterStartupScript Method (control, type, key, script, addScriptTags)」後,發現程式需要做些調整。該文章有提到,如果是在 UpdatePanel 裡,當我們使用了非同步更新的方法,要改用 ScriptManager 的 RegisterStartupScript,如下所示:

protected void Button1_Click(object sender, EventArgs e)
{
    ScriptManager.RegisterStartupScript(this.Page, this.GetType(),
        "JustAlert", "alert('Hello');", true);
}

既然在 UpdatePanel 裡需改用 ScriptManager 來呼叫 JavaScript,那理論上我在後端也可以呼叫前端所定義的 JavaScript 囉!所以,再試了一下:
/* .aspx  */
<head runat="server">
    <title></title>
    <script>
        function SayHi() {
            alert('Hi');
        }
    </script>
</head>

/* .cs */
ScriptManager.RegisterStartupScript(this.Page, this.GetType(),
    "JustAlert", "SayHi();", true);
果然可行。原來呼叫前端的 javascript 都沒問題,那是否也可以在 UpdatePanel 裡觸發整個頁面的 PostBack 呢?
ScriptManager.RegisterStartupScript(this.Page, this.GetType(),
    "JustAlert", string.Format("__doPostBack('{0}','');", this.ClientID), true);
測試後,還蠻好用的,能夠順利的讓整個頁面都 PostBack。

這讓我想到一個情境。如果我的頁面很複雜很複雜,其中會有一個按鈕。當使用者按下這個按鈕後,會在後端先去做初步的判斷,決定是否要進一步去執行後續的動作,如果判斷沒必要繼續往後執行,則會在前端跳出訊息。但因為畫面很複雜很複雜(想像一下嘛,總有一天你會遇到),當被判斷沒必要往後執行時,也不希望造成整個頁面的 PostBack 而重載所有物件。

這情境下,我可以把確認按鈕,放到 UpdatePanel 裡面,並在 Button 事件裡,先去判斷是否有必要繼續往後執行。如果有必要,就會執行上面所說的,在 UpdatePanel 裡面觸發前端的 __doPostBack() 來達到頁面 PostBack 的效果。相反地,如果判斷沒有必要往後執行,則是透過 ScriptManager.RegisterStartupScript() 來產生前端 javascript 的 alert( ) 效果。

以下是測試的範例程式:
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>   
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>   
        <asp:Label ID="Label1" runat="server" Text=""></asp:Label>
        <div>
        <img src="https://encrypted-tbn2.google.com/images?q=tbn:ANd9GcSloWls9-zDJUF2RvunIn3-5zB8HJ-PTGJ3tc_jXIMJcbxBDvu7DQ" border="0"></img>

        </div>
        <asp:RadioButtonList ID="rbl" runat="server" 
            RepeatDirection="Horizontal">
            <asp:ListItem Value="1">未滿18歲</asp:ListItem>
            <asp:ListItem Value="2">滿 18 歲</asp:ListItem>
        </asp:RadioButtonList>

        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                <asp:Button ID="Button1" runat="server" onclick="Button1_Click" Text="確認" />
            </ContentTemplate>
        </asp:UpdatePanel>

    
    </div>
    </form>
</body>

public partial class ChangeMsgShow4 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //取得是誰觸發了 PostBack
        string strTarget = "" + Request.Form["__EVENTTARGET"];
        //判斷觸發 PostBack 的物件名稱是否為 MySpecial
        if (strTarget == "MySpecial")
        {
            //執行自訂的後續執行方法
            DoSpecial();
        }
    }

    private void DoSpecial()
    {
        Label1.Text = "看蜘蛛人有這麼麻煩嗎?";
    }

    protected void Button1_Click(object sender, EventArgs e)
    { 
        switch(rbl.SelectedValue)
        {
            case "1":
                //判斷不需要繼續往後執行,所以只在前端 alert 一些訊息
                ScriptManager.RegisterStartupScript(this.Page, this.GetType(),
                    "JustAlert", "alert('孩子,等待是值得的!');", true);
                break;

            case "2":
                //判斷後有必要往後執行,所以在前端先跳出一段訊息後,接著觸發 __doPostBack,並
                //把觸發 PostBack 的物件來源名稱設定為 MySpecial
                ScriptManager.RegisterStartupScript(this.Page, this.GetType(),
                        "JustAlert", "alert('恭喜你已經成年!'); __doPostBack('MySpecial','');", true);
                break;

            default:
                //判斷不需要繼續往後執行,所以只在前端 alert 一些訊息
                ScriptManager.RegisterStartupScript(this.Page, this.GetType(),
                    "JustAlert", "alert('請摸著良心選擇!');", true);
                break;

        }
    }
}

以前 Asp.Net 的前端與後端,總像是牛郎織女一樣,久久才會見面一次。但透過 Ajax 的技術,讓這兩人隨時都可以明來暗去,樂鬧許多。
參考:
01:RegisterStartupScript Method (control, type, key, script, addScriptTags)

2012年8月14日 星期二

在 WinForm 使用 HtmlEncode

今天想在 VS2010  WinForm 的專案下,使用 HtmlEncode( ) 方法對使用者輸入的內容進行編碼,上網 Google 了一下,是要使用 :

System.Web.HttpUtility.HtmlEncode(str)
啥!還是不能用,於是再 Google 了一次,原來還要加入參考: System.Web.dll
於是接著進行很簡單的加入參考的動作,但...我的清單裡竟然找不到 System.Web ,不會吧~

後來在「visual studio 2010 winform程序不能添加对system.web的引用」有提到,如果在 WinForm 環境下要加入 System.Web 參考,需先確認開發環境的 Target FrameWork 是否為 .Net FrameWork 4,很不巧的,我打開 Project ->Project Properties->Target framework,發現自己的設定竟然是:.Net Framework 4 Client Profile。於是將他調整到 .Net Framework 4 後,在加入參考的候選清單中,就出現了 System.Web 了。


只是有點好奇,為何 .Net Framework 4 Client Profile 與  .Net Framework 4 會存在著如此不同的差異呢?原來,.Net Framework 的可轉散佈檔目前的檔案大小已經肥到 231 MB(公斤) ,一般來說,工程師所寫的程式,都遠比他小很多很多。所以出現了 Client Profile 的解決方案,也就是所謂的瘦身專案。既然是瘦身,可想而知有些不常用的功能,預設就不會加進來。

可以在「Assemblies in the .NET Framework Client Profile」查閱到瘦身後的 Framework 可參考的  Assemblies。而我所需要的 System.Web,其實並不在裡面。依目前的需求來看,我是沒辦法使用瘦身專案了,只能選擇原先的 .Net Framework 4。不過即使是胖胖的,只要樂觀開朗,也可以過的很開心。至少,你吃下去的幸福,就比別人多了一點,當然享受到的快樂也就比別人多一些!


參考:
01: visual studio 2010 winform程序不能添加对system.web的引用
02: Assemblies in the .NET Framework Client Profile
03: .NET Framework 4 与 .NET Framework 4 Client Profile
04: [VS2010] .NET Framework 4.0 Client Profile