2012年6月14日 星期四

CTE Recursive

於 MSDN 「Recursive Queries Using Common Table Expressions」所說,遞迴的 CTE 必須包含兩個部份,一個是固定部份,另一個則是遞迴部份。而整個遞迴 CTE 的架構長的如下所示:


WITH cte_name ( column_name [,...n] )
AS
(
CTE_query_definition –- 固定部份的定義.
UNION ALL
CTE_query_definition –- 遞迴部份的定義,且參考了 cte_name
)
-- 使用 CTE 的宣告
SELECT * FROM cte_name
舉一個範例來說明,假設某家公司從 BOSS 開始,下面分了 AP 與 CT 兩個事業處,而每個事業處底下又分了好幾個部門。
create table #tmp(CompanyID int not null primary key,
ParentCompanyID int null,
CompanyName nvarchar(50) )

insert into #tmp (CompanyID,ParentCompanyID,CompanyName)
values
(1,NULL,'BOSS')

insert into #tmp (CompanyID,ParentCompanyID,CompanyName)
values
(2,1,'AP')

insert into #tmp (CompanyID,ParentCompanyID,CompanyName)
values
(3,1,'CT')

insert into #tmp (CompanyID,ParentCompanyID,CompanyName)
values
(4,2,'E100')

insert into #tmp (CompanyID,ParentCompanyID,CompanyName)
values
(5,2,'E200')

insert into #tmp (CompanyID,ParentCompanyID,CompanyName)
values
(6,3,'K100')

insert into #tmp (CompanyID,ParentCompanyID,CompanyName)
values
(7,3,'K200')

insert into #tmp (CompanyID,ParentCompanyID,CompanyName)
values
(8,3,'K300')

select * from #tmp

有了以上的基本資料後,接下來透過 CTE 的遞迴語法來獲得公司的樹狀結果。

;with CompanyTree(ParentCompanyID,CompanyID,CompanyName,CompanyLevel)
as
(
 select ParentCompanyID,CompanyID,CompanyName,0 as CompanyLevel from #tmp 
 where ParentCompanyID is null
 union all
 select c.ParentCompanyID,c.CompanyID,c.CompanyName,p.CompanyLevel+1 from #tmp c
 inner join CompanyTree p on c.ParentCompanyID=p.CompanyID
)
select * from CompanyTree OPTION (Maxrecursion 20)

drop table #tmp

上面 CTE 的語法中,固定部份是:
select ParentCompanyID,CompanyID,CompanyName,0 as CompanyLevel from #tmp
where ParentCompanyID is null

這語法很明顯是把父子關係裡我們所謂的根節點 Root 找出來,所以他的查詢條件是找父親索引 (ParentCompanyID) 為 NULL 的資料。自訂欄位 CompanyLevel 則是用來顯示樹狀結構的深度,由於在固定部份所取得的都是 Root 資料,所以樹狀深度都是 0,所得到的結果用 T0 來表示 .

接下來,則是透過 UNION ALL 將遞迴部份的資料加進來。

select c.ParentCompanyID,c.CompanyID,c.CompanyName,p.CompanyLevel+1 from #tmp c
inner join CompanyTree p on c.ParentCompanyID=p.CompanyID


我們用 T1 來表示第 1 次迴圈的結果集合。由目前的範例,我們會獲得

主要是因為在固定部份 T0 我們一開始得到的是



所以透過 c.ParentCompanyID=p.CompanyID
(或視為 #tmp.ParentCompanyID = CompanyTree.CompanyID)
的關聯條件就可以得到 AP 與 CT 兩筆資料。這一次的關聯條件,翻譯成白話,就好像是問說:原始資料裡面,有誰的爸爸叫做 1 的阿?而翻譯成SQL 條件,就是:

#tmp.ParentCompanyID=1


,於是就會跑出 AP 與 CT 這兩個結果。

接著進行第 2 次迴圈後,則 T2 所得到的結果,分別是 :

AP 的
與 BP的

這兩部份的加總,也就是 E100、E200、K100、K200。


而遞迴的特性,會一直持續的往下探尋,直到找不到資料為止,也就是從 T1 … Tn
目前的範例,則是將  T1,T2 這 2 層的資料回傳。
最後的樹狀結果加總 T0 … Tn 所有資料 ,如下所示:


或許一開始會有個疑問,為什麼一定要用 UNION ALL ,為什麼一定要用 inner join,但當你試著用其他選擇,例如改成 UNION 後,會得到以下錯誤訊息:

Recursive common table expression 'CompanyTree' does not contain
a top-level UNION ALL operator.


改成 left join 後會得到

Outer join is not allowed in the recursive part of a
recursive common table expression 'CompanyTree'.


就可以發現,其他的選擇,會造成 CTE 遞迴的語法錯誤,沒得商量。

此外,CTE 其實還有一個比較特別的參數設定可以使用。由於其強大的遞迴功能,為了避免使用者因為邏輯上的錯誤而造成了無限迴圈,他提供使用者自行定義要往下探尋幾層的限制。假設你已經知道自己的樹狀結構不會超過 10 層,那你可以在程式裡面加上  OPTION (Maxrecursion 10),當迴圈超過10層時,程式就會出現錯誤訊息而終止,不會跑無限迴圈。
超過10 層的錯誤訊息:

The statement terminated. The maximum recursion 10 has been exhausted
before statement completion.


完整程式如下:
;with CompanyTree(ParentCompanyID,CompanyID,CompanyName,CompanyLevel)
as
(
 select ParentCompanyID,CompanyID,CompanyName,0 as CompanyLevel from #tmp 
 where ParentCompanyID is null
 union all
 select c.ParentCompanyID,c.CompanyID,c.CompanyName,p.CompanyLevel+1 from #tmp c
 inner join CompanyTree p on c.ParentCompanyID=p.CompanyID
)
select * from CompanyTree OPTION (Maxrecursion 10)

這個 Maxrecursion 或許你不會想去設定,但必須要注意,即便你沒設定,但它的預設值卻幫你設定好了,預設是 100 ,也就是說,當你的探尋深度有可能超過100 時,記得要將這個參數調整為設合您的大小。他的設定範圍為 0 ~ 32767,0 則表示沒有限制,也就是說可以超過 32767,但請注意,當你的迴圈深度達到這麼深時,整個的執行效能是非常差的。


Ref:
01:Recursive Queries Using Common Table Expressions

02:一般資料表運算式(Common Table Expressions, CTE)

2012年6月11日 星期一

關於 SQL 的 Common Table Expression


Common Table Expression (CTE) 是 SQL Server 2005 開始加入的新成員,千萬別跟 ETC 搞混囉(雖然把名字倒過來唸還真的一模一樣)!!

會對 CTE 開始有興趣,是因為以前在撰寫比較複雜且多層的子查詢時,自己當下是很清楚要寫什麼,可是經過一段時間後,自己已經有點不懂當時的想法,更不用說其他接手維護的工程師了。舉例來說,以前寫的好幾層子查詢,可能長得如下:

select A1 from A where A2 in
 (
        select B1 from B where B2 in
                    ( select C1 from C where C2 like '%test%')
 )

透過 CTE,可以讓我們不用這麼辛苦,可以暫時先把每個子查詢的結果存放在一個記憶體變數中,而結果會如下:
;with tC as
      (select C1 from C where C2 like '%test%')
,tB as
      ( select B from B where B2 in (select * from tC))
select A1 from A where A2 in (select * from tB)

在 Insert 的用法:

create table #tmp (Country nvarchar(200),OkNum int)

;with T as
(select Country,OkNum from AsiaData )
insert into #tmp select Country,OkNum from T

select * from #tmp
drop table #tmp
在 Group 的用法:
;with T as
(
   select Country,OkNum from AsiaData union select Country,OkNum from EuropeData
)
select Country,sum(isnull(OkNum,0)) OkNum from T
group by Country
個人覺得,使用 CTE,程式碼不見得會寫得比較少,但至少對閱讀上來說,的確是比較容易理解。在使用上,除了一般所撰寫的 T-SQL 查詢之外,還可將 CTE 用於使用者自訂的函數( Function)、預存程序( Stored Procedure)、檢視( View )、觸發程序( Triggers )。

CTE 語法的基本架構如下(Ref 01:Using Common Table Expressions):
--定義 CTE
;WITH CTE_運算式名稱 [ ( column_name [,...n] ) ]
AS
( CTE_查詢_定義 )

--使用 CTE
SELECT <column_list> FROM CTE_運算式名稱;

在定義 CTE 時,如果沒有指定欄位名稱,則結果的欄位名稱預設會跟 CTE_查詢_定義 裡的結果一樣;如果有指定欄位名稱,則欄位名稱的值會跟 CTE_查詢_定義 裡出現的結果順序一樣,且 CTE 的欄位名稱是不可重複的。舉例來說:
;with T (T1,T2) as
     (select C2,C1 from C )

這裡要注意,CTE 的結果裡,T1 欄位所呈現的值會是 資料表 C 的 C2 欄位,原因是 CTE 的結果欄位是依據 CTE_查詢_定義 的順序來決定。(Ref 02:SQL with)

使用 CTE ,有個比較特別的現象,他並不像 View 或 暫存資料表一樣可以一直被引用,實際上,他只能被引用一次。也就是說,當你定義完 CTE 的結構後,你必須在後面接著馬上使用他,使用完了之後,他就不見了。
;with EuropeData (Country,OkNum) as
(
   select top 5 Country,OkNum from AsiaData

)
-- 第一次查詢
select top 5 Country,OkNum from EuropeData
-- 第二次查詢
select top 5 Country,OkNum from EuropeData

在上面的範例中,一開始我定義了 CTE 的運算名稱為 EuropeData ,其實這名稱也剛好是我資料庫裡的某一個資料表,只是我刻意把 CTE 的名稱取的跟我既有的資料表名稱一致,只是 CTE 的結果,其實是從 AsiaData 而來。當定義完我的 CTE 之後,連續 2 次執行查詢,則可以在結果中發現,第一次的查詢,資料是從 AsiaData 而來。但第二次的查詢,因為自訂 CTE 的記憶體空間已被回收,所以會抓到原先資料庫已經存在的 EuropeData 資料表。

CTE 除了上述的介紹之外,他還有另一個蠻有趣的主題:遞迴,這功能可以讓我們在處理行父子關係的資料表、組織圖,更有效率,於後面再詳述。

Ref:

01.Using Common Table Expressions
02.SQL with
03.SQL中使用WITH AS提高性能-使用公用表表达式(CTE)简化嵌套SQL
04.一般資料表運算式(Common Table Expressions, CTE)
05.T-SQL -- COMMON TABLE EXPRESSION (CTE) 教學重點筆記
06.SQL 2005 T-SQL Enhancement: Common Table Expression
07.Performance Effect of Common Table Expressions in SQL Server 2005

2012年5月31日 星期四

ASP.Net 使用 Form 認證設定使用者登入資訊

在 ASP.Net 專案,如果使用的認証方式為 Form 認證,當確認了登入者身份後, 會將使用者是否已經經過認證的訊息寫到 Cookie裡,而 Cookie 名稱為 ASPXAUTH。透過以下方法,可以看到 Cookie 的值。

Request.Cookies[“ASPXAUTH”].Value

或是用下面這段迴圈去抓出目前的 Cookie 資訊:
StringBuilder sb = new StringBuilder();
        
for (int i = 0; i < Request.Cookies.Count; i++)
{
     sb.Append(string.Format("<br>Name:{0} Values:{1}", Request.Cookies[i].Name,Request.Cookies[i].Value));
}

lb_Cokies_info.Text=sb.ToString();

當然,你所看到的 Cookie 值是經過編碼加密後的一串文字。
剛剛提到,確認並通過了使用者身份後,會將資訊寫到 Cookie 裡,那這寫入的動作,是誰做?不用想太多,當然是程式設計師自己去做啦~

介紹目前自己常常看到的三種寫入認證 Cookie 方式:

一、RedirectFromLoginPage()
舉例來說,假設使用者 paladin 填寫完帳號密碼,並檢查無誤後,就可以用下面語法將 “paladin” 這帳號訊息寫到 ASPXAUTH Cookie裡。所以RedirectFromLoginPage 方法裡的第一個參數,是用來存放被認可的使用者帳號資訊。

第二個參數還蠻值得玩味的,官方說法是表示建立持久性 Cookie (跨瀏覽器工作階段儲存的 Cookie) (Ref.http://msdn.microsoft.com/zh-tw/library/bk50ykcd.aspx) )。經實際測試後,所謂的跨瀏覽器並不是跨 IE、Chrome、Firefox,而是指是否允許在另開 IE 瀏覽器時使用同一份 Cookie,所以IE 裡的 ASPXAUTH Cookie 與 Chrome 或 Firefox 都不一樣,他們都有各自的 Cookie。但如果你是使用開啟新索引標籤的方式,則不管值為何,都是使用同一份 Cookie,每一個標籤都抓的到。

此外,當第二個參數設為 true 時,預設 Cookie 的存活時間約 30 分鐘,也就是說,當你把所有瀏覽器都關閉時,在30分鐘內都還可以存取到有效的 Cookie 值。但如果參數設為 false,則當視窗都關閉後,Cookie也就會自動失效。
using System.Web.Security;

FormsAuthentication.RedirectFromLoginPage(“paladin”, false);

此外,使用RedirectFromLoginPage()方法時,顧名思義當他執行完後,應該會把頁面轉到(Redirect)其他頁面去吧,實際上也真是如此。當執行完後,他會先檢查目前的 URL 裡是否有設定 ReturnUrl,如果有,就會將頁面轉到  ReturnUrl 所定義的頁面。舉例來說,如果你的驗證登入的頁面是:



http://paladin.love.girl.tw/login.aspx?ReturnURL=hello_baby.aspx

當執行 RedirectFromLoginPage() 方法後,你的頁面就會轉到

http://paladin.love.girl.tw/hello_baby.aspx



但如果你忘了或不想在 URL 上面指定 ReturnURL 參數,那也沒關係,他會根據目前 FormsAuthentication.DefaultUrl 所定義的值來決定歸處,但預設值是 default.aspx。這裡需要注意的是,FormsAuthentication.DefaultUrl 只能取得他的值,卻不可直接設定他,如果真要改變的話,需在 web.config 裡的 defaultUrl 調整(Ref.FormsAuthentication.DefaultUrl 屬性)。


<authentication mode="Forms">
  <forms loginUrl="member_login.aspx"
    defaultUrl="index.aspx" />
</authentication>

二、SetAuthCookie()
如果對於 ReturnURL 還耿耿於懷,或是希望自己能夠完全主導認證後頁面轉向的控制權,則可以使用 SetAuthCookie() 方法來完成。他的使用方式跟前面的 RedirectFromLoginPage() 非常相似,舉例來說:


FormsAuthentication.SetAuthCookie(“paladin”, false);

第一個參數是將登入者的帳號寫到 ASPXAUTH Cookie 裡,第二個參數也跟RedirectFromLoginPage() 一樣,用來表示是否建立持久性 Cookie。最大的不同,在於SetAuthCookie() 執行完後,不會將你目前的頁面帶離,而是留給程式設計師自行決定。

三、FormsAuthenticationTicket
前面兩種作法,都算是輕巧簡便型,他們把很多複雜的設定都封裝起來,所以用起來很方便,但從另一個角度來說,也就失去許多原本可以設定的選項,變得比較沒有彈性。如果希望自己撰寫個比較有彈性的寫法,可以利用 FormsAuthenticationTicket 類別來實作。以一段程式來說明:

      bool isPersistent = false;

        FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
            1,
            "paladin",
            DateTime.Now,
            DateTime.Now.AddMinutes(30),
            isPersistent,
            "Admin",
            FormsAuthentication.FormsCookiePath
            );

        // Encrypt the ticket.
        string encTicket = FormsAuthentication.Encrypt(ticket);


        HttpCookie authenticationCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);

        //將HttpCookie是否永久存在的屬性與FormsAuthenticationTicket綁在一起
        if (isPersistent)
            authenticationCookie.Expires = ticket.Expiration;

        // Create the cookie.
        Response.Cookies.Add(authenticationCookie);


首先建立了一個 FormsAuthenticationTicket 類別,建構式裡有7個參數可以設定。
1.設定 ticket 的版本
2.設定要存放的使用者帳號
3.設定 ticket 產生的時間
4.設定 ticket 過期時間
5.設定這個 ticket 是否是持續的
6.允許使用者任意輸入的文字(但不是無限制長度,Cookie 能保存的文字長度有限制)
7.Cookie 存放路徑

這裡的參數設定,主要針對第六個參數要特別注意。通常我們確認了使用者身份,且將使用者的帳號記錄下來後,還有可能會進一步去將他的群組資料也放進去。例如使用者是「Admin」還是「Guest」,或是同時具備多種群組身份。第六個參數允許你存放的是字串格式,可以拿來存放群組資訊。如果準備存放多種群組角色時,一般都可以使用 | 或其他分隔字元來區別,例如:Admin|Guest 。但當你在使用時,需特別注意,因為 Cookie 的長度並非無限制,如果將太多的資訊(例如:分機、住址...)都全部往裡面寫,那就有可能會造成錯誤,所以盡可能寫比較重要且精簡的內容,過多的相關資訊,可以考慮存放索引值就好,需要用到時再去資料庫反查。


最後,我們可以透過以下方式取得先前所存放的使用者資訊:





//取得使用者帳號
        if (User.Identity.IsAuthenticated)
        {
            lbUserInfo.Text = string.Format("目前登入者帳號為:{0}", User.Identity.Name);
        }
        else
        {
            lbUserInfo.Text = "目前尚未登入";
        }

//取得使用者自訂資料
string cookieName = FormsAuthentication.FormsCookieName;
HttpCookie authCookie = Context.Request.Cookies[cookieName];
if (authCookie == null) return;
FormsAuthenticationTicket authTicket =FormsAuthentication.Decrypt(authCookie.Value);
Response.Write(authTicket.UserData);



以上一切的一切,都是假設您是使用 Form 認證才會成立的,所以在 web.config 中,必須確認有以下的設定喔。
<authentication mode="Forms">

參考:
01.概略解釋 Forms Authentication 的運作

2012年5月27日 星期日

一念之間的差異

盧智芳於 Cheers 提到,她在國三畢業典禮前夕,校長到每個班級對畢業生講話,他一開始什麼都沒說,只在黑板上畫了一個簡單的圖,然後說:在人生中,畢業典禮就像這個角的頂點,一開始,每個人的出發點都一樣;但是隨著時間的推移,有一個因素會讓彼此的差異愈來愈大,愈來愈大。這個因素是什麼?就是一個人的觀念。要是觀念一開始是錯的,以後就全錯了。

記得禪聞法師也曾提到:

不對的方法,做再多,都是錯,浪費時間。
對的方法,慢慢做,才會進步,不見得時間會花比較多。

所以,一開始能夠找到正確的觀念,正確的方法,是決定你圓滿的關鍵。這裡強調的是圓滿,而不是成功。因為成功有可能只是表象,例如你成功獲得了選舉,卻使用了見不得人的手段,雖說你成功了,但卻不圓滿。

侯吉諒於其「一念之間」文章中有提過:

「人很容易受到環境的影響,因此需要一定程度的內在修養,才能不受外來惡戾之氣的影響,能知不足,則不敢不努力,能以平常心看得破一時際遇的興衰,則世間榮辱可以不驚不畏,能無所住而生其心,則不會有偏見,能以善待人,於是物物大好,事事好極了,不必求福而自然萬得福。」


雖說是外來惡戾之氣的影響,但說到底,還是自己內心調伏的問題。會左右我們選擇的因素,常常是因為「我無甲意輸的感覺!(台語)」。因為害怕,所以採取了許多明知不可為而為的作法,最後反而輸的更慘,付出更大的代價。


在強調運動精神的公平競賽中,競爭結果的輸贏並不是壞事。它訂出公平的標準,讓參賽者分出高下。贏的人很清楚知道哪一方面的表現,適合自己發揮長才;輸的人也可以藉此檢討自己努力不夠、或是方法有問題,還是根本就是把自己放錯了位置,不該參加這個賽局。

如果不能接受自己落在輸局的處境,進而透過檢討反省找到新的方向,就會讓自己執著在「輸」的負面感覺裡,變得憤怒而失去理性。

在該篇文章中,有幾個很棒的觀點,節錄於後:

  1. 願意成全別人,是更實際的贏家
    分享一個精彩的故事:
    左宗棠,並非天生贏家。他也有過輸的紀錄。據說,非常擅長下圍棋的他,曾經在一次出兵前的空檔,路過一個號稱「天下第一」的棋王住處,於是他主動下馬挑戰,當場贏了三盤棋,爽快地離開趕赴戰場之前,還特別糗了一下主人「天下第一」的封號。

    為朝廷打完勝仗,班師回朝途中,左宗棠又再度路過棋王住處,意猶未盡地進去下棋,想要再度衛冕成功,沒想到這一回卻連輸三盤。棋王微笑地說:「上一次你來找我下棋的時候,正好是要率兵出征,我不能讓你因為輸棋而挫失銳氣。現在你打勝仗回來,我就當仁不讓了。」

    棋盤上的輸贏,也許有真有假。人生裡的輸贏,也未必就此見真章。但只要擁有足夠的自信,能夠體察別人的需要,願意成全別人,即使表面上輸了,並不是真的輸,反而可能是更實際的贏家。棋王,贏了友誼,也贏了自己。
  2. 輸贏不是跟別人的比較,而是對自我的挑戰
    戰勝自己一直想「贏過別人」的念頭,是層次更高的一種贏法。
  3. 輸贏不是單線的,而是多面的
    學業或事業很成功的人,可能忽略了人際關係或家庭幸福。評估輸贏時,別忘了各個面向的均衡。
  4. 輸贏不是表面的,而是一體的
    輸贏是一體兩面的,贏到某些東西的時候,可能正失去其他的東西。


參考:
01: Cheers 130期,一念之間的差異
02:侯吉諒:一念之間
03:面對自我挑戰-輸與贏在一念之間(每週一讀)

2012年5月22日 星期二

.Net 1.1 的有痛升級

於 .net 1.1 ,專案名稱 UpTestWeb ,預設在 /bin 資料夾就會產生 UpTestWeb.dll。當要升級為 .net 2.0 以上時,一般都習慣使用升級精靈來自動將專案轉換為新的 .net 版本。目前我則是將 .net 1.1 升級為 .net 4.0,升級完成後,一開始執行都沒甚麼大問題,但當有需要修改程式時,才發現原先自己定義的一些類別,不管怎麼改,程式都不會去執行剛剛修改的地方,還是照著他自己的舊寫法去跑,真的還蠻奇怪的。

理論上,於 .net 4.0 時,/bin 資料夾並不會出現跟專案名稱一樣的 dll 檔,但檢視我升級後的專案,裡頭卻有一個 UpTestWeb.dll ,於是我把他砍了,接著一堆錯誤就出來了,眼睛很花,頭...很沈重。

在網路上看到許多前輩討論 .net 1.1 升級的經驗談,其中於 shang 的一篇文章「 从ASP.NET 1.1升级到2.0 」提到:「将所有独立的代码文件和AssemblyInfo.cs都被移到 App_Code 目录下」,發現自己少了這個步驟。原來,如果將與 .aspx 無關的 .cs 都移到 /App_Code ,於 .net 2.0 以後才可以讓其他程式碼存取。基本上,我們自己所定義的類別程式,都應該放在 /App_Code,若是已經編譯過的 dll 檔,則是擺放在 /Bin 下面,如此其他程式才能參考的到(Ref.ASP.NET 網站中的共用程式碼資料夾)。

在這次升級的過程中,我大致做了以下動作:

01:將與 .aspx 無關的 .cs 都移到 /App_Code
02:把 .aspx 裡的Codebehind 改成 CodeFile,同時也要調整 Inherits。舉例來說:
<%@ Page language="c#" Codebehind="Default.aspx.cs" AutoEventWireup="false" Inherits="UpTestWeb._Default" %>

換成

<%@ Page language="c#" CodeFile="Default.aspx.cs" AutoEventWireup="false" Inherits="_Default" %>

03:把 .aspx 的
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >

換成

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

04:將 .aspx.cs 裡的 namespace XXX { ... } 拿掉, 並將
public class xxx : System.Web.UI.Page 改成
public partial class xxx : System.Web.UI.Page

05:於 .aspx.cs 裡,類似 protected System.Web.UI.WebControls.Label lblUserName; 這種的宣告都砍掉。

參考:


01:ASP.NET 網站中的共用程式碼資料夾

02:A Beginner's Guide to ASP.NET Application Folders

03:从ASP.NET 1.1升级到2.0

04:KB-ASP.NET 2.0 網站部署的變革

2012年4月20日 星期五

hyperlink 可以 disabled 嗎?

記得以前在網頁上,如果想要把一個按鈕變成不可點擊,只需要將該按鈕的屬性設為  disabled  就可以了。只是現在我是有很多個超連結(hyperlink),也希望透過設定 disabled 就能把該超連結變成無效且不可點擊。

於是我試著寫出以下的測試程式:

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>    
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <a href="#" onclick="alert('A1');" >A1</a> <br />
    <a href="#" onclick="alert('A2');" >A2</a> <br />   
    <a href="#" onclick="alert('A3');" disabled="disabled" >A3</a>
    </div>

    <div>
    <input type="button" value="B1" onclick="alert('B1');" /> <br />
    <input type="button" value="B2" onclick="alert('B2');" /> <br />
    <input type="button" value="B3" onclick="alert('B3');" disabled="disabled" />
   
    </div>
    </form>
</body>
</html>

這似乎沒甚麼難的,只是... 當我開啟 firefox、chrome 等瀏覽器時,原先被設為 disabled 的超連結竟然還可以被點選,且觸發 onclick 事件。這種現象在 Button 身上並不會因瀏覽器不同而有所差異,目前只有超連結才會。

為了避免這不一致的運作行為,我參考了 Disable anchors in Chrome/WebKit/Safari 的作法,去判斷每一個超連結,如果屬性有設 disabled,就避免觸發 click 事件。

所以加上一段 jQuery Code 去處理所有 disabled 的超連結後,就可以適用於各瀏覽器了。


<script>
    $(function () {
        //避免 disabled 的 hyperlink 被觸發, chrome,firefox 需要這段處理
        $("a").click(function () {
            if ($(this).attr("disabled") == "disabled") {

                event.preventDefault();
            }

        });
    });
</script>
 
參考:
01: Disable anchors in Chrome/WebKit/Safari
02: jQuery .attr(“disabled”, “disabled”) not working in Chrome
03:.prop() vs .attr()

2012年3月13日 星期二

雖然你已經沒了,但我還是會難過

走在大賣場的餅乾區,隨手一拿,幾乎大部分的零食所標示的反式脂肪酸都寫 0,就連泡麵也都以顯著的標語寫著:『本產品絕不含防腐劑』,人家都已經做得這麼有誠意,只差沒跪在你面前發誓,難道你不給台灣廠商一個機會嗎?

我還特別挑了一盒孔雀餅乾。它不僅反式脂肪酸是 0 ,而且還是台灣製造,重點是它還增重 45 公克,就這樣,它上了我的推車,入了我家冰箱,也進了我的肚子。



在偶然的機會裡,說真的,機會不高。我拿起塵封已久的康健雜誌,翻到第 158 期的一篇:『零反式脂肪酸,真的嗎?』它裡面有提到:

自政府於 2008 年規定,如果產品原料未使用氫化加工工序,且含量在 0.3 克/ 100 克以下,反式脂肪酸一欄即可標示為「」。

0  +  0  +  0  ≠   0


這數學式還真的走入了你我的日常生活當中,你會對這完美的數據感到高興,還是更顯不安呢?往好處想,世界衛生組織建議每人每天的攝取量以不超過 2 公克為限,0 與 2 之間,似乎還有著一些些的距離...

但是切記,別買「樂天小熊餅乾」給你們家寶貝吃,因為在康健雜誌於 2011年11月8日 ~11月11日 所抽查的報告中,它的反式脂肪酸是 4.09 。



參考:
01:康健雜誌 158 期
02:零反式脂肪,真的嗎?
03:小心隱形的油脂殺手-反式脂肪!