November 2009 - 文章

光華很危險滴,我還是快點回火星吧

摳門節儉向來是個人最自豪的優點之一。

商店廠商業務小販推銷員想騙我掏出口袋裡的錢,倒不如去金瓜石重開廢坑道檢找找看有沒有漏採的黃金屑來得實際些。

很不幸地,最近的我趕案子忙到想吐。而依最新一期黑暗科學雜誌的說法,有科學研究指出,當人處於精神壓力下,會喪失原有的冷靜沈著、理性與判斷力也會受到影響。於是,事情就這麼發生了...

前幾天,親戚店裡營業用的PC無法開機,雖然是忙不過來之際接到求救電話,但基於有人己快被條碼作業還原成的紙筆作業逼上絕路,只好抱著救人一命勝造七級浮屠的佛心先衝去救援再回公司加班。在光華商場等待工程師換Power的當下,我忽然感到一陣意識模糊(但我確定當時沒人拍我肩膀)... 等走出店家時,居然發現手上除了抱著送修的電腦,還多拎了一顆WD 640GB AAKS跟一台BENQ SATA DVD燒錄器。不會吧? 家裡那顆Seagate 320G HD只不過開機十次有六次會抓不到硬碟,明明開機殼拔插幾次換個位置就可以解決;幾台PC是有一台的DVD ROM壞了,但又不是沒有就開不了機,大不了真的要用時再從另一台拆過去就好了呀... 我怎麼會買了這些多餘的東西? 不過,我猜店家不會因為精神耗弱而退我錢,只好自認倒楣乖乖帶回去用吧,但明明是幫人修電腦怎麼會是我破財這事實在太詭異了一點,光華商場真是危險的地方!

昨天帶小朋友回娘家看阿公阿媽,中間有段空檔,因為最近天雨少有運動,就想走路去光華健身,順便蒐集一下報價單,考量是不是要把公司那台己經冒出藍、紫、紅、綠四條垂直線的老爺LCD換掉? (誠心發問: 如果我再獲得黃、橙、靛三色垂直線,可否能用”轉吧!七彩霓虹燈"隱藏版LCD的名義將它高價網拍掉?)

新光華好大一間,這回才算第一次全部走過,從1F、2F、3F,每一層都巡一圈逐家看看兼拿報價單,一路晃到了6F。出來時可怕的事又發生了!! 手上除了厚厚的報價單,居然又多了BP 511 Canon 10D電池跟充電器,還有一台陽春三用電表! 這這這...

好吧! 這回我承認有意識啦。家裡的原廠充電器每回插電會傳出微小的高頻噪音,空電池放上去會在幾秒內狀態燈就轉成充滿狀態,要卸放數次才能正常充電。不知是否因為充電器有問題,近兩年買過幾次電池都在一年內夭折。有機會到電池專賣店,索性就買一個新充電器另外加購一顆電池輪替兼備援。

三用電表則是有好幾次臨時要修電器,想檢查線路斷路、電路過電、電池狀態都缺乏工具驗證。在光華高樓層的電子儀表店看到老牌子YFE也出了陽春款,店家開價不到200元,完全不知行情的我躺在地上耍賴使出不賣拉倒絕技還價,可能4F以上人潮冷清生意難做,老板雖然滿臉糾結表情痛苦,居然還真的讓我殺價十元成交,只是結帳時不斷碎唸,這東西外面都賣兩百多的blah blah… 回家一查,還真的哩,賺到賺到~~~

俗話說,敗家血拼足以撫慰人心! 黑暗大叔現身說法,傑佛瑞,這真的太神奇了! 看到小朋友們在偌大的電子商場裡快樂奔馳,確實可以讓人暫時忘卻世俗庸擾,將煩惱抛到九霄雲外~~~ 大家不妨體驗一下!

CODE-具有Default Namespace XML文件的查詢

我恨透了XML的Namespace, 尤其是xmlns這種預設Namespace。

每次寫Code處理這段都要東查西抄才能搞定,最慘的是連怎麼弄出來自己也模模糊糊。再不然就是走偏門,抓到OuterXML Replace掉xmlns="..."後重新LoadXml,當作沒有Namespace這回事(好像很多人也是這樣搞)。當然,這種態度是不對的,叔叔太頹廢了,小朋友們千萬不可以學~~

今日有幸承蒙chicken大師點化,終於學會怎麼正確處理Default Namespace,特貼文分享。

public static void TestNamesapce()
{
    XmlDocument xd = new XmlDocument();
    xd.LoadXml(@"
<root xmlns=""http://blog.darkthread.net"">
<item></item><item></item>
</root>");
    //太天真了,這樣是查不到的,0分
    Console.WriteLine(xd.SelectNodes("//item").Count);
    //對了,要指定Namespace
    XmlNamespaceManager nsm = 
        new XmlNamespaceManager(xd.NameTable);
    //加入預設的Namesapce,那就是空字串嗎?
    nsm.AddNamespace("", "http://blog.darkthread.net");
    //SelectNodes查詢時要傳入XmlNamespaceManager
    //代誌不像憨人想得那麼簡單... 還是零分
    Console.WriteLine(xd.SelectNodes("//item", nsm).Count);
    //請教chicken大師, 要自己加一個Alias
    nsm.AddNamespace("wtf", "http://blog.darkthread.net");
    //XPATH也要加上Namespace Prefix
    Console.WriteLine(xd.SelectNodes("//wtf:item", nsm).Count);
    //終於得到想到的結果... 感謝大師開示
}
CODE-用jQuery實作<select>選項上下移動(複選版)

91哥在留言裡出了個jQuery考題:

清單的項目有1,2,3,4,5,單選上下都沒問題了。

多選OK的情況:

(A) 選了2,3,按上,清單會變成2,3,1,4,5。
(B) 選了1,3,按上,清單會變成1,3,2,4,5。
(C) 選了2,5,按上,清單會變成2,1,3,5,4。

小的現在碰到的情況是:
選了1,2,按上,清單會變成2,1,3,4,5。原因是因為 each會從依序從最前面開始判斷,當1不做事時,下一個2則會跟1換位置,需求應該是「選了1,2,按上,清單仍維持1,2,3,4,5」。

不曉得針對這樣的需求,黑大有沒建議的作法?

好一個難易適中的挑戰題,忍不住手癢,就試解如下: (程式行數較單選版稍有增加,但仍維持jQuery慣有的簡潔性。)

<html>
<head>
<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js'></script>
<script type='text/javascript'>
    $(function() {
    
        //宣告jQuery陣列逆轉函數, 這是網路上找到的寫法
        //借了Array.reverse的native function來用(哇哩咧,這樣也行)
        $.fn.reverse = [].reverse;
        
        $("#bUp,#bDown").click(function() {
            var $opt = $("#selList option:selected");
            if (!$opt.length) return;
            var bGoUp = (this.id == "bUp");
            //向下移時, 逆轉結果陣列,由下往上做
            if (!bGoUp) $opt.reverse();
            $opt.each(function(i) {
                var $src = $(this);
                var $dst = $src[bGoUp ? "prev" : "next"]();
                //移動對象若也被選取,則不動
    if ($dst.length && $dst[0].selected) return;
                $dst[bGoUp ? "before" : "after"]($src);
            });
        });
    });
</script>
</head><body>
<select id='selList' size='7' style='width: 100px' multiple>
<option>Item 1</option>
<option>Item 2</option>
<option>Item 3</option>
<option>Item 4</option>
<option>Item 5</option>
</select><br />
<input type="button" id="bUp" value="Up" />
<input type="button" id="bDown" value="Down" />
</body>
</html>
CODE-C#變身術懶人包

以前老覺得,用C#寫身份切換(用不同身份存取資源)程式得借重API,有一堆細節要處理,麻煩得很。

這回發了狠,一口氣把複雜工作通通裝在一個Class裡,呼叫時只要傳入帳號、密碼和網域,取回WindowsImpersonationContext,一切搞定。

就叫它【C#變身術懶人包】吧!

//REF: http://support.microsoft.com/kb/319615
public class ImpersonateHelper
{
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool LogonUser(string lpszUsername, 
        string lpszDomain, string lpszPassword, int dwLogonType, 
        int dwLogonProvider, ref IntPtr phToken);
 
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, 
        SetLastError = true)]
    private static extern bool CloseHandle(IntPtr handle);
 
    [DllImport("advapi32.dll", CharSet = CharSet.Auto, 
        SetLastError = true)]
    public extern static bool DuplicateToken(IntPtr existingTokenHandle, 
        int SECURITY_IMPERSONATION_LEVEL, ref IntPtr duplicateTokenHandle);
 
    // logon types
    const int LOGON32_LOGON_INTERACTIVE = 2;
    const int LOGON32_LOGON_NETWORK = 3;
    const int LOGON32_LOGON_NEW_CREDENTIALS = 9;
 
    // logon providers
    const int LOGON32_PROVIDER_DEFAULT = 0;
    const int LOGON32_PROVIDER_WINNT50 = 3;
    const int LOGON32_PROVIDER_WINNT40 = 2;
    const int LOGON32_PROVIDER_WINNT35 = 1;
 
    public static IntPtr GetDupToken(string userName, string password, 
        string domain)
    {
        IntPtr token = IntPtr.Zero;
        IntPtr dupToken = IntPtr.Zero;
        bool isSuccess = LogonUser(
                            userName,
                            domain,
                            password,
                            LOGON32_LOGON_NEW_CREDENTIALS,
                            LOGON32_PROVIDER_DEFAULT,
                            ref token);
        if (!isSuccess)
            throw new ApplicationException(
                "Failed to LogonUser, Code = " + 
                Marshal.GetLastWin32Error());
        isSuccess = DuplicateToken(token, 2, ref dupToken);
        if (!isSuccess)
            throw new ApplicationException(
                "Failed to DuplicateToken, Code = " + 
                Marshal.GetLastWin32Error());
        return dupToken;
    }
 
    public static WindowsImpersonationContext 
        GetImpersonationContext(
        string userName, string password, string domainName)
    {
        return new WindowsIdentity(
    ImpersonateHelper.GetDupToken(userName, password, domainName)
            ).Impersonate();
    }
 
}
static void test()
{
    Dictionary<string, string> latestList = new Dictionary<string, string>();
    using (WindowsImpersonationContext wic =
        ImpersonateHelper.GetImpersonationContext("username", "pwd",
        "domainOrRemoteServerIp"))
    {
        foreach (string f in
            Directory.GetFiles(@"\\remoteServer\Folder", "*.txt"))
            Console.WriteLine(f);
        wic.Undo();
    }
}
2009第五屆華文部落格大獎初選入圍

俗話說得好,入圍就是得獎!

所以... 我得獎了!! XD

前些時候看到噗浪上有人提到今年度的全球華文部落格大獎開始報名,雖然覺得像本站這種拿技術當主軸的硬綁綁技術部落格,離大眾市場有段距離,得獎希望渺茫。但你也知道,搞技術搞久了,腦袋磨損,就會產生一些偏差行為。例如: 什麼都要量化,凡事都要建立度量的指標、把日常生活也當成系統Tuning... 而網站上也掛滿了Hit Counter、RSS訂閱人數、funP推文計數、部落格觀察排名。

聽到有部落格比賽,不管自己的Blog離雅俗共賞還有多遠的距離,也暫時忘卻網站的人氣其實都靠離題的鬼扯文撐場,反正不用繳報名費(咳... 這是重點),參加就是了,到時拿個成績來參考也好。(好想呼籲大會公布所有評分細節,不過應該沒人理我)

昨個兒初審入圍名單出爐,我參賽的訊息觀點類共158人參加,初審取58人,錄取率為36.70886%。(謎之聲: 怎麼又算起數字來,你無藥可救了)

PS: 最近工作頗為吃緊,部落格人數悄悄地破了110萬也無心發文留念,索性未來就改為20萬再刻一度好了。

深秋採柿行

兒子幼稚園同學的爸爸媽媽揪了一團採甜柿,找了幾個小朋友家庭相約一同去新竹五峰遊玩兼採柿。因為自已宅讓小朋友跟著孤陋寡聞是一種不負責任的行為,加上團長爸媽說這是他們每年必跑的行程,算是口碑卓著, 種種考量下,就決定跟團一遊。(謎之聲: 其實最吸引你的是行程規劃都有人安排得好好吧? 有前科的懶鬼)


(北二高有一段車速較慢,原來駕駛人忙著看直升機洗礙子)

我們先到關西的一家溫室番茄甜椒果園採番茄。第一次見識到所謂的水耕溫室裁培,人工嚴格管控的結果,在廣大的溫室中,番茄"樹"生長狀況良好,碩果纍纍。

  

小朋友在採番茄上得天獨厚,成熟番茄多集中在根部附近(可能上半部的熟果都被摘光了吧?! 不知為什麼講到這個,一直讓我不斷聯想起王戎大膽推論路邊皆苦李的故事,後來才恍然大悟,原來王戎也是茶包射手一族,所以深得我心呀!), 大人彎腰後還要撥開葉子才能一窺的成熟紅番茄,剛好就在小朋友視線的熱區。

只見女兒拖著竹籃子殺進一片青翠的番茄叢林不見人影,沒幾分鐘就笑兮兮地拖著沈甸甸的籃子跑來現寶。哇! 好多五顏六色的大番茄! 我趕緊阻止殺紅眼的小蘿拉把整個果園的番茄搬回家,免得她爹要留下來做長工。

 

不過顯然我多慮了,小朋友耐心有限,小童工做不到十分鐘,新鮮感消失,就丟下籃子,開始跟她弟弟玩起軌道搬運車來。

在番茄園學會認桃太郎跟牛番茄兩個品種, 還知道番茄會長得奇形怪狀是因為重覆授粉(就是嘿咻兩次)。時近十點, 溫室開始向我們展示什麼叫"溫"室? 又溼又熱又不通風的環境對我們下了逐客令。

 

 

離開番茄園,我們接著往森林農場出發採柿。毫無概念的我,以為柿園不過開個幾十分鐘到鄉間就有得採,隨著海拔一路攀升,還漸漸出現傳說中的之字形連續髮夾彎,路旁開始出現歡迎光臨某某部落的原住民社區地名,這才體悟到,原來採甜柿要到高山上才採得到的啦!

一路山路巔搖,另一車有小朋友抓兔子了,於是我們暫停路邊向一個民宅前賣甜柿的小攤借洗手間。跟人家借了洗手間,就想順便買點杮子。
(盛竹如旁白: 這幾個媽媽作夢也沒想到,原來只是在路旁借個洗手間,卻從此徹底改變了原本排好的行程,命運的鎖鏈,正一步步將他們推向山上的果園! 讓我們繼續看下去)

幾個媽媽對攤子上碩大鮮橘的柿子著了迷,順口跟顧攤的原住民青年聊說我們要去山上的森林農場採甜柿,青年人馬上說,這些漂亮甜柿是他舅舅種的,要採柿子去找舅舅採就好了。於是一群人馬上把原本的行程表撕碎灑向風中,由青年騎著機車,領著我們開上40度斜坡的產業道路,展開Exceptional的採柿行程。

 

到柿園的路雖小,但並不遠,很快我們到了一片山坡地果園間的小平房。一位賽夏族大哥出來接待,馬上端出兩大盤水果請我們吃,為人還十分風趣親切,非常吻合印象中原住民特有樂天、熱情的形象。我的生活圈很小,平常接觸的人總是同一群,算了算,長這麼大機會跟原住民朋友相處,也只有專科時班上兩個女同學,跟當兵時的一個修空調的學弟。空調學弟十分有趣,皮膚很白,他的師父選兵時有聽出他口音怪怪的,但人又不黑,就以為是客家人之類,來報到時才發現是花蓮阿美族,師父學長一陣苦笑! 後來他在部隊做出不少好玩的事,他永遠笑咪咪,沒什麼心機,沒什麼煩惱像個開心果,休假三天忽然興起,就跟朋友跑去果園打零工,回部隊時全身曬傷;也曾喝醉酒在寢室打赤膊坐在衣櫃前欄查來往人員,順便嗆一下梯數。遇到學弟就罵一下為什麼現在還不睡,若是學長就惦惦不做聲,醉歸醉還是嚴遵軍中倫理哩,真鮮。阿美族學弟鮮明獨特的性格讓我對原住民留下深刻的印象...

好像離題離太遠了... (跳一下)

 

量產的杮子樹如上圖所示,會用竹架撐起,才能托住大杮甜的重量, 另一方面也控制樹的高度,方便採收。結果後會用白色紙袋包起來,避免果實受傷或遭蟲咬。所以在果園裡並不會出現一棵棵大樹上掛滿鮮橘甜柿的壯觀景象,反而有點像絲瓜棚或葡蔔園。

當然好奇的我不會放棄增長見聞的機會,遇到熱情親切的專業人士,當然要請教一下栽種甜柿的知識。原來甜柿都是日本品種,要透過接枝"整合"在台灣本地的杮樹上(瞧,這Interface設計得多好,兩套系統可以天物無綘地接合在一起,還維持這麼好的Throughput,造物主應該深諳OOP之道吧!)。我看樹幹上有一圈圈的痕跡,也好奇發問,賽夏大哥說這是在雨季期怕過樹枝吸收過多水份,所以會在樹幹上做環切,破斷樹皮減少水份傳輸。另外,樹幹上如有青苔要刷乾淨,可以讓樹、果生長得更好...

親眼見識也品嚐了新鮮甜柿,加上訂價並不貴,一行人開始發了狂似在荒山野地裡血拼起來,十斤八斤地秤、一千兩千地買,園主大哥也很會做生意,秤到480,就再補上一大粒,說這樣算五百。沒幾分鐘,兩大紙箱的甜柿就被掃到近乎見底,最後園主使出必殺技,在箱底抓了一顆秤秤重,再點一下顆數,喊出整箱400,順手成交。我們真的把他全部的貨都掃光啦~~~

算下來我們一行人前後貢獻業績近萬元,別人周年慶都在百貨公司敗家花錢,我們卻在山上果園殺聲震天,但我深深感覺,花錢消費享受台灣水果的鮮甜,好過進貢給LV、GUCCI、香奈兒遠在歐洲的大爺。鄉親吶,這就是愛台灣的啦~~~~

滿足了採柿的心願,我們結束意外行程,回到原本的規劃上(啊… 行程表剛才是誰撕碎的?),前往山上的森林農場吃午飯。農場算是這幾年典型的休閒園區風格,歐化建築、原木陽台、露天咖啡座... 可惜天候不佳,身處海拔1200公尺註定要與雲霧為伍,但這無損小朋友的玩興。團長媽媽從車上拿了紙箱拆開變成一塊塊滑草板,讓所有的小孩在農場的大草坪"滑"得不亦樂乎。不過,瓦楞紙板是沒辦法在和緩的斜坡產生足夠的推進動力滴,得靠"獸力"拖行,所以爸爸們都很累!

 

踏上歸途前,眼前出現高山才看得到的雲海美景,機不可失,我趕緊抓下幾個帶有幾分中國水墨畫風格的鏡頭,為採柿行留下一個"山在虛無漂渺間"的驚嘆結尾。

 

MEMO-使用LINQ to SQL直接執行SQL指令

使用LINQ to SQL時,難免會遇到基於簡潔效率考量需要直接下SQL指令的場合。依我的習慣,遇到這類情境我就不用LINQ寫法硬幹了。DataContext物件提供了ExecuteCommand及ExecuteQuery兩個方法,可以直接撰寫有效率的SQL語法,交給DB執行批次更新動作或取回複合式查詢的結果。

但切記!! 直接操控SQL語法並不代表用直接組裝SQL指令字串,這樣很容易產生SQL Injection漏洞。基於這條開發基本常識,ExecuteCommand當然也鼔勵大家用Parameter處理動態參數,Method的第一個參數是CommandText,後續可再逐一傳入參數值。這裡CommandText的參數宣告寫法與傳統"@paramName"的格式不同,要透過{0}, {1}方式指定。實際執行時,冰雪聰明的DataContext會將{0}, {1}轉成@p0, @p1,並依傳入參數值物件的型別決定Parameter型別。

我寫了個簡單的程式,分別傳入字串、整數及日期做示範:

public static void Test3()
{
    using (MyLabDataClassesDataContext db = 
        new MyLabDataClassesDataContext())
    {
        db.Log = Console.Out;
        db.ExecuteCommand(@"
UPDATE HumanResources.Employee 
SET Title = {0}
WHERE EmployeeID = {1}
AND BirthDate = {2}",
             "Jedi Master",
             1,
             new DateTime(1972, 5, 15)
        );
        Console.Read();
    }
}

利用Log轉向,可以觀察實際被轉換成的SqlCommand資訊如下:

UPDATE HumanResources.Employee
SET Title = @p0
WHERE EmployeeID = @p1
AND BirthDate = @p2
-- @p0: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [Jedi Master]
-- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [1972/05/15 上午 12:00:00
]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.1

很簡便吧!

查詢時的處理方法也差不多,但ExecuteQuyer必須指定查詢結果要對應的物件型別,如果查詢結果來自JOIN或有另外組合欄位的話,就沒有現成的物件型別可用,要自行定義物件來承接查詢結果。如以下範例:

class ResultClass
{
    public int EmployeeId { get; set; }
    public string Title { get; set; }
}
 
public static void Test4() 
{
 
    using (MyLabDataClassesDataContext db =
        new MyLabDataClassesDataContext())
    {
        db.Log = Console.Out;
        var q = db.ExecuteQuery<ResultClass>(@"
SELECT EmployeeID,Title 
FROM HumanResources.Employee 
WHERE Title LIKE '%' + {0} + '%'",
                             "Manager");
        foreach (ResultClass x in q)
        {
            Console.WriteLine("{0}-{1}",
                x.EmployeeId,
                x.Title
                );
        }
        Console.Read();
    }
}

執行結果如下:

SELECT EmployeeID,Title
FROM HumanResources.Employee
WHERE Title LIKE '%' + @p0 + '%'
-- @p0: Input NVarChar (Size = 7; Prec = 0; Scale = 0) [Manager]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.1

3-Engineering Manager
6-Marketing Manager
21-Production Control Manager
30-Human Resources Manager
42-Information Services Manager
71-Finance Manager
90-Document Control Manager
139-Accounts Manager
150-Network Manager
158-Research and Development Manager
200-Quality Assurance Manager
217-Research and Development Manager
218-Facilities Manager
268-North American Sales Manager
274-Purchasing Manager
284-European Sales Manager
288-Pacific Sales Manager

【茶包射手專欄】aspnet_regiis加密失敗的還原

同事遇到狀況,用aspnet_regiis加密web.config的ConnectionString區段,過程發生錯誤(錯誤訊息不可考),之後ASP.NET程式不管點選任何ASPX連結都呈現無法顯示(Hang住遲無回應)。

我找到Global.asax裡有段Application_OnStart涉及資料庫存取,懷疑它是導致所有ASPX都癱瘓的源頭。先跳過其中的邏輯,就進入下一關,得到以下訊息:

無法使用提供者 'RsaProtectedConfigurationProvider' 解密。來自這個提供者的錯誤訊息為: 無法開啟 RSA 金鑰容器。
Failed to decrypt using provider 'RsaProtectedConfigurationProvider'. Error message from the provider: The RSA key container could not be opened.

猜想這與先前加密設定過程出錯有關,我的計劃是先退回到原本設定前的狀態再做打算。決定砍掉重練---用aspnet_regiis -u / aspnet_regiis -i 重新註冊ASP.NET,或許可以洗掉不好的記憶(與開大水沖澡有異曲同工之妙)。

不幸地,解除並重新註冊ASP.NET並沒能解決問題。再追了一下,發現\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\web.config的檔案時間是今天寫入的,意思是aspnet_regiis -i時並不會還原它到預設狀態。此時,我發現同一目錄下有個web.config.default,檔案大小與內容與web.config相近,八成是微軟留下來的還原用滴,那我就不客氣了,用web.config.default覆寫web.config,問題排除,收隊回家!

TOOLS-IDbCommand.Parameters抓漏工具

不知天生粗枝大葉還是怎樣,每回只要操作大量欄位Table的新增修改,我總會迷失在茫茫的Parameter大海中...(這也解釋了為什麼我愛死了LINQ to SQL/LINQ to Oracle)

複雜的資料表有三四十個欄位是家常便飯,CommandText就得宣告三四十個參數名稱,然後照著這堆參數名稱一一Parameters.Add,只要名稱打錯或多一個少一個,就會冒出"ORA-01036 illegal variable name/number 變數名稱/號碼無效"之類的錯誤,通知我練眼力的時間到了! 此時別無他法,只能逐一檢查有無打錯字,多參數少參數的。動轍要核對三四十個參數名稱,對耐性欠佳的我是種折磨,偶爾甚至會有輕生的念頭...

為了避免自己常陷入性命攸關的危機,我寫了一個工具函數來幫忙檢核。如以下的範例,一旦Parameter數目、名稱與CommandText中的宣告對不上,就會得到警告:

System.Data.OracleClient.OracleCommand cmd = 
    new System.Data.OracleClient.OracleCommand();
cmd.CommandText = "insert into pack values (:v_id, :v_name, :v_count)";
cmd.Parameters.Add("v_name", OracleType.VarChar).Value = "Item1";
cmd.Parameters.Add("v_id", OracleType.Number).Value = 1;
//cmd.Parameters.Add("v_count", OracleType.Number).Value = 100;
cmd.Parameters.Add("v_wtf", OracleType.VarChar).Value = "WTF";
string result = DbJobHelper.VerifyCommandParameters(cmd, ":v_", false);
Response.Write(result);

結果為: Parameter[v_wtf]-not used! Parameter[:v_count]-missing!

另外,函數還提供了reorder的功能,讓Parameter可以依其在CommandText裡的出場順序重新排列。

程式碼如下,需要的朋友請自取:

    /// <summary>
    /// IDbCommand.Parameters檢核,找出Parameters數目不合問題
    /// </summary>
    /// <param name="cmd">IDbCommand物件</param>
    /// <param name="prefix">CommandText中的參數名特徵,如:v_, @p_</param>
    /// <param name="reorder">是否重新依參數出場順序排序</param>
    /// <returns></returns>
    public static string VerifyCommandParameters(
        IDbCommand cmd, string prefix, bool reorder)
    {
        Dictionary<string, string> paramSymbolCheck = 
            new Dictionary<string, string>();
        Dictionary<string, string> paramUsageCheck = 
            new Dictionary<string, string>();
        Dictionary<string, IDataParameter> paramPool = 
            new Dictionary<string, IDataParameter>();
        List<string> paramOrder = new List<string>();
        string s = "";
        for (int i = 0; i < prefix.Length; i++)
            s += "[" + prefix.Substring(i, 1) + "]";
        //用RegEx找出CommandText中的參數宣告
        foreach (Match m in Regex.Matches(cmd.CommandText,
            "(?ims)(?<n>" + s + "[a-zA-Z0-9_]+)"))
        {
            //OracleParameter要去掉前方的:才是ParameterName
            string pn = m.Value.ToUpper().TrimStart(':');
            paramSymbolCheck.Add(pn, m.Value);
            paramOrder.Add(pn);
        }
        //找出所有參數物件
        foreach (IDataParameter p in cmd.Parameters)
        {
            paramUsageCheck.Add(p.ParameterName.ToUpper(), 
                p.ParameterName);
            paramPool.Add(p.ParameterName.ToUpper(), p);
        }
        StringBuilder sb = new StringBuilder();
        //檢查是否每個Parameter都有被用到?
        foreach (string n in paramUsageCheck.Keys)
        {
            if (!paramSymbolCheck.ContainsKey(n))
                sb.AppendFormat("Parameter[{0}]-not used!\n", 
                    paramUsageCheck[n]);
        }
        //檢查是否每個參數宣告都有對應的Parameter?
        foreach (string n in paramSymbolCheck.Keys)
        {
            if (!paramUsageCheck.ContainsKey(n))
                sb.AppendFormat("Parameter[{0}]-missing!\n", 
                    paramSymbolCheck[n]);
        }
        //依出現順序重排參數
        if (sb.Length == 0 && reorder)
        {
            cmd.Parameters.Clear();
            foreach (string n in paramOrder)
                cmd.Parameters.Add(paramPool[n]);
        }
        return sb.ToString();
    }
【茶包射手日記】關於狂牛症蛋白質600度金剛不壞之謎

【聲明: 本文乃對狂牛症議題裡幾點邏輯方法的探討,無涉支持或反對牛肉進口,也無意挑起兩派論戰。歡迎大家提出自己的看法,但希望能聚焦在文中所提的幾點科學論證與研究方法上,與內文焦點無關的留言可能會視狀況刪除,請見諒!】

老讀者們都知道我是一個嗜邏輯如命到有些偏執的怪胎,在接受任何主張推論前,總會先要求具體事證。我曾不只一次在同事說"自從上回修改過那個設定後,系統就開始出問題"後,先不著手研究二者因果,而是冷酷質問: "有沒有檔案更新時間與系統錯誤Log的對照可以支持你的說法?"。也曾經多次在事故調查時,面對夥伴已在喊冤:  “我這次修改元件,完全沒有動到與出事模組相關的功能",還繼續冷血主張: "依目前蒐集到的資料來看,暫時還不能排除更新元件與本次意外的關聯,這陣子先不要出國或旅行,最好確定我隨時可以找到你"。由這些只認邏輯、枉顧人情的機車行徑來看,我沒被蓋布袋真是萬幸~~~

茶包射手做久了,不但對工作遇到的系統問題事事推敲,連看到生活周遭有不合邏輯、缺乏理性判斷的事件,也總不免要碎唸上幾句方休。(例如: 這個例子這個例子這個例子) 而最近我觀察到比較有趣的例子是正反雙方正在激烈辯論的美國牛肉進口與狂牛症相關議題。

噗友R30給了一個很傳神的結論,狂牛症議題己演化成政治、信仰類的問題,漸漸失去了理性討論的空間。反對者被對方抺黑成不明事理只知人云亦云的鄉巴佬;支持者被對方屈解成硬拗狡辯一心為當局護航的王八蛋。先將對手妖魔化是政治操作的第一步,從此,對方說什麼都可以當成放屁置之不理,然後雙方各自表述,永無交集...

我熱愛科學,最主要的原因是科學很客觀,很公平,總能找到一個全世界公認的方法證明你是對的(幾乎啦! 我沒有足夠的事證可以主張永遠沒有例外),不像政治、信仰,每人心中各有一把尺,吵到進棺材也不可能產生結論。因此,看待狂牛症討論,我感興趣的部分也只在其中科學論證部分是否謬誤或存在我認為不合理之處,至於要辯出最終雙方都能接受的共識,我沒抱太高期望。

我必須要說,雖然有很多朋友都積極主張狂牛症極其可怕所以吃美國牛會發生嚴重後果,甚至有人下了"人類最後天譴"的聳動標題,在看過一些相關論述後,我仍覺得並沒有說服我。(謎之聲: 看你平常的機車表現,不意外)

我較持保留態度大概在幾點上:

  1. 狂牛症極其可怕應該是大家有共識的部分(我完全贊同),但關於吃美國牛肉導致狂牛症的風險機率,則正反兩方有很大分歧: 支持方說跟雷打到一樣低(牛絞肉5.77*10^-10, 牛內臟1.50*10^-10, 帶骨牛肉2.72*10^-11, 不帶骨牛肉7.18*10^-12),反對方則有人主張因亞洲人愛吃牛內臟,故風險比美國人更高。我覺得這點忽略了美國人比亞洲人更愛吃漢堡熱狗,食用牛絞肉的機率高於亞洲,在未否認牛絞肉致病率比內臟更高的情況下(反對方沒有提出此一論述或說明),要以此點推論台灣人因為愛吃肉臟而提高風險便不合邏輯。(倒不是要否定台灣將承受更高風險的結論,而是質疑"內臟論"邏輯推演的有效性,即便風險真的較高,亦非源於吃內臟)
  2. 在未能證明上一點【台灣人會比美國人因為吃美國牛而承擔更高風險】前,要求台灣要比美國人抱持更高警戒心與更嚴苛管控手段的主張就顯得缺乏支持力。而我的一大疑惑也在於此: 如果牛絞肉引發狂牛症的機率不容小覷,為何美國人還能每天面無懼色地大嚼牛肉漢堡? 觀察下來,似乎要解釋此點就只得搬出陰謀論,例如: 美國人吃的保證不是問題牛、黑心牛一律都出口、為免經濟解體只好欺騙人民... 之類的。對邏輯性要求很機車的我,基本上對陰謀論的接受度偏低,目前還沒看到能說服我的說法。
  3. 在反對文章裡看到一段讓人震憾的事實:"狂牛症蛋白質甚至用高溫700度或放射線皆無法滅絕,化為灰燼後仍有感染性,若任意丟棄土壤也會感染" 。由於跟我學生物時的認知差實在差距太大,一開始看到時我拒絕相信,問了生化專長的Lucas,他的猜測是"70 度大概是指 巴斯德殺菌法 的72^C 15分鐘, 跟放射線一樣都是常用而且溫柔的殺菌方法, 對這種頑固的蛋白質無效",因此我一直誤以為700度是70度的筆誤。當我把此一看法貼到朋友的Blog上,朋友很細心地回覆我一個更震憾的補充參考(針對疑點提出相關佐證,我喜歡這樣的討論互動): 一篇德國的論文指出"為了消除變形Prionen蛋白(朊病毒)須將感染的物質以攝氏133 ° C加熱 20分鐘之久, 且以氣壓力為3 Bar(蒸氣高壓鍋)的方法才可能消滅它。即使在加熱至攝氏 600度15分鐘之後,還是可以找到依然完好Prionen蛋白(朊病毒)"。這就是足以說服我的事證了,不過基於與常理差異實在太大,實在很難接受此一論點,但畢竟這個領域遠超出我的知識範圍,總不能找一塊牛排跟一枝番仔火就親自驗證。在沒有其他證據可以推翻它之前,似乎只能選擇接受。

明知有疑,卻不能深入一探究竟對茶包射手來說是一種煎熬! 所幸,Lucas跟我一樣篤信科學,對於可疑事物一向也愛追根究底,經過專業考證與解讀後得到了一個重要心得 -- 這個推論出到600度金鋼不壞的科學研究,存在一些疑點(感謝Lucas用白話把深澀的豆芽菜生物學論文解釋到連我都看得懂):

  1. 未能解釋實驗結果裡加熱5分鐘未致病,加熱10分鐘後反而可以致病?
  2. 未如預期產生稀釋後機率下降的結果(莫非另有致病源?)
  3. 缺乏對照組實驗排除實驗系統的背景污染

質疑研究方法與結論對錯是兩件獨立的事。要說服大家600度燒成灰的蛋白質仍帶有致病力本來就是個艱鉅挑戰,至少這個實驗存在的疑點,可以讓我先對它持保留的態度,留待更多的資訊再做決定是否要相信600度金鋼不壞的神話。

最後推一下Lucas文中提到的有趣觀點: "先科學、再民主",就算全民公投通過把圓周率從3.14改成4.13,我會接受這個公投結果,但心中仍然選擇繼續相信可以用公式驗證為永遠是3.1415926...的圓周率數值。比伽利略幸福的是,我們身處一個可以堅持自己科學信仰,不用擔心被送上宗教法庭判刑燒死的民主時代,I love it!

Visual Studio偵錯時無法重新選擇原始檔位置

在Visual Studio Debug過程中,如果專案裡引用了其他自製元件的DLL檔案,而不是直接參考自製元件的Project,Debug過程若該元件內部出錯,找不到原本設定的原始程式碼,Visual Studio便會彈出如下訊息:

The source file is different from when the module was built. Would you like the debugger to use it anyway?

此時你可以答Yes,選擇忽略這段原始碼,斷續偵錯其他部分。

不過天不從人願,往往追了大半天,問題來源還是指向自製元件的內部邏輯,因此想改變心意,找來該元件的Source Code想Debug進去,卻發現因為先前選擇略過它的原始檔,之後Debug時就無法將執行中斷點與原始檔結合,VS會一律略過它,沒機會重新選擇原始檔位置。

摸索了一陣子,發現原來忽略原始檔位置的設定被儲存在Solution中。如下圖,由Solution的內容屬性找到Debug Source Files選項,就可以將原本的略過設定清除囉! 祝大家Debug愉快。

MEMO-Procedure Transaction: SQL Server vs Oracle

同事詢問關於Procedure內是否會自動包成Transaction的問題,我的認知是SQL要額外下SET XACT_ABORT ON,但Oracle則預設會自動包成Transaction。記憶有些模糊,所以索性做個實驗最準。

分別在SQL, Oracle寫了Procedure,先塞入兩筆資料,第三筆故意產生PK重覆錯誤,觀察資料庫是否有前兩筆資料來判別是否有Transaction保護。實驗證明,原先的認知沒錯,二者行為不同。

SQL Server Procedure要將操作包成Transaction的話,記得要下SET XACT_ABORT ON!

/*** SQL Server Test ***/
--建立有PK的Table
create table jefftable (
    id decimal, 
    constraint pk_jefftable primary key (id)
)
--建立SP, 故意塞入重覆PK
create procedure jeffproc
as 
begin
   /* SQL SP要自成Transaction, 要加這列
    set xact_abort on
   */
   insert into jefftable values (1)
   insert into jefftable values (2)
   insert into jefftable values (1)
end
--執行SP, 會產生錯誤
exec jeffproc
--查Table會看到兩筆
select * from jefftable
--清理實驗廢棄物
drop table jefftable
drop procedure jeffproc
 
/*** ORACLE Test ***/
--建立有PK的Table
create table jefftable (
  id decimal, 
  constraint pk_jefftable primary key (id)
)
--建立SP, 故意塞入重覆PK
create procedure jeffproc
is
begin
  insert into jefftable values (1);
  insert into jefftable values (2);
  insert into jefftable values (1);
end;
--執行SP, 會產生錯誤
declare
begin
jeffproc;
end;
--查Table, 沒有資料
select * from jefftable
--清理實驗廢棄物
drop table jefftable
drop procedure jeffproc
活到老學到老-CSS Media Types

在部落格上分享技術心得,常帶來意外收獲!

網友的迴響總讓我見識增長,額外學到的新東西超乎想像,每每發現過去忽略或未曾接觸的寶藏,總令我欣喜若狂。

說出來也不怕大家笑話,從事Web開發多年,卻未曾在Javascript下過苦工,對CSS也是開始把玩jQuery後才漸漸熟悉的。終日周旋於工作家庭間,己難再鼔起滿腔熱血,心無旁鶩地狂K三天,將一個技術摸到爛熟。【需要再學,夠用就好】成了不追根究底的藉口,找到堪用工具,儘速解決問題,如期領到薪餉,珍惜家庭時光成為中年程序員卑微卻知足的生活方式,噗友說,這叫MOP(Money Oriented Programming),一針見血! (謎之聲: 嘴砲一堆,何不痛快承認你是喪失偉大理想抱負的現實程式工就好了?)

話說,昨天貼了一篇網頁列印強迫分頁的jQuery心得分享。本站常客,Javascript界的絕地大師,Ammon,忽然現身點醒我—Use the force, Luke Use the media, Dark。

一語警醒夢中人,其實過去在檢視一些網頁HTML Source時,早看過media="print,screen”的寫法,只是一向得過且過,從沒深究過它的意涵。經Ammon一提醒,才發現我忽略了好東西。

簡單來說,透過指定CSS Media Type,我們可以對同一個元素分別為顯示、列印、行動裝置指定不同的呈現樣式! 老實說,這是非常基本的CSS應用;但搞Web這麼久,我卻一直忽略了它,直到今天。

廢話不多說,請看我整理的範例:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Print Friendly</title>
    <style type="text/css">
        .cContent  
        {
            margin-top: 10px;
            margin-left: 10px;
            width: 400px;
            border: dotted 1px #444444; 
        }
        /* 可以用@media mediaType的方式宣告不同媒體專用樣式 */
        @media screen 
        {
            /* 螢幕上是藍底白字 */
            .cContent  
            {
                color: White; 
                background-color: Blue;
            }
        }
        @media print 
        {
            /* 列印時是白紙黑字的 */
            .cContent { color: #222222; }
        }
    </style>
    <!-- link, style可用media="mediaType"宣告適用時機 -->
    <style type="text/css" media="screen">
        /* 顯示時隱藏 */
        .cPrint { display: none; }
    </style>
    <style type="text/css" media="print">
        /* 列印時隱藏 */
        .cScreen { display: none; }
    </style>
</head>
<body>
<div class="cPrint">Print Header</div>
<div class="cScreen">Display Header</div>
<blockquote class="cContent">
One of the most important features  
...
certain media types. 
</blockquote>
</body>
</html>

應用這個技巧,昨天的程式我們可以修改成:

<style type="text/css">
@media print 
{
    .cPrint  
    {
        display: block;    
        page-break-before: always; 
    }
    #btnPrint { display: none; }
}
@media screen
{
    .cPrint { display: none; }
}
</style>
<script type="text/javascript">
    $(function() {
        //DataList的每六列加一個表頭
        var $rows = $("#DataList1 > tbody > tr");
        $rows.each(function(i) {
            //取6的倍數, 排除最後一筆
            if ((i + 1) % 6 == 0 && i != $rows.length - 1) {
                //加上頁首並設定跳頁
                $(this).after("<tr class='cPrintOnly'><td>" +
                $("#dvPageHeader").html() + "</td></tr>");
            }
        });
        $("#btnPrint").click(function() {
            //列印
            window.print();
        });
    });
</script>    

再次感謝Ammon大師開示。

MEMO-網頁列印強迫分頁

專案裡有個網頁列印的需求,在一份清單中,每個項目以Table方式呈現,而列印時不希望項目Table跨頁。以下是我搞出來的解決方案:

<style type="text/css">
.cPrintOnly { 
    page-break-before: always; 
    display: none;
}
</style>
<script type="text/javascript">
$(function() {
    //DataList的每六列加一個表頭
    var $rows = $("#DataList1 > tbody > tr");
    $rows.each(function(i) {
        //取6的倍數, 排除最後一筆
        if ((i + 1) % 6 == 0 && i != $rows.length - 1) {
            //加上頁首並設定跳頁
            $(this).after("<tr class='cPrintOnly'><td>" +
        $("#dvPageHeader").html() + "</td></tr>");
        }
    });
    //列印鈕功能
    $("#btnPrint").click(function() {
        var $dv = $(this).parent();
        var $pgHdr = $(".cPrintOnly");
        //將列印鈕隱藏
        $dv.hide();
        //顯示換頁頁首
        $pgHdr.show();
        //列印
        window.print();
        //顯示列印鈕、隱藏換頁表頭
        $dv.show();
        $pgHdr.hide();
    });
});
</script>

【程式說明】

  1. 列印時能強迫分頁歸功於CSS屬性: page-break-before
  2. 利用jQuery的each()及index參數,做到每隔六個項目一數,在後方插入一個表頭並設定強迫換頁印列。(奉天承運,User詔曰: 每頁都要有頁首,不然很醜...)
  3. 用window.print()列印。(註: 有跨瀏覽器Issue,但本案瀏覽器限IE)
  4. 換頁頁首預設隱藏,列印按鈕按下時,顯示換頁頁首,並隱藏列印鈕,印完再恢復。
    【2009-11-05更新】關於列時印隱藏元素,有更好的做法

搜尋

Go

<November 2009>
SunMonTueWedThuFriSat
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345
 
RSS
【工商服務】
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


BlogLook Score and Rank

Syndication