September 2007 - Posts

TIPS-About UI Thread Limitation

這是一個程式"中鳥"開發Windows Form要面對的問題...

Windows Form裡的Threading有些討厭的限制。菜鳥還處於天真無邪的Single Thread打天下階段,渾然不知它的險惡;而老鳥早就吃過苦頭,熟知要如何對付它,所以也不畏懼它的刁難。而剛開始學會在Windows Form中展現Multi-threading威力的中鳥,一起步多半要先闖過這一關。

用一個最簡單的例子來說明好: 我寫了一個Windows Form,放了一個label1一個button1, 在button1_Click事件另外建立一個Thread呼叫chgLabel(text)修改label1的Text屬性:

        private void button1_Click(object sender, EventArgs e)
        {
            Thread trd = new Thread(new ThreadStart(jobBad));
            trd.Start();
        }
 
        private void jobBad() 
        {
            chgLabel("Wrong way.");
        }
 
        private void chgLabel(string text)
        {
            label1.Text = text;
        }

程式碼很簡單,看起來應該沒什麼問題吧?

如果你這麼想,表示你在Windows Form的世界還涉世未深...

這段程式觸犯了Windows Form裡的一條鐵律: 在非UI Thread裡更動了UI的內容!

從沒用C++, Windows SDK角度探索Windows Application Model的我,並不知道它的細節原由,但歷經多次慘痛的經驗教訓,我學到一點: 如果你另外開了一條Thread,程式碼如會變更到Windows Form上的Control內容,請記得繞道而行。在上述的例子中,我們很明確地修改了Label的內容,但有時程式碼會在你不知情的情況下更動了UI元素。最典型的例子是DataGrid Bind到一個DataTable,而我們在另一條Thread中表面上只更改了DataTable的內容,並沒有修改UI上的Control;但由於Data Binding的關係,一更動DataTable就會間接觸發了修改DataGrid顯示的事件,進而違反了在非UI Thread修改UI內容的規則。

記得在.NET 1.1時代,違反這條規則並非絕對會出錯,而是偶爾冒出Null Reference Exception讓你丈二金剛摸不著腦袋,完全不知自己犯了什麼天條遭此報應。.NET 2.0比較仁道一點,會很明確地彈出以下的訊息:
Invalid Operation Exception:
Cross-thread operation not valid: Control 'label1' accessed from a thread other than the thread it was created on.

這張罰單把違規事由寫得清清楚楚,想當初由Null Reference去追出問題出在UI Thread限制就讓我吃盡苦頭,由.NET 2.0入門的朋友算是幸福多了。

Good! 知道問題後,要怎麼修改? 官方的標準答案是宣告一個deleate, 再用InvokeBeginInvoke來觸發它,這樣可以強迫Windows切回UI Thread來執行程式

        private void button1_Click(object sender, EventArgs e)
        {
            Thread trd = new Thread(new ThreadStart(jobGood));
            trd.Start();
        }
 
        private void chgLabel(string text)
        {
            label1.Text = text;
        }
 
        delegate void ChgLabelHandler(string text);
        private void jobGood()
        {
            this.Invoke(new ChgLabelHandler(chgLabel), "Correct way.");
        }

還有一些進階的課題,例如: 某些Code可能被UI Thread呼叫,也可能被非UI Thread執行,所以可以用InvokeRequired來偵測,決定要直接變更UI內容或是使用Invoke,這裡就先不要搞得太複雜,以免嚇到大家。先記住一點,當你遇到"Cross-thread operation not valid",要知道發生了什麼事以及該怎麼辦,就夠了。

第二次攝月練習

 

一個月前,拍了張月全蝕的照片,幾位好"攝"的老友們紛紛提供了拍攝建議,我歸納的結論大約有以下幾點:
1.300mm長鏡頭的細節解析力畢竟還是比135mm*1.6高出許多。
2.月亮移動的速度也許比我們想像來得快。
3.穩定的腳架對於長焦距拍攝很重要。(這表示我那支幾百元的破腳架可能是幫凶)

不過,我並沒有趁機用這些理由去敗一支小白或刷一根碳纖腳架回家,窮酸人家有窮酸人家的玩法。月亮你很會跑是吧? 破腳架你很愛晃是吧? 我就用拍王建民投伸卡球的快門來對付你!

報載今天中秋月最圓的時刻是28日凌晨3點45分,而且還是九年內最接近地球的一次,不過中年上班族沒這種體力跟它拼命,大約十點左右,看到月亮剛好從薄雲中露出臉來,我趕緊衝到樓下架好器材。這次我把快門拉上1/500-1/750,光圈則在f5.6-f6.7,ISO 100-200,試了幾種組合。最後證實,和上回相比,提高快門有效地讓清晰度大幅上升,本次的Performance Tuning可謂已獲得明確的驗證及成果,Case Closed!

TIPS-Hide <select> On IE6

前幾天我寫了篇帖子介紹利用高z-index+半透明Filter DIV做為HTML元件防點防選防編遮片的點子,不過留了個尾巴: 在IE6上,<select>是誰都蓋不住的!

事實上,IE Team自己也知道這個問題,並高興地在Blog中宣佈在IE7中總算做了改善(http://blogs.msdn.com/ie/archive/2006/01/17/514076.aspx),可是,使用IE6的User不在少數,總不能強迫所有User都升級IE7吧?

這幾天,認真地考慮將它用在我的專案中,在前端作出防編輯的效果。為了解決頭痛的IE6 <select>,在Google上用力地找了一番,結果找到一個天才發現的方法(http://www.hedgerwow.com/360/bugs/css-select-free.html),利用iframe z-index:-1; filter:mask(),可以在IE6中將<select>藏起來。只是有點可惜,<select>在遮罩下會完全消失,而無法呈現半透明刷暗或刷淡的效果,就看看這個"將就"的方案能不能被User接受了。

我將上次介紹的Script改成以下的做法:

maskStyle.height=tgt.offsetHeight+"px";
mask.innerHTML = "<iframe style='display:none;display:block;position:absolute;top:0;left:0;z-index:-1;filter:mask();width:"+
    tgt.offsetWidth +"px;height:" + tgt.offsetHeight + "px;'></iframe>";
document.body.appendChild(mask);

產生的遮蓋效果如下,算不算折衷的解決方案? 就見仁見智了。

WOW! Amazing Javascript Intellisense

VS 2008目前仍在Beta 2階段,當然還不適合開發正式的.NET 3.5專案,但VS 2008有所謂的Multi-Targeting,可以用VS 2008編譯出.NET 2.0, 3.0, 3.5版本的程式。或許有人會問,這樣有什麼好處? 答案是可以提前享受新版IDE帶來的貼心功能。

這兩天我改用VS 2008在一個現有ASP.NET 2.0專案裡加入新功能,呼~~~ 終於體驗到其中一項令人感動的新設計--Javascript Intellisense。

一張圖勝過千言萬語,看看我在ASPX中編輯Javascript時發生了什麼事...

居然可以像編輯.cs一樣帶出函數及參數說明,酷斃了吧?! 想知道這是怎麼辦到的嗎? 可以參考這篇文章

警告!! VS 2008畢竟還是Beta 2,仍有不少Bug,我遇過好幾次編輯頁面錯亂、IDE Hang、IDE Crash的狀況,Design View在編輯Control時,Properties Window經常慢半拍才有反應,想嚐鮮的人要做好心理準備,切記,"歡喜測甘願當"。XD

TOOLS-Implement Operation Mask With Javascript

設計網頁時,有時需要從Client-Side禁止使用者點選、編輯某些網頁元素(如TextBox、Select、Radio、Checkbox...)。偏偏設成唯讀的做法Input, TextArea, Select不盡相同,還要小心設定disabled屬性會影響值是否傳回Server-Side,過去我還特別寫了很複雜的JS萬用函數負責將各元件設成唯讀。

AJAX開始流行起來之後,我注意到其中一項有意思的特效--Modal Dialog。當使用者叫出某個檔案上傳或選取器對話框時,為了要求使用者完成選項才將焦點交回原網頁,會用一段Javascript將整個網頁刷灰,禁止使用者點選操作,唯一能操作的區塊只有聚焦的對話框。這樣就做出了IE showModalDialog的效果,而重要的是這種Javascript式的ModalDialog特效可跨瀏覽器,Firefox也適用。

仔細研究了一下它的原理,原來是它建了一塊跟網頁一樣大小的半透明灰底Mask DIV(使用style.filter特效),以絕對座標方式放在網頁的正上方,由於DIV的z-Index設得很大,所以會"浮"在網頁其他元素的上方,形成灰色刷暗效果,提示使用者網頁已被停用;而實體上也隔絕使用者接觸到下方的網頁元素。至於要開放操作的對話框,則將z-Index設得比Mask DIV大1,等同於疊在DIV之上,不受其影響。

搞懂了它的原理,我想到這招可以用來禁止使用者對Input, Button, Link等的操作(很遺憾Select在IE6中z-Index似乎是無限大,沒有東西可以蓋得住它,但在IE7 & Firefox中則可以適用。只是如果要用它做出停用欄位的效果,必須額外考量IE6的Select),甚至可以一口氣停用整個DIV, TABLE中的全部元素,十分直覺簡便。我寫出了以下的afa_PutMask(elementId)及afa_Remove(elementId)可用來加上及移除"遮片",加上遮片後被蓋住元素就不能被點選操作,在IE6, IE7, Firefox上測試都OK,有興趣的朋友可以拿去玩看看。

【注意】由Client-Side所加諸的唯讀、防寫效果並不具任何資安保全效果,當Javascript出錯或被停用時就玩完了,而一些高階玩家也能輕易找到工具穿越防線。所以Client-Side的唯讀保護,只適合用在被破解也無傷大雅的資料欄位上,或者要在Server-Side加上第二層防護。

<body>
<div>
<input id ="myInp" type="text" value="Darkthread" />
</div>
<hr />
<div id="myDiv">
This is test line 1.<br />
This is test line 2.<br />
<button>Click Me!</button>
<table><tr>
<td>Example Link:</td>
<td><a href="http://www.google.com" id="myLink">Google</a></td>
</tr></table>
</div>
<script type="text/javascript">
//JS Utility: Put operation mask on HTML element 
//Ver 1.0 @ 2007-09-22
//by Jeffrey Lee, http://www.darkthread.net
function afa_PutMask(targetId) {
    var tgt = document.getElementById(targetId);
    var mskId= targetId + "$$Mask";
    var testMsk = document.getElementById(mskId);
    if (testMsk) testMsk.parentElement.removeChild(testMsk);
    mask = document.createElement("DIV");
    mask.id = mskId;
    var maskStyle = mask.style;
    var tgtStyle = tgt.style;
    maskStyle.display="block";
    maskStyle.position="absolute";
    maskStyle.zIndex="100";
    maskStyle.filter="alpha(opacity=40)";
    maskStyle.MozOpacity="0.4";
    maskStyle.opacity="0.4";
    maskStyle.backgroundColor="#333333";
    var pos = afa_FindPos(tgt);
    maskStyle.top=pos[1]+"px";
    maskStyle.left=pos[0]+"px";
    maskStyle.width=tgt.offsetWidth+"px";
    maskStyle.height=tgt.offsetHeight+"px";
    document.body.appendChild(mask);
}
function afa_RemoveMask(targetId) {
    var tgtMask = document.getElementById(targetId + "$$Mask");
    if (tgtMask) document.body.removeChild(tgtMask);
}
function afa_FindPos(obj) {
    var curleft = curtop = 0;
    if (obj.offsetParent) {
        curleft = obj.offsetLeft;
        curtop = obj.offsetTop;
        while (obj = obj.offsetParent) {
            curleft += obj.offsetLeft;
            curtop += obj.offsetTop;
        }
    }
    return [curleft,curtop];
}
</script>
<script type="text/javascript">
afa_PutMask("myInp");
afa_PutMask("myDiv");
afa_PutMask("myLink");
window.setTimeout("afa_RemoveMask('myDiv');", 3000);
</script>
</body>

[Tech Summary] afa_PutMask() helps you to put a "operation mask" on any HTML element, the mask make elements (ex: input, textarea, button, link...)  look gray, non-clickable and non-editable.  It can be used as a tool to make element readonly, but there are two considerations:
1. It works fine on IE7 & Firefox, but <select> in IE6 is impossible to be masked.  If you want to use it to make every element readonly, you have to write some extra code for IE6 <select>.
2. Client side readonly doesn't provide any *REAL* security.  Javascript could be disabled or cracked, so never use it on impartant data integrity protection, or add server-side validation is a good compromise.

我的收藏: TechEd 2007 電腦背包

外型配色不錯,背起來也挺服貼舒適,只可惜今年背包上還多了三家"鑽石級"贊助商的Logo,有點減損包包的價值。(讓我想到兄弟象隊棒球衣上的那塊"五洲製藥"招牌)

另外再展示本次TechEd 2007的另一項戰利品,在胡百敬老師Session上回答問題拿到的--SQL Server狗狗鑰匙圈,少見這類走"精品路線"(因為完全沒有商業攝影的素養,所以看起來完全不"精",請見諒)的行銷贈品,特別拍照留念一番。

我的收藏: TechEd 2006 電腦背包

拖了一整年才順便連同2007的背包一起出來亮相,灰色的尼龍布材質,中規中矩,沒太多設計巧思,2006年款算是比較沒特色的。

TechEd 2007 Notes - Javascript Performance

Javascript是一種特殊的語言,既不是編譯式,也不是直譯式,算是一種動態語言(Dynamic Language)。其中有項特性,就是在Javascript中,包含了變數(Variable)、函數(Function)等等,在存取時,並不使用Pointer方式直接指向記憶體,而是以變數名稱、函數名稱字串在一個Hashtable中查詢(Symbol Lookup),找到變數/函數。尋找的過程會有所謂的Scope Chain的概念:

先找區域變數-->找不到時再找全域變數-->再找不到時搜索DOM

由於Symbol Lookup的過程相當耗用Resource,所以在這上面下功夫可以改善效能。幾個設計要領如下:

  1. 在{ }的範圍內使用var varName宣告,可將變數定義成區域變數,會勝過直接宣告varName;因其存取過程會先找區域,找不到再找全域,都找不到時再馬上建立。
  2. 每個”.”都代表一次Symbol Lookup,例如: document.all.objTable.all.objInput.value會觸發5次。所以,”.”愈少愈好!
  3. 把Function或DOM Object的Method設成區域變數也可以改善效能(很玄) 。
  4. 少用eval!! 每次叫用eval都等於載入一個完整的<script> Block。
  5. 避免String反覆相加,可以利用Array.push, Array.join的寫法取代,效能會好很多。

不過,在TechEd的這堂課程中並沒有提供上述改善的效能數字,我有點懷疑,把原本直覺式的寫法改得面目全非,是否真值回票價?? 於是我寫了以下的網頁,做幾個實地測試。程式主要是反覆用迴圈執行某項特定動作10萬到500萬次,並計算執行完成所耗的時間,如此周而復始進行十次以求平均值。

<body>
<table id="objTable"><tr><td colspan="2">
<input id="objInput" value="Darkthread" /><br />
<select id="objList" style="width:200px" size="11">
</select>
</td></tr>
</table>
<script type="text/javascript">
var lst=document.getElementById("objList");
var sum = 0, times = 10;
for (var c=0; c<times; c++) {
    var st = new Date();
    //Core Testing
    var findObj = document.getElementById;
    for (var i=0; i<100000; i++) {
        var x = findObj("objInput").value;
    }
    //================
    var ed = new Date();
    lst.options[lst.options.length]=
        new Option(c + "->" + (ed-st).toString());
    sum += (ed-st);
}
//Get Average Duration
lst.options[lst.options.length]=
    new Option("Avg->" + (sum/times).toString());
//Function for test
function func1(i) {
    return i;
}
</script>
</body>

測試1: Local Variable vs Global Variable 500萬次

X=1 -- > 2211.2 ms
var X=1 --> 2194.1 ms

測試2: DOM 10萬次

document.all.objInput.value = 1 --> 2828.1 ms (換算成500萬次,要141,405ms)
先var localVar = document.all.objInput,迴圈中再myInp.value = 1 --> 971.4ms (換算成500萬次,也是要45,870ms)

測試3: Function 100萬次

迴圈中直接var x = func1(i); --> 2477.6 ms
先var funcPtr = func1; 迴圈中var x = funcPtr(i); --> 2471.6 ms

測試4: DOM物件存取 100萬次

迴圈中跑var x = objInput.value --> 9808.1 ms
迴圈中跑var x = document.all(“objInput”).value --> 18765ms
迴圈中跑var x = document.getElementById(“objInput”).value --> 16692ms
先宣告var findObj = document.getElementById; 迴圈中跑var x = findObj(“objInput”).value --> 25283ms

測試5: 字串相加 10萬次

var s=""; for (var i=0; i<100000; i++) { s += "A"; } var x=s; --> 3325ms
var a=new Array(); for (var i=0; i<100000; i++) { a.push("A"); } var x = a.join(''); --> 900.3 ms

【結論】

  1. Local Variable與Global Variable的效能差異並不明顯。
  2. 用Lolcal Variable取代某個DOM特定元素,速度快很多(ex: var localVar = document.all.objInput,速度可以快三倍),應歸因於"."的使用減少,加速效果相當明顯。
  3. 利用Local Variable指向DOM中的Method Funtion(例如: var findObj = document.getElementById)意外地比直接呼叫還慢(原因不明),加上這種寫法會讓Code變得難懂,看來沒有採用的必要。
  4. Arrary.push()+join()取代直接字串相加,產生了超過三倍的加速效果,如有極大量的字串相加,可以考慮改寫。另外,ASP.NET AJAX MicrosoftAjax.js裡的StringBuilder物件也有同樣效果,如果專案有啟用ASP.NET AJAX,建議可改用它做字串相加。

PS: 如果想自己用以上的Code做測試,記得要先修改Registry,否則IE發現Javascript連續執行大量程式碼時,會出現"A script on this page is causing Internet Explorer to run slowly. If it continues to run, your computer may become unresponsive. Do you want to abort the script?"的警告,原本這是防止Javascript陷入無窮迴圈拖垮IE的保護機制,卻會對我們的測試造成困擾,可以參考這篇KB,將MaxScriptStatements Registry調成0xffffffff。

TechEd 2007 Notes - Are Your Application Ready For Vista?

TechEd 2007今天有一堂關於應用桯式Vista相容性的課程,對於有在寫Windows Form的人來說,這一天遲早會來。講師聖哥已經提供了完整的投影片(程式範例不久後會提供),要深入研究的請到此下載。只想懂個皮毛又不怕被我誤導的,可以參考以下我整理對.NET開發者較有關的心得摘要:

  1. Vista強化了系統相關檔案、資料夾、Registry的保護,只有Windows Update可以更動,應用程式最好避開,要儲放資料或設定,可以考慮%userpfile%下的資料夾及HKCU Registry Key。
  2. 在Vista裡,User個人資料已不是放在X:\Documents And Settings\UserName目錄下了,請使用環境變數的方式取得路徑,不要Hard-coding寫死。
  3. 以前Windows Service程式可以丟訊息到Console端的桌面上,在Vista中,User與Service不會共享同一個Session。如果Service要Popup一個訊息,User會被提示有個來自Session 0的訊息,若要檢視,會先切至黑色背景的特殊桌面,稍後才切換回來。要避免這個問題,可使用Vista另外提供與User Session溝通用的API-WTSSendMessage(), CreateProcessAsUser()。
  4. 應用程式在檢查OS版本請不要限定某個特殊版號,而應做成大於某個版本以上的就OK,例如: 限定XP以上,則2003、Vista也應適用。(XP/2003/Vista可指定用某版本OS相容模式執行程式,解決部分問題)
  5. Vista的UI顯示改為在背景畫好後才顯示出來(沒有OnPaint事件了),減少畫面快速變化時不必要的閃爍,但因此部分如Tooltip、Popup效果會出現邊緣有黑框的狀況。另外,在Vista中,中文字可以不等寬,可能有顯示上的差異。
  6. 高解析度下,Vista會強迫將Icon放大,避免畫面元素難以閱讀,若因此造成與原設計的比例不符,可用SetProcessDPIAware()防止。
  7. 若使用到Access DB File,須為Access 2003 SP1 +,且MS不提供Support,建議用SQL Express SP2。
  8. IIS7變了很多,但我們應該不會用Vista當Web Server,所以這問題可以等Windows 2008再來頭痛。
  9. User Access Control, UAC:
    即使是管理者,平常也是用一般使用者身份執行程式,必要時才升級成管理者權限。而Vista提供了所謂的”資料轉向”,當應用程式企圖寫入資料到%SystemDrive%\Program Files, %WinDir%\System32或Registry HTLM\Software時,Vista會另外將資料存在Virtual Store中(Ex: C:\Users\jeffrey\AppData\Local\VirtualStore\Program Files),避免不必要的權限升級。但是這樣資料會被寫入非預期的地方,對其他User而言看不到。要處理此問題的方法有三:
    1) 避免對這些高權限的資料夾進行更新動作
    2) 在Button指定為高權限動作,Button上會出現小盾牌的Icon,則按鈕時會提示升級權限。
    3) 設定appName.exe.manifest,要求以管理者權限執行。(或在應用程式內容頁上也可設定)
  10.  安裝程式升級至MSI 3.1,避免使用自動更新機制(除了ClickOnce之外)
  11. 不要在程式中檢查使用者是否有管理者權限,答案將永遠為False。
  12. 檢測相容性的工具: Application Verifier!
    可以監控特定程式執行過程中發生的錯誤,有助於判別是存取何目錄、何Registry時出了問題。
    PS: 另外它還有一個很有用的功能,可以模擬RAM、CPU極度不足的執行環境。
  13. UAC分析專用工具: Standard User Analyzer
    架構在Application Verifier上,專門用來分析UAC相容問題。
  14. Application Compatibility Toolkit:
    可以透過Shim的方式修正檔案、Registry、INI、Token、Namespace、Process上不相容的問題,但權限、其它元件方面的問題則沒救。Shim原指用來塞住縫隙的小木片,它可以欺騙應用程式,使之察覺不到不相容的問題。不過Shim只有MS可以開發,一般User將不相容問題透過ACT上傳至MS的Issue資料庫,等待MS發了佛心針對它開發Shim。
  15. MS會維護一份不相容清單,當你執行不相容程式時,會跳出警告視窗,並提示你連上網站取得修補程式(有時根本沒有修補程式)。若想眼不見為淨,可以更動%WinDir%\AppPath下的檔案,也可以用sbdinst.exe安裝自訂的不相容警告。
ClickOnce Rocks!!!

前幾天千辛萬苦把一個WinForm專案從1.1拉拔到2.0,含淚播種的,必歡呼收割,今天我就已經開始享受2.0所帶來的另一項便利功能---ClickOnce!

早在VS 2005上市時就知有這項功能,但因為過去都以Web Application開發為主,也沒時間嘗試。今天實地測試了一下,哇靠! 真是簡單到爆、方便到不行,真他X的一整個好用! (對不起,失態了,實在是太興奮了)

原本以為要用ClickOnce得另外加一個Setup Project再設定一些東西才會Work,沒想到比想像中的還簡單,只需在專案上按右鍵,選Publish(公佈),輸入IIS URL,決定是否可以離線使用即可。


接著VS 2005會建立一個安裝程式用的Web Page, 使用者只需按下網頁上的連結就會完成程式的安裝:

當然,ClickOnce的精髓在於日後有新版本時,程式能自動更新,所以當你執行程式時,會先出現一個檢查更新版本的進度畫面。(你也可以設定等程式跑起來才檢查更新,這樣還可以指定多久檢查一次更新,不必每次Check)

如果有新版,會提示使用者是否要安裝新版本?

不過呢,身為歷盡滄桑的中年人,深知看過小程式的漂亮Demo後只能高興十分鐘,接下來要仔細考量它在實務應用上的可行性。(就像你永遠不會知道購物頻道裡那把可以削椰子殼、直接刨冰塊的霹靂無敵菜刀,是不是用不到一個月就鈍到連切豆干都會牽絲?)

我用手上的專案做了測試,包含Reference 3rd Party Control、額外附加檔案、EXE中使用TCP/IP... 測試OK,看起來ClickOnce的第一步是成功的。我打算使用它做為對內Beta測試時的版本更新用途,將來待更了解後看看可否有進一步的應用,Anyway,ClickOnce Rocks!!

中文編碼解析工具 Ver 1.3

上回發表中文編碼解析工具 Ver 1.2後,網友Esther建議再加上GBK-Q, GBK-B的編碼解析。

說實在話,我沒聽過這兩個名詞,但Google了一下馬上晃然大悟,原來指的是我們常在Mail Subject或收件寄件人資料上看見的那種編碼。格式像這樣:
=?x-gbk?q?=B5=C4=B7=AD=D2=EB?=

怎樣,一Show出格式,大家都很有感覺了吧? 這Mail Server為了克服Mail Header只支援7bit字元所想出來用在底層的編碼法,卻常因為Mail Server、Mail Client間的默契不足沒被順利解碼還原露了餡,赤裸裸地在郵件軟體上顯示出來。以前挺常遇到的,但這些年來,郵件伺服器及軟體漸漸聰明了,就不太常再出現。有網友提出表示這還是某些人的痛,而我用Google找了一下,也還看見不少,索性在中文編碼解析工具加入這項新功能(目前只支援Big5與簡體中文,應該夠用了),推出Ver 1.3版,有需要的朋友可以下載回去玩玩。

程式下載

【茶包射手專欄】艱辛的.NET 2.0之路

手上有個專案是承接了前人版本修改而成的,原本的專案是以.NET 1.1開發,用了不少自訂UserControl提高共用性,其中還應用了不少進階的寫法(甚至滿Hacking的),例如在Constructor中向MainForm Instance註冊,在Dispose中移除註冊等等。

由於我已經改用VS 2005很長一段時間了,回頭用VS.NET 2003寫了幾個星期,曾經滄海難為水,愈寫愈不是滋味,其中幾個我最不習慣的地方包含:

  1. 同時開啟多個Class編輯時,IDE只能顯示其中五六個Tab,畫面之外的只能用向左向右去找Tab,再不然要從選單的視窗清單中去選。相形之下,VS 2005在可以直接下拉顯示所有編輯中Class的清單,實在貼心。
  2. 寫Code輸入變數時,不會自動提示(Intellisense)區域變數名稱,常要向回捲去找。至於Class Members,也得也輸入"this."之後才有得提示。
  3. 由於是修改原有程式,動一個Method之前要先知道有多少地方呼叫到它。在VS.NET 2003中只能用"Go to Reference"一個個找,VS2005的Find All References就顯得價值連城。
  4. .NET 1.1沒有List<T>, Dictionary<T, T>的Generic可用,用ArrayList跟Hashtable每次都要轉型別,煩。
  5. 由於專案挺複雜的,每次啟動都要很久,少了VS 2005 Debugging Edit And Continue的功能,讓我在不斷修改->重新Debug間多白了好幾根頭髮。
  6. VS.NET 2003跟Office 2007內附的新中文輸入法處得不太好,偶爾在切成中文輸入時,IDE會整個Hang住,必須重開VS.NET 2003。這常發生在寫了數十行Code,最後發了佛心切到中文想寫個註解時,當死了~~~ 大哥,我還沒存檔呀! 好心寫中文註解還遭天譴,情何以堪哪~~~
  7. ... 唉! 罄竹難書 ...

吞忍了幾個星期後,終於忍不住了,狠下心來,想把它翻到.NET 2.0,就可以快快樂樂用VS 2005了。只是代誌不像憨人所想的哈尼甘單~~~

前面提了,原有專案用了一些進階的UserControl設計觀念,所以專案用VS 2005開啟後自動升級,轉換後的專案表面上可以執行,但苦難才開始:

幾個1.1版的3rd Party元件在2.0跑起來的顯示有破圖的現象,這在設法取得2.0版元件後獲得解決。

接著,我發現一個引用自訂UserControl的Form設計頁面,在開始Debug或關閉該設計頁面時,會出現可怕的錯誤導致元件整個設計頁面壞掉,訊息內容是Resize事件引發了Null Reference Exception,看來是UserControl的Designer(控制UserControl在IDE顯示樣子的部分)出了問題,但我沒有頭緒。反覆找了好幾天,除了確認UserControl是導致出錯的原因,一直沒什麼起色,我已經灰心到想放棄2.0,乖乖回去用1.1了。

今天早早起床決定再做垂死的掙扎,經過一夜的沈澱,思緒格外清晰,我注意到每次UserControl導致IDE出錯後,後面接連編輯了幾個其他Class功能都正常,但在關閉VS 2005時卻會發生整個VS 2005 Crash的情況,VS 2005會說它已產生了Memory Dump可送回MS分析並重新啟動程式。

為什麼UserControl的錯誤發生後,可以繼續編輯其他程式相安無事,卻在VS 2005關閉時引發致命錯誤呢?

反覆思索這個問題。有了!!! 是Garbage Collection機制嗎? UserControl的Designer物件在面頁關閉時並未真的結束,直到程式關閉時才觸發了Dispose? 而Dispose中的程式碼引發致命錯誤?

不確定自己的想法是否是正確,但驗證的方法很簡單,把Dispose中額外加入移除對MainForm Instance註冊的Code Remark掉,VS 2005就不當了,BINGO!!!!

證實了問題所在,要解決只是彈指的功夫! (記得國父御用水電工的故事嗎? 敲一下1元,知道敲哪裡99元。) 只需在Dispose Code前加上if (this.DesignMode)的判斷,避免在Designer階段觸發該段邏輯即可。(但很奇怪,同樣的寫法在VS.NET 2003中並沒有問題,或許是VS 2005這方面的容錯較低)

就這樣,花了近兩天的時間,逐步接近問題的核心,最後先從煩躁中抽身,冷靜思考再加上對.NET的基本知識(Garbage Collection & Dispose),終於柳暗花明。再會了VS.NET 2003,VS 2005我來了!!

KB-About .NET Framework 2.0 Compatibility

說出來不怕大家笑話,我一直搞不清楚一件事: .NET 2.0支援"向前相容"嗎?
(Backward Compatibility,雖然大家口語都習慣稱"支援以前的版本"叫"向前相容",但Backwards翻成向前有點怪,後面我會沿用MS的翻譯,稱為"回溯相容",以免混淆)

換句話說,.NET 1.1 Build出來的程式可以直接在.NET 2.0中跑嗎?

過去我對VS 2005的ASP.NET Project Upgrade Wizard的無痛升級十分讚賞,其中有一點很神奇的是,就算ASP.NET 1.1的專案參照了.NET 1.1開發的DLL檔,在變身成ASP.NET 2.0 Project後,馬照跑、舞照跳,順得很~~ 當時猜想其中說不定有什麼複雜的機制,找過Google並沒看到什麼相關的介紹(應該也是找得不夠用力),就很鄉愿地知其然不知其所以然傻傻活到今天。

這兩天在Vista上測試一個.NET 1.1開發的Window Form,無意間揭開了這個謎團...

我發現我的Window Form(.NET 1.1)程式在Vista上執行時,畫面顯示有點差異,跟曾經把專案轉到VS 2005裡跑時看到的一樣。仔細一查,原來我的Vista只有裝.NET 2.0,沒有裝.NET 1.1,所以Vista用.NET 2.0的Runtime在跑我用.NET 1.1 Build出來的WinForm exe。這下算是親眼見證了.NET 2.0的回溯相容能力,確定用.NET 2.0直接執行.NET 1.1 Build出來Binary Code是OK的(精確一點來說應該叫MSIL Code)。

MS官方文件裡有篇詳細的介紹,MSDN Magazine則有一篇.NET RD開發這一段功能的心路歷程

我歸納的心得如下:

  1. .NET Framework 2.0提供Side-by-Side(SxS) Execution及Backwards Compatibility來解決與1.1的相容問題。
  2. 在同時安裝1.1及2.0的作業平台上,.NET 1.1 App出來的程式會用.NET 1.1 Framework,.NET 2.0 App會跑2.0。(SxS)
  3. 在只有安裝2.0的作業平台上,.NET 1.1 App與.NET 2.0 App都會用2.0來跑。
  4. 原則上,.NET 1.1 App在.NET 2.0 Framework上執行時,絕大部分的功能都應正常無誤,但非100%,例如 我就遇至UI呈現結果不同的狀況。(Backwards)
  5. 若透過非.NET程式去啟動.NET程式時(最簡單的例子: 用.NET寫元件,包上COM Callable Wrapper, CCW變成ActiveX Control,提供給ASP呼叫),則會用最新的.NET Framework版本執行。
  6. 如果你想限制你的.NET 1.1 App只能用.NET 1.1 Framework跑(通常是用2.0跑會有問題又不想改Code時,呵),則可以利用config檔中的startup/supportedRuntime加以限定。
摔車!!!

鬼月的最後一天,一如往常早上趕著上班,騎著車在萬芳社區的山路奮力向上衝。忽然一陣搖晃,感覺後輪失去動力,車身不聽使喚地扭了幾下;我試著剎車穩住車身,但無濟於事,最後龍頭打橫,人就狠狠地趴向地面(所幸載的是全罩式安全帽,各位同學,安全帽一定要買全罩式的哦!)。回過神,發現自己的右腳被壓在車下,包包飛出去,裡面的文件差點散了一地。我趕緊將車身扶高一些以便"抽腿",此時後方一位好心的騎士朋友停下車來,幫我把車扶起來推到路邊,並詢問我的狀況,我也開始站起來收拾散落的文件、背包,並檢查受傷狀況。右手腕爆痛,手掌無法出力,應該是扭到了;著地的右側西裝口袋及褲腳磨得破破爛爛,右腳踝的襪子破了幾個洞,拉下襪子可以看到左右兩側各有幾個傷口,正滲著血,但看來只是擦傷。

好心的騎士說他在後方看著我滑倒,並指著馬路中央一條暗色的痕跡,懷疑可能是油漬之類的。此時他詢問是否需要幫我叫車去醫院,我判斷了一下,自己的行動能力還OK,謝過了他,離家還不遠,先回家再說。

重點來了,車丟著還是騎回去? 車子除了右剎車桿摔到翹起來之外,看起來OK,右手痛得凶,但幸運的是虎口還可以勉強夾著油門,一路慢慢地"龜行"回家。

回到家,把正趕著送女兒上學的老婆嚇了一大跳,由於傷勢不太重,就換了短褲拖鞋先自己坐計程車去看急診,等會兒她再來會合。

生平第一次自己掛急診,傷檢分類員問完"什麼事? 哪裡痛?"之後就指示我去外科診療室外等候。第一次有機會就近觀察急診室,這才發現除非命在旦夕,急診室只是另一種隨時可掛的門診,排隊、等叫號的程序一樣不缺。等了快十分鐘才才輪到我,很奇怪的是,急診室的醫生們一聽到是摔車都要問清楚是自己摔還是被撞,還在我的診單上註明"騎車自摔",即使一再強調地上有油他們也不鳥我,早知就想一個好一點的理由,什麼看到飛碟、試圖阻止要攻擊小白兔的老鷹、想搶救掉在地上的國旗之類的(小朋友不要笑,在我們那個年代,為了救國旗受傷的英勇事蹟,絕對有資格變成國小國語課本教材哩! 運氣再差(好)一點的,說不定有機會入祀忠烈祠),反正死無對證。回答醫生痛處所在後,一個短髮小女生(我不確定是護士還是醫生,他們都穿一樣的衣服)熟練地用生理食鹽水洗傷口,再用棉花棒沾優碘藥膏豪邁地"擦"傷口,痛得我呲牙咧嘴,最後蓋上紗布,以膠帶固定傷口處理就大功告成。下一步是沿著地上的"黃線"去照X光片。我這才注意到,急診室的地上有黃、紅、藍、綠各色的箭頭線,分別標著X光、藥局等不同目的地,好個顏色管理的實踐。醫生看過手跟腳的X光片,確定骨頭沒有受傷,急診的責任就結束了。

下午小睡了一下,右手開始痛了起來,家裡附近有間生意很好的中醫診所,有在看跌打損傷,這回經西醫驗證骨頭無傷,倒可以放心地去看看中醫,體驗一下中國人數千年來的神奇醫學成就。這輩子還沒看過中醫喬骨頭,在刻板印象裡應該會痛到得多帶一條內褲備用,搞得我在排隊候診時,心中忐忑得很,不過診所的醫生跟推拿師都是年輕人,出奇地斯文親切,跟腦海中仙風道骨不苟言笑的老中醫形象大異其趣。醫師沒有用想像中迅雷不及掩耳的速度把我的手掌轉成90度,而是由輕漸重的拉扯無名指跟小指,並不斷詢問會不會痛來判斷病況。接著最神奇的部分來了,推拿師要我伸起腳來,我以為是要看右腳的外傷,沒想到他要的是左腳,接著在左腳踝下方的一個位置(穴道)壓按搓揉,再問: 手指有沒有好一點? 說也奇怪,右手無名指跟小指的靈活度還真的變高,張握之間的疼痛感也下降了,也不知是心理作用還點穴法真有奇效。總之整體的看診感覺很不錯,蓋上帶有輕微清涼感的中藥敷料包紮了一晚上到睡前拆掉(初次使用怕過敏,所以要控制時間),隔天起來右手已經可以恢復到使用五指打字及使用滑鼠(謎之聲: 你夠了沒? 連手傷的恢復指數也得跟電腦有關嗎?)

這次意外的損失包含: 一條褲子毁損、急診及後續診療費用、請病假扣薪、工作進度延誤、身體疼痛及家人擔心(無價),而右手受傷也粉碎了我加入大聯盟的夢想(免費)... 可謂損失慘重。

下圖就是馬路滑壘時所著的戰袍,口袋跟褲腳破爛到令人心驚,口袋的災情應該源自一口袋的零錢跟鑰匙圈,金屬製的鑰匙圈甚至當場分家,可見當時力道之猛。人能平安真是萬幸,在此提醒每天在外辛苦奔波的朋友們,每天都要快快樂樂出門,平平安安回家哦!

【茶包射手專欄】Cassini's Response Header Encoding

先前有一篇文章討論ASP.NET如何正確傳回中文下載檔名,網友帆歷經九九八十一難後,終於修成正果,還揪出一隻鬼 --- VS 2005內建的ASP.NET Development Server似乎不支援HeaderEncoding!

為了解開這個謎團,茶包射手再次整裝出發!

這回我們使用的辦案工具是HttpWatch Pro(原因是在我的Vista上Fiddler啟動時的UAC升等視窗都會被埋掉,啟動後會導致IE7 Hang住,最要命的是它抓不到我存取Localhost的記錄,怕明明要射茶包變成修理小提琴,我很有魄力地立刻換了武器),用HttpWatch的錄製封包功能,Dev Server與IIS7的Header差異馬上現形!

ASP.NET Development Server

HTTP/1.1 200 OK
Server: ASP.NET Development Server/9.0.0.0
Date: Sat, 08 Sep 2007 08:07:38 GMT
X-AspNet-Version: 2.0.50727
content-disposition: attachment;filename=銝剜?瑼?.xls
Cache-Control: private
Content-Type: application/octet-stream
Content-Length: 16384
Connection: Close

IIS 7

HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 16384
Content-Type: application/octet-stream
Server: Microsoft-IIS/7.0
X-AspNet-Version: 2.0.50727
content-disposition: attachment;filename=中文檔名.xls
X-Powered-By: ASP.NET
Date: Sat, 08 Sep 2007 08:08:21 GMT

我發現ASP.NET Dev Server傳回的Header中,明明要求用Big5 Encoding,但傳回的卻是亂碼,而依多年來跟中文亂碼打交道的經驗,直覺上像是UTF-8被解成Big5(長度變長,而且有問號)。我寫了兩行Code驗證! Bingo! 就是UTF-8被硬轉成Big5。

由這個現象來推斷,ASP.NET Dev Server並未依我們的要求,在Header區使用Big5 Encoding,而是用UTF-8囉?

要確證這一點,得靠Source Code,但沒有ASP.NET Dev Server的Code怎麼辦? 用Reflector? 可以。但我想到另一招,ASP.NET Dev Server的前身是一個Sample專案Cassini,所以我們可以找它爸爸驗一下DNA吧!

在我的機器上把Cassini Sample Web Server架起來,第一件事先確定是不是有其父必有其子? 下載中文檔名也會失敗?

答案是!!! Cassini傳回的Response Header與ASP.NET Dev Server除了Server Name外,完全相同。

Cassini

HTTP/1.1 200 OK
Server: Microsoft-Cassini/1.0.0.0
Date: Sat, 08 Sep 2007 08:02:56 GMT
X-AspNet-Version: 2.0.50727
content-disposition: attachment;filename=銝剜?瑼?.xls
Cache-Control: private
Content-Type: application/octet-stream
Content-Length: 16384
Connection: Close

追蹤了程式碼,我在Cassini.Connection Class中找到了答案,在Cassini的邏輯中,並不會因為指定Response.HeaderEncoding而變更編碼方式,一律用UTF-8解成byte[]後送至Socket,而在IE端則是以Big5去解析它,符合先前的實驗與分析。Case Closed!

private static String MakeResponseHeaders(
int statusCode, String moreHeaders, int contentLength, 
bool keepAlive) {
    StringBuilder sb = new StringBuilder();
    sb.Append("HTTP/1.1 " + statusCode + " " + 
     HttpWorkerRequest.GetStatusDescription(statusCode) + "\r\n");
    sb.Append("Server: Microsoft-Cassini/"
        +Messages.VersionString+"\r\n");
    sb.Append("Date: " + DateTime.Now.ToUniversalTime().
        ToString("R", DateTimeFormatInfo.InvariantInfo) + "\r\n");
    if (contentLength >= 0)
        sb.Append("Content-Length: " + contentLength + "\r\n");
    if (moreHeaders != null)
        sb.Append(moreHeaders);
    if (!keepAlive)
        sb.Append("Connection: Close\r\n");
    sb.Append("\r\n");
    return sb.ToString();
}
 
public void WriteEntireResponseFromString(
int statusCode, String extraHeaders, String body, 
bool keepAlive) {
    try {
        int bodyLength = (body != null) ? 
            Encoding.UTF8.GetByteCount(body) : 0;
        String headers = 
            MakeResponseHeaders(statusCode, extraHeaders, 
              bodyLength, keepAlive);
        _socket.Send(Encoding.UTF8.GetBytes(headers + body));
    }
    finally {
        if (!keepAlive)
            Close();
    }
}
More Posts Next page »

Search

Go

<September 2007>
SunMonTueWedThuFriSat
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456
 
RSS
【工商服務】


BlogLook Score and Rank

Syndication