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

2012年7月24日 星期二

複製資料到有自動增值欄位的資料表

我一直以為,在 SQL Server 複製資料表A 到資料表 B 不是件難事,直到遇到「有自動增值欄位的資料表」。

舉例來說,以下是兩個一模一樣的資料結構 TB1 與 TB2 :
CREATE TABLE [dbo].[TB1](
 [SN] [int] IDENTITY(1,1) NOT NULL,
 [SName] [nvarchar](50) NULL
) ON [PRIMARY]


CREATE TABLE [dbo].[TB2](
 [SN] [int] IDENTITY(1,1) NOT NULL,
 [SName] [nvarchar](50) NULL
) ON [PRIMARY]


當我執行
insert into TB2 select * from TB1
會遇到下面錯誤訊息:
位於資料表 'TB2' 的識別欄位其外顯值只有當使用了資料行清單且 IDENTITY_INSERT 為 ON 時才能指定。

為了能夠解決上述問題,必須暫時讓「有自動增值的欄位」變成沒有自動增值。可以到管理介面去變更資料表的定義,如下:(但記得匯完資料之後要記得改回去)

另外也可以透過程式來完成:
--表示在有自動增長欄位的情況下還允許新增
SET IDENTITY_INSERT  TB2 ON 

--要注意,不可以用 select * from TB1,
--必需詳細的指出欄位
insert into TB2 (SN,SName)
select SN,SName from TB1

--關閉在有自動增長欄位的情況下還允許新增
SET IDENTITY_INSERT  TB2 OFF 


需特別注意的地方有二:
一、透過 IDENTITY_INSERT ON/OFF 來完成
二、不可以用 select * from ,需明確指出各個欄位名稱,如 select SN, SName from

自己實際遇到的資料表內容,其實非常巨大。當我使用第一種方式去設定時,匯出資料沒問題,但當我要將「自動增長欄位」的欄位改回「是」的時候,SQL Server 就會跳出訊息,告訴我偵測到大量的資料,警告我會花很多時間,問我要不要等待,當然選要,結過,他跑一陣子之後就發出逾時的錯誤訊息而中斷工作,而自動增長欄位的設定也沒有改回去。遇到這情況,就一定得改用剛剛介紹的第二種方式來處理。


參考:
01:要將資料新增至有自動增值欄位的資料表中

2012年7月13日 星期五

物件為何要 new 完之後才可以使用?

關於「物件為何要 new 完之後才可以使用?」這個問題,我在 Java 的書上看到作者將程式語法搭配記憶體的配置,解釋的很清楚,也分享一下。

MyObj obj=new MyObj(10);

把上述語法拆開來,分成三個部份。

首先是: MyObj obj
宣告了 obj 這個變數,會在記憶體中,如下圖,先配置一塊空間給 obj。而這塊記憶體空間,目前是空的(null),所以我們現在並沒辦法存取 obj 裡的資料。

接著是: new MyObj(10)
這個動作,會在記憶體另外配置一塊空間,而參數10,也一併會寫入這塊記憶體空間。

最後是:MyObj obj = new MyObj(10);
將 MyObject 的記憶體位址(0x1a2b3c4d),寫到 obj 的記憶體空間裡。日後,從 obj 就可以存取 MyObject 這塊記憶體裡的資料了。



參考:
01:Java SE 6全方位學習

2012年7月6日 星期五

透過 jQuery 取得 Asp.Net 裡 CheckBoxList 被勾選的文字(二)



先前曾經在「透過 jQuery 取得 Asp.Net 裡 CheckBoxList 被勾選的文字」文章記錄了當時的解決方法,最近再次遇到同樣的問題,試著換另一種做法來處理。

CheckBoxList 在網頁上的原始碼如下:

<table id="cb" border="0">
<tr>
 <td><input id="cb_0" type="checkbox" name="cb$0" />
 <label for="cb_0">paladin</label>
 </td>
</tr><tr>
 <td><input id="cb_1" type="checkbox" name="cb$1" />
 <label for="cb_1">ltt</label>
 </td>
</tr><tr>
 <td><input id="cb_2" type="checkbox" name="cb$2" />
 <label for="cb_2">lee</label>
 </td>
</tr>
</table>


在上面程式可以發現,CheckBox 的內容,是由後面緊接著的 Label 來定義。這次透過 jQuery 所提供的 .next() 函數,去找到所挑選物件的下一個兄弟姊妹,剛好 Label 正是 CheckBox 的第一順位兄弟姊妹。換言之,只要利用:

$("#cb_0").next().text()

就可以得到第一個 CheckBox 的值 :「paladin」 了。而最初目的:透過 jQuery 取得 Asp.Net 裡 CheckBoxList 被勾選的文字,就可以改寫如下:

<script>
    function ShowData() {
        var str = "";
        $("input[type=checkbox]").filter(":checked").each(function () {
            str = str + $(this).next().text() + ',';
        });

        alert(str);
    }

    $(function () {

        $("input[type=checkbox]").click(function () {
            ShowData();
        });           
    });
</script>

比較起來,這個寫法似乎比過去的簡潔許多。

但如果 CheckBoxList 的 RepeatLayout 屬性設為 Flow,這方法是否依然可行呢? RepeatLayout=Flow 的 HTML Code 如下:
<span id="cb_flow">
 <input id="cb_flow_0" type="checkbox" name="cb_flow$0" />
 <label for="cb_flow_0">paladin</label><br />
 <input id="cb_flow_1" type="checkbox" name="cb_flow$1" />
 <label for="cb_flow_1">ltt</label><br />
 <input id="cb_flow_2" type="checkbox" name="cb_flow$2" />
 <label for="cb_flow_2">lee</label>
</span>

CheckBox 後面的第一個兄弟姊妹,正是我們所要的值,所以這方法依然還是行的通。
參考:
01:透過 jQuery 取得 Asp.Net 裡 CheckBoxList 被勾選的文字
02:.next()

2012年7月5日 星期四

jQuery 的 :checked 在 Opera 不正常



在 jQuery 裡,如果想要知道 checkbox 有幾個被選取,可以使用

$("input:checked").size()

但我發現這方法,適用於 IE、Firefox、Chrome ,卻不適用 Opera 瀏覽器。我試著用一段測試程式如下:


<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
    <script>
        function DoCheck() {
            alert($("input:checked").size());            
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <input type="button" id="btn" value="check" onclick="DoCheck();" />
    <div>
    <input id="cb1" type="checkbox" name="cb"  />111 <br />
    <input id="cb2" type="checkbox" name="cb"  />222 <br />
    <input id="cb3" type="checkbox" name="cb"  />333 <br />
    </div>
    </form>
</body>
</html>

在 Opera 測試時,任意更改 checkbox 的選取組合,會發現 alert($("input:checked").size()); 所得到的結果,竟然會出錯,一開始還以為自己眼花,但沒想到是真的。在 jQuery 的 BUG TRACKER,可以看到目前已經有人提出同樣的問題了。

慶幸的,是目前還有一個各個瀏覽器都還支援的寫法:

$("input").filter(":checked").size()

所以,在 jQuery 尚未提出修正之前,還必須先以 .filter() 的方式來取代。

Opera 版本:11.62
jQuery 版本:1.7.2