2012年4月11日

[Mac] WinShortcutter 讓 Mac 悠遊 Windows 的網路芳鄰

WinShortcutter 這玩意兒太讚了,竟然沒有中文的介紹,實在可惜。
首先先前情提要一下,
自從我把 Notebook 換成 MBA 後,日常作業中,最讓我困擾的問題就是,
沒有辦法很自在的快速切換到別台 Windows 的共用資料夾,
雖然可以
在 finder 下按 Command + k ,然後輸入 smb://yourWinServer/sharedFolderName
但如果是在文件中看到的是長這樣的路徑
\\yourWinServer/sharedFolderName
那就要很辛苦的把 "\" 換成 "/" 如果很多層的話那心情就會很糟。
所以我一直在尋找解決方案,
終於皇天不負苦心人,讓我找到了 WinShortcutter 這個法寶,
安裝過程一般都沒什麼問題,安裝完成後,它會請你重新開機,
這時候別不信邪,因為服務要更新,所以請重新開機,
開完機後(其實安裝完後就可以看到這個改變了),
回到 finder 中就可以看到,原本 Mac 不認識的 *.lnk 的圖示變了,


雙擊後就會有神奇的效果出現 ... 是的 ... Mac 就學會了如何 follow the link。
當然,神奇的還不止這樣,
接下來要請你跟我這樣做(下面的動作請在重新開完機後做,不然會找不到服務),

首先,打開你的 Finder -> 服務 ->  服務偏好設定(如下圖)

在服務偏號設定 ->服務 裡面尋找
Copy Path to Clipboard 及 Open as Windows Link 並把他勾起來


接著就可以用 選取文字 -> 右鍵 的方式,在選單 -> 服務 -> Open as Windows Link

在 Finder 裡,則是可以 選取檔案或資料夾 -> 右鍵 -> 服務 -> Copy Path to Clipboard

複製出來的檔案路徑如果要把 "/" 變成 "\"  可以在

系統偏好設定 -> WinShortcutter 中,把 Use Backward Slashes 勾起來即可

*.lnk 也可以用快速查看,查看捷徑內容



是不是很方便呢!


WinShortcutter 網址:這裡

2012年4月2日

[.Net] Communication between the host page and the user control

紀錄一下,
我原本有一支 Web 程式,畫面像圖中上面那樣,
有幾個欄位、一個 Grid 、當 Grid 點選不同紀錄時,
下面的 Tab 內容就會依選中的資料,重新繫結資料內容。
這支程式暫且叫他 HostPage ,
他會 Implement 一個 IHost 的 Interface ,
Usercontrol 若要存取 HostPage 中的資料,一律透過 IHost 進行。
而每個 Tab 中的內容則是鑲入 Usercontrol ,
該 Usercontrol 會 Implement 一個叫 Itabcontent 的 Interface ,
HostPage 要操作 Usercontrol 時,也一律透過 Itabcontent 進行。

在開發的過程中 Tab 內容越來肥,導致整個頁面載入都明顯的變慢許多,
於是我們想把 Usercontrol 鑲到一個無縫的 iframe 中,讓他看起來像下圖下面那樣,
因為一開始的設計就透過 Interface 將 HostPage 與 Usercontrol 分開,
所以變成下面鑲入 iframe 的過程,沒有多大的困難,
原本的 Usercontrol 改成一支 Usercontrol_Relay , Relay 網頁中放一個 iFrame ,
Relay 則是當需要的時候把相關資訊 post 給 iframe 中的 AnotherHostpage ,
而 AnotherHostpage 則是把從 Relay 接收到的參數轉換成 IHost 定義的屬性,
並依參數決定要鑲入的 Usercontrol 是哪一個,讓原本的 Usercontrol 改放在 AnotherHostpage 中。
當然,Usercontrol_Relay 要 Implement Itabcontent
AnotherHostpage 也要 Implement IHost。


[.Net] 如何取得 GAC 中的 Dll

有時遇到 GAC 中 Dll 版本有問題的時候,
我們需要把 GAC 裡的 Dll 拿出來看看,
但我們沒辦法用檔案總管來幫我們完成這件事情,
下面提供幾個 Command Line 指令,
讓我們快速找到 Dll 在 GAC 中的位置。

如下圖,假設我們要找 GSS.Stirrup.NBase20

先用 cd 指令移到 assembly 目錄,這個目錄會在 windows 的安裝目錄中,
接下來用 dir 指令去尋找相關的檔案,
下面指令中 * 表示萬用字元, /s 表示遞迴尋找其下的子目錄,
| 比較特別,因為 /s 出來會有一堆目錄的資訊,這邊把尋找到的內容導向 find 指令,
讓 find 在 dir 出來的文字中找含有 %windir% 內容的行,
而 %windir% 則會被置換成系統環境參數中設定的 windows 目錄。
cd /d %windir%\assembly
dir GSS.Stirrup.NBase20* /s |find "%windir%"

如果已經知道要找的 Dll 完整檔名,可以用下面的指令
這個指令會搜尋目前所在目錄下所有子目錄中是否有存在(...)中指定的檔案,
若有則 echo 印出來
@for /r . %f in (GSS.Stirrup.NBase20.dll) Do @if exist "%f" echo %f


找到檔案後,當然就是把他 Copy 出來,至於 Copy 的指令應該不需要多加說明
copy [source] [destination]

2012年3月29日

[Web Develop] Debug 小技巧

紀錄一下小小的 Debug 心得
俗話說工欲善其事,必先利其器,
所以我們就先從工具說起,
首先是在開發 Web 程式時,最常用的網頁開發工具
firefox - 以 firebug 為例 (http://getfirebug.com/wiki/index.php/Main_Page)

IE (http://msdn.microsoft.com/en-us/library/hh772704(v=vs.85).aspx)


Chrome (http://code.google.com/intl/zh-TW/chrome/devtools/)




以上三種 Browser 中的 Developer Tool 介面上差異頗大,但其實內容大致上相同,
大致上可以分成
Console(IE譯作主控台)、HTML、Javascript、Network

Console 的部份在之前的文章中有提到,
我們可以利用 console.log(...) 、 console.info(...) 、 console.error(...) 等方法,
將偵錯的資訊印出來,顯示這些訊息的地方就是 Console ,
通常在 Console 中也允許我們用 Command line的方式輸入 Javascript 做運算。

HTML 的部份則是協助我們,將網頁上的元素轉換成他在 HTML 中的位置,
相關的屬性、CSS Style 等等,大都可以在這個地方找到。

Javascript 的部份,通常都允許我們在這邊做 Javascript 除錯,可以下中斷點,
觀察 Javascript 運作的情形。

Network 則是紀錄 Browser 發出什麼 Request 、 Web Server 做出什麼回應,
花了多少時間、以什麼方式傳遞...等資訊。

對這些工具有了大概的瞭解之後,來看看平常如何運用這些工具。

當網頁出現 javascript error 的時候,
我們可以在 Console 裡看到,所有發生 Error 的程式片段,
用滑鼠點擊這些錯誤時,這些工具通常都可以直接跳到 Javascript 中,
並標示出發生 Exception 的程式碼位置。
這個功能對我們在 Javascript 程式除錯上非常有幫助。

下圖示範,當按下畫面中的 Test Button 時,會 throw 一個 exception 出來,
在 Console 的畫面中就可以看到他指出錯誤發生的地方。


當按下上圖中描述錯誤發生點時,就會跳到 Script 的畫面。


下圖是 Network 的畫面,從這個畫面,
可以看出 Browser 對 Server 發出的每個 Request,
以及每個 Request 回應的狀態方式等等。

從上圖每個 Request 可以再進細項到該 Request 的內容,
包含 Request 和 Response 的 Header 、 Cookies 、及實際傳輸的內容都可以看得到,
下圖是在使用 UpdatePanel 發出 AsyncPostback 從 Server 回應的內容,
仔細看內容很不一樣喔,這是因為 UpdatePanel 是部份更新,
更新的方式是透過 Javascript 將 Server 傳回來的容 Parse 後,
再展現到畫面上,所以他並不是一個完整的 HTML 頁面。

再來看看, ScriptManager.RegisterStartUpScript 丟出來的內容長什麼樣子。 

從上圖可以知道,如果 ScriptManager 丟出來的 Script 有錯的話,
很可能會造成 Asp.Net AJAX framework 底層 Parse 錯誤,
導致 Javascript 無法正常執行,而 DevTool 又無法正確指出有問題的程式碼位置。
有可能發生的情況是, Server 端程式 Throw 出來的 Exception 包含有單引號,
或其他 Javascript 的保留符號,但沒有處理到,
這時候就可以用這種方式,看到 Server 到底是 throw 出什麼 exception。

[Refactoring] 愛他就請在最接近第一次使用他的地方宣告他(變數)

變數宣告是不是一定要在函數的開頭?

在 C 的年代,問這種問題應該會被恥笑到抬不起頭,
沒有人去挑戰,因為 Compiler 就告訴你這樣不行(其實是因為 Compiler 這樣比較好做),
而現在, C 的輝皇年代過去了,後繼的程式語言取消了這個限制,
於是我們再來討論一下這個問題:變數宣告是不是一定要在函數的開頭?

就我認為,沒這個必要,因為他一點好處都沒有,
反而會帶來不必要的困擾,常常會看到那種宣告完了,
就再也不用他的變數,因為你並不是為了用他而宣告他,
而是覺得等一下會用,但寫了幾行程式碼之後,就把他忘得一乾二淨了,
但這並不是最惹人厭煩的,

試從以下這種角度思考:
假設函數有 20 行程式碼,我在第一行便宣告了一個變數,
這代表接下來的 19 行程式碼都看得見他,
即使他們一點關系都沒有,如果這中間又做了什麼處理,
要知道這個變數最終的結果,或者這變數會影響什麼,
你都無法忽略這 19 行程式碼,
那如果是在第 10 行宣告呢?
這表示前 10 行程式和這變數一點關系都沒有,
你只要關心剩下來的 10 行程式,多開心ㄚ。

也許你會說,那全域變數怎麼辦,但,別忘了,我這裡說的是函數中的區域變數,
既然談到了全域變數,那就來探討一下全域變數,
就我的觀點,是能不用就不用,從上面的角度思考,
假設程式有 1000 行,我在第一行便宣告了一個變數,
這表示接下來的 999 行程式碼都可以看到他,
方便是方便,但這也表示如果這個變數有問題,
你得看完這 999 行程式碼,也許你又會說,
可我只有在一兩個地方用到他,所以其實我只要看那一兩個地方,
除非你的程式很小,一般情況,你會很快的忘了你只有在一兩個地方用過他,
更甚者在多人開發的情況下,沒人會知道只在一兩個地方用他,
甚至有人還會參一腳,把他拿去用,而這些情況都會讓你的程式耦合度變高,

所以,我主張,變數的能見範圍,最好是夠用就好,
像 Code Complete 書上說的,在最接近第一次使用他的地方宣告他即可,
別再偏執的把區域變數都集中在一塊宣告了

PS. 全域變數,如果真的要用,那還是集中在一起比較好,
因為你可能會想知道他們各自的初始值是什麼,
但,聽我個忠告,能不用就別再用他會對你比較好。

2012年2月23日

[Refactoring] 去除讓人困惑的巢狀判斷

if 很好用,但遇到一層又一層,一層又一層巢狀 if 的時候常常搞得人昏頭轉向,
今天剛好看到一個很經典的案例,可以做重構案例的分享。

首先重構的第一個步驟,先瞭解原本程式在做什麼。(因為我們並不是要重寫,只是要讓程式以更優雅的姿態呈現),而這也是最困難的一個部分,因為一個需要重構的程式,通常都是因為不容易閱讀,才會需要重構。

所看一下程式的主要目的是什麼:
畫面上有 5 個欄位,分別是開始日期、開始日期的時間、結束日期、結束日期的時間、總時數; checkTimeFormat 被呼叫到的時機是,當相關的欄位 onchange 時就觸發。
function checkTimeFormat() {

    var startDate = new Date(ctrls.idtpCouseOpen_DATE_txtPreBox_txtDate.val());
    var endDate = new Date(ctrls.idtpCouseOpen_DATE_txtPostBox_txtDate.val());
    var startTime = ctrls.itxtCouseOpen_TIME_txtPreBox.val();
    var endTime = ctrls.itxtCouseOpen_TIME_txtPostBox.val();
    if ((!isNaN(startDate)) & (!isNaN(endDate))) {
        if ((!isNaN(startTime)) & (!isNaN(endTime))) {

            if (((parseInt(startTime) < 2359) & (parseInt(endTime)) < 2359)) {

                if ((parseInt(startTime)) < (parseInt(endTime))) {
                    caculateTTLHR(startDate, endDate, startTime, endTime);
                } else {
                    alert("開始時間大於結束時間");
                    ctrls.itxtCouseOpen_TIME_txtPreBox.val("");
                    ctrls.itxtCouseOpen_TIME_txtPostBox.val("");
                    ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                }
            }
            else {
                if (parseInt(startTime) > 2359) {
                    alert("時間輸入錯誤,請重新輸入!");
                    ctrls.itxtCouseOpen_TIME_txtPreBox.val("");
                    ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                }

                if (parseInt(endTime) > 2359) {
                    alert("時間輸入錯誤,請重新輸入!");
                    ctrls.itxtCouseOpen_TIME_txtPostBox.val("");
                    ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                }

            }
        }
        else {
            if (isNaN(startTime)) {
                alert("時間輸入錯誤,請重新輸入!");
                ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                ctrls.itxtCouseOpen_TIME_txtPreBox.val("");

            }
            if (isNaN(endTime)) {
                alert("時間輸入錯誤,請重新輸入!");
                ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                ctrls.itxtCouseOpen_TIME_txtPostBox.val("");
            }

        }
    }
}
以下就程式判斷的邏輯做分析
1. 如果日期欄位轉成日期物件後不是非數值內容,才處理,
    否則什麼事都不做。

2. 如果時間欄位都有輸入非數值內容時,檢查日期、時間輸入是否合邏輯
    否則分別判斷哪個時間欄位非數值,並提示使用者時間欄位輸入錯誤。

3. 承 2 ,如果輸入的時間 < 2359 才判斷日期和時間的起迄關系(起不可大於迄)
    否則分別判斷哪個時間欄位大於 2359 並提示使用者時間欄位輸入錯誤。

4. 承 3 ,如果起時間小於迄時間才計算時數,
    否則提示使用者起迄時間輸入有誤。
    (因為需求的關系,只針對時間的起迄判斷,也就是說日期和時間並沒有關系)

>>> 考慮用 Replace Netsted Conditional with Guard Clauses

接下來就是思考如何重構,
從上面程式來看,最深有 4 層 if ,而最終正確結果只有一個(事情的真相只有一個,唯一看透了真相是一個外表看似小孩,智慧卻過於常人的名偵探柯南 XD),也就是最深那層的判斷,
其他的分支,都是錯誤的情況,且錯誤發生後續都不用再做額外的處理,
這是很典型可以用 Guard Clauses 處理的結構。

1. 我們把第一個 if 拿到最前面 ! 拿掉 & 改成 ||,成立便 return,馬上去掉一層,就語意層面來看也清楚多了(如果起時間不是數值 或 迄時間不是數值 就 離開不處理)。
if ((isNaN(startDate)) || (isNaN(endDate))) return
2. 把第二層的 if 中,else 的部分拿到最前面,於是我們又少了一層 if ,而且少了一個 if 判斷。

            if (isNaN(startTime)) {
                alert("時間輸入錯誤,請重新輸入!");
                ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                ctrls.itxtCouseOpen_TIME_txtPreBox.val("");
                return;
            }
            if (isNaN(endTime)) {
                alert("時間輸入錯誤,請重新輸入!");
                ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                ctrls.itxtCouseOpen_TIME_txtPostBox.val("");
                return;
            }
3. 把第三層的 if 中, else 的部分拿到最前面,結果同 2
                if (parseInt(startTime) > 2359) {
                    alert("時間輸入錯誤,請重新輸入!");
                    ctrls.itxtCouseOpen_TIME_txtPreBox.val("");
                    ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                    return;
                }

                if (parseInt(endTime) > 2359) {
                    alert("時間輸入錯誤,請重新輸入!");
                    ctrls.itxtCouseOpen_TIME_txtPostBox.val("");
                    ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
                    return;
                }
於是我們就初步完成了 Replace Netsted Conditional with Guard Clauses
這樣程式是不是更容易閱讀了呢?(雖然還有重構的空間,不過就先這樣囉)
function checkTimeFormat() {

    var startDate = new Date(ctrls.idtpCouseOpen_DATE_txtPreBox_txtDate.val());
    var endDate = new Date(ctrls.idtpCouseOpen_DATE_txtPostBox_txtDate.val());
    var startTime = ctrls.itxtCouseOpen_TIME_txtPreBox.val();
    var endTime = ctrls.itxtCouseOpen_TIME_txtPostBox.val();



    if ((isNaN(startDate)) || (isNaN(endDate))) return;




    if (isNaN(startTime)) {
        alert("時間輸入錯誤,請重新輸入!");
        ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
        ctrls.itxtCouseOpen_TIME_txtPreBox.val("");
        return;
    }
    if (isNaN(endTime)) {
        alert("時間輸入錯誤,請重新輸入!");
        ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
        ctrls.itxtCouseOpen_TIME_txtPostBox.val("");
        return;
    }

    if (parseInt(startTime) > 2359) {
        alert("時間輸入錯誤,請重新輸入!");
        ctrls.itxtCouseOpen_TIME_txtPreBox.val("");
        ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
        return;
    }

    if (parseInt(endTime) > 2359) {
        alert("時間輸入錯誤,請重新輸入!");
        ctrls.itxtCouseOpen_TIME_txtPostBox.val("");
        ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
        return;
    }

    if ((parseInt(startTime)) < (parseInt(endTime))) {
        caculateTTLHR(startDate, endDate, startTime, endTime);
    } else {
        alert("開始時間大於結束時間");
        ctrls.itxtCouseOpen_TIME_txtPreBox.val("");
        ctrls.itxtCouseOpen_TIME_txtPostBox.val("");
        ctrls.txtTRAIN_CLASS_TOTALHR_EditText.val("");
    }
}

上面重構完之後,經測試發現有問題,
輸入起時間還沒輸迄時間的時候,會說開始時間大於結束時間,
原因在於如果時間欄位為 "" 空字串時, isNaN 會是 false 但是 parseInt 後會變成 NaN,
而在步驟 3 去掉這層 if 的時候,
本以為不是 parseInt > 2359 就是 parseInt < 2359,
結果出現了第三種可能,使得原來的程式不會去執行到開始時間小於結束時間的判斷。
而這個狀況雖然使得原來執行的結果是正確,但程式表達出來的語意卻不是這個樣子。
所以這邊要修正這個 Bug 就在判斷開始時間大於結束時間之前再加上

if (endTime=="" ||startTime=="") return;

最後,這支程式還有一個問題,就是 parseInt 方法 應該要傳入兩個參數 ~~ 才不會有 '08' 的問題 ~~


2012年2月9日

[.Net] Thread Local Storage

Thread Local Storage(TLS),顧名思意就是在 Thread 裡的儲存空間,
在 MultiThread 的世界裡面,Thread 和 Thread 之間本來就井水不河水,
要存取共用的資源時,就要透過鎖定同步等方式,才不會有競速的問題,
TLS 要解決的問題,並不是 Thread 之間資源共享的問題,
而是 Thread 本身資源共享的問題。

假設我們要設計一個 ADODB 物件,
讓程式都透過底層取得 ADODB 物件,再使用 ADODB 物件執行 SQL ,
而 Connection 的事都交給 ADODB 物件處理,
程式只需要
ADODB dbAdpter = ADODBFactory.getADODB();
dbAdpter.execSQL(...);
而 ADODB 物件的設計,會盡量使用同一條 Connection ,
void main()
{
    methodA();
    methodB();
}

void methodA() {
    ADODB dbAdpter = ADODBFactory.getADODB();
    dbAdpter.execSQL("...");  
}

void methodB() {
    ADODB dbAdpter = ADODBFactory.getADODB();
    dbAdpter.execSQL("...");  
}
如上面程式所示,
這邊會希望每次從 ADODBFactory.getADODB() 取出來的物件是同一個,
而 connection 就 keep 在這裡面,
對 ADODBFactory 而言,並不曉得呼叫他的是不是同一個 thread ,
他也不能隨便丟一個 ADODB 回去,如果每次都 new 一個 ADODB 的話,
就變成會開一堆 connection ,即使有用 ado.net 的 connection pooling ,
connection 用量還是很大,更慘的是,如果有 transctionscope 還會被升級成 DTC,
如果 ADODBFactory 能夠從 thread 中取得現有的 ADODB ,
沒有的時候就 new 一個給他,這樣就完美了,
所以我們需要在 thread 中佈置一塊空間,而且讓 thread 外面可以看得到,拿得到,
這時候 TLS 就很好用了,直接就在 thread 上放一個俱名的 Data Slot ,
ADODBFactory.getADODB 就先看目前 thread 裡有沒有存在的 ADODB 物件,
如果有就直接拿,沒有的話就 new 一個給他。
public static ADODB getADODB()
{
    if (Thread.GetData(Thread.GetNamedDataSlot("YourDataSlotID"))==null) {
        Thread.SetData(Thread.GetNamedDataSlot("YourDataSlotID"), newADODB());
    }
    return (ADODB)Thread.GetData(Thread.GetNamedDataSlot("YourDataSlotID"));
}
於是我們就很輕鬆的用 TLS 完成這個任務了。

參考資料:
Thread Local Storage: Thread-Relative Static Fields and Data Slots