2012年12月3日

[iCalendar] 如何用 .ics 檔預約或更新 Outlook 行事曆

細節要參考 RFC2445, 2446, 2447 關於 VEVENT 的設定

RFC 2446:

iCalendar Transport-Independent Interoperability Protocol (iTIP)
specifies how calendaring systems use iCalendar objects
to interoperate with other calendar systems. It does so in a general
way so as to allow multiple methods of communication between systems.
Subsequent documents specify interoperable methods of communications
between systems that use this protocol.

iTIP 在描述的是行事曆與行事曆之間利用 iCalendar objects 的溝通方式,
舉例:
A 行事曆的 User PUBLISH 一個 VEVENT 給 B 行事曆的某個 User。
A 行事曆的 User REQUEST 一個 VEVENT 給 B 行事曆的某個 User 更新先前 PUBLISH 的 VEVENT。
B 行事曆的 User COUNTER (改變的建議)一個 VEVENT 給 A 行事曆的召集人。

而這邊的需求比較簡單,我們只需要統一的一個召集人,發出會通知(或更新)給所有參與人即可,
所以只需要用到 VEVENT REQUEST ,

一個 VEVENT REQUEST 的 .ics 格式如下:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//RDU Software//NONSGML HandCal//EN
METHOD:REQUEST
BEGIN:VEVENT
DESCRIPTION:描述寫這邊
DTSTART:20121225T080000
DTEND:20121225T170000
DTSTAMP:20121129T093257
SEQUENCE:1
SUMMARY:標題寫這邊
UID:guid
STATUS:CONFIRMED
ORGANIZER:mailto:your@mail
END:VEVENT
END:VCALENDAR


基本上如果只要發佈(PUBLISH),就不需要用 ORGANIZER(rfc2446上說需要,但測試結果可有可無),
但 PUBLISH 不能更新原有的行事曆,
如果要更新就要用 REQUEST ,用 REQUEST 就要有 ORGANIZER。
要更新一個原有的行事曆,
必須要注意以下幾點:
1. UID:唯一識別代碼,前者和後者要一致
2. DTSTAMP: 產生 ics 的時間後者要比前者大
3. SEQUENCE: 要遞增,一樣後者要比前者大( SEQUENCE 從 1 開始,往後遞增)
4. ORGANIZER: 要填入符合 email 格式的內容

以上四點在 microsoft outlook 2007 中,2 和 3 有檢查後者一定要比前者大,
第 4 項貝沒有檢查一定要一樣(因為可以變更召集人),
但是一定要有 email 格式的內容,
這裡要注意召集人如果是自己(沒有設召集人,outlook預設是自己)的話,
就沒辦法更新( 這個超重要,試了很久才試出來 XD )。



METHOD 的部份定義在: http://tools.ietf.org/html/rfc2446#page-7

The ITIP methods
Method Description
PUBLISH Used to publish a calendar entry to one or more Calendar Users. There is no interactivity between the publisher and any other calendar user. An example might include a baseball team publishing its schedule to the public.
REQUEST Used to schedule a calendar entry with other Calendar Users. Requests are interactive in that they require the receiver to respond using the Reply methods. Meeting Requests, Busy Time requests and the assignment of VTODOs to other Calendar Users are all examples. Requests are also used by the "Organizer" to update the status of a calendar entry.
REPLY A Reply is used in response to a Request to convey "Attendee" status to the "Organizer". Replies are commonly used to respond to meeting and task requests.
ADD Add one or more instances to an existing VEVENT, VTODO, or VJOURNAL.
CANCEL Cancel one or more instances of an existing VEVENT, VTODO, or VJOURNAL.
REFRESH The Refresh method is used by an "Attendee" to request the latest version of a calendar entry.
COUNTER The Counter method is used by an "Attendee" to negotiate a change in the calendar entry. Examples include the request to change a proposed Event time or change the due date for a VTODO.
DECLINECOUNTER Used by the "Organizer" to decline the proposed counter-proprosal.



另外關於 content type ,截錄一下rfc2447的說明:

2.4 Content Type
   A MIME body part containing content information that conforms to this
   document MUST have an [RFC-2045] "Content-Type" value of
   "text/calendar". The [RFC-2045] "Content-Type" header field must also
   include the type parameter "method". The value MUST be the same as
   the value of the "METHOD" calendar property within the iCalendar
   object.  This means that a MIME message containing multiple iCalendar
   objects with different method values must be further encapsulated
   with a "multipart/mixed" MIME entity. This will allow each of the
   iCalendar objects to be encapsulated within their own "text/calendar"
   MIME entity.


2012年11月8日

[IIS] URL Rewrite 設定備忘

備忘:
我有一個網站,網站內容是純 html、js 組成的,但 js 會呼叫 services 做事,
我們希望 services 可以被 hosted 在別的 server 上,但我們同時也希望,
js 裡面在寫呼叫 services 的事,不需要管 services 的 server 在哪裡。
於是我們在這個網站上,設了一個反向 Proxy 做 URLRewrite ,
把特定的路徑 request 都導向另一台 Server 。

^s/(.*) 表示 s 這個目錄所以有網址都要 rewrite 。
("^"是指一開始,因為後面馬上接 "/",所以就是 "s" 這個目錄,

圖二中 {R:1} 表示上面表示式中 "(.*)" match 到的字串,{R:0}則表示含 "s/"。
(圖一)
(圖二)

參考資料:http://www.microsoft.com/taiwan/technet/iis/expand/URLRewrite.aspx



2012年10月9日

[EasyFlow] 增加TipTop XML 傳來之欄位


原因:
   TipTop 的請購單,備註是放在單身中,USER 反映簽核時不夠明顯,也無法輸入太多字

改善:
1.在 TipTop APMT420 中的自訂欄位,新增一欄備註( pmkud01),設定此欄位要傳到 EasyFlow

2.打開此表單的 EasyFlow XML檔 (路徑 EFNET \ WS \ EFNETService \ TTXMLDoc \ Draw \ apmt420.xml )
       在<Grid name="gr5261" width="52" height="7">的最後插入
     
<Label text="備註" posY="0" posX="69" gridWidth="8" sizePolicy="dynamic"/>
<FormField name="pmk_file.pmkud01" colName="pmkud01" sqlType="VARCHAR(255)" fieldId="999" sqlTabName="pmk_file"
      tabIndex="999">
<ButtonEdit width="10" action="controlp" image="zoom" posY="0" posX="81" gridWidth="12" scroll="1"
      sample="MMMMMM000000" comment="備註[pmkud01]"/>
</FormField>


3. 修改此EasyFlow表單的HTML(路徑 EFNET \ src \ TEI \ TEIAPMT420)
         在TABLE的最下面新增

<tr>
<td width="90" id="pmkud01_1" nowrap="true" align="right" class="normalFormCaption">備註</td><td nowrap="true" width="100">
<table style="margin:0px;padding:0px;" border="0" cellspacing="0" cellpadding="0">
<tr>
<td><span id="pmkud01_2" class="normalSpan"></span></td>
</tr>
</table>
</td>
</tr>

成果:



[EasyFlow] DropDownList 下拉式選單的加工

加工一:在SQL Command加上其他較複雜的SQL語法

原因:
      因為在 EasyFlow 表單設計中的DropDownList選單中的 SQL Command 無法用 SQL 的
      ORDER BY  or HAVING 之類的語法, 所以必須手動加在 CS 檔中.
作法:  
      找到 ” #region 重新取得 select selectXXXXX下拉選項 ”,修改 string tSQL 後面的  SQL 語法



#region 重新取得selectcategory2下拉選項 
[AjaxPro.AjaxMethod(AjaxPro.HttpSessionStateRequirement.ReadWrite)]  
public string ajaxGetselectcategory2Item(string pselectcategory1Val)  
{  
 string strReturn = ""; string tSQL = @"select distinct * from LMIT where FormCode='表單名稱' 
        and ItemNo='1' and Category=N'" +  pselectcategory1Val + "' order by 00";
 strReturn = createEasyFlowDataTable(tSQL, "Details", "Details");  
 return strReturn;  
}  
#endregion 重新取得selectcategory2下拉選項 

加工二:DropDownList 第一個選項是空白

作法:   
  要刪除這個空白的話,在if (!IsPostBack)中,找到這個下拉式選單,註解掉this.selectXXXXX.Items.Insert(0, new ListItem("", ""));
  在JS檔中的FunOnChange_ selectXXXXX 註解掉CreateOption("","請選擇", selectXXXXX DDL);即可

2012年10月8日

[EasyFlow] 自製 TipTop 放行單



原因:
         在 TipTop AXM520 中,沒有信用查核放行的單子可拋轉至 EasyFlow 簽核,且因 EasyFlow 版本為 .net ,鼎新將 CS 檔包成 DLL 檔了,沒有 Code ,故無法在出貨單拋轉至 EasyFlow 簽核完成時回寫 TipTop .
             
 故現行公司的做法是:
         需要放行時由相關人員填寫紙本的信用查核放行單,依核決權限簽核後,交由會計至下面欄位勾選允許放行.

流程改善:
1. 當需要放行時,負責的人員至 EasyFlow 開立信用查詢放行單,依核決權限簽核,
         當簽核完成後,更新 TipTop DB OGA903 欄位(改為Y)





    防呆:
a)送出前檢查: 須為出貨單 / 確認碼不為X:作廢 / 狀況碼不為9:作廢 / 信用額度查核放行不為Y / Gridview裡面的單號須為同一客戶且幣別相同
b)簽核完成後檢查:流程中是否有會計部門的員工
        c)輸入出貨單單號自動帶出該訂單之金額
3.    <!--[endif]-->檢查後帶出相關資料:
a)客戶編號 / 客戶名稱 / 授信條件 / 付款條件 / 幣別 / 本幣金額
b)計算出原幣金額(單價*數量*匯率)
c)檢查通過才會出現送出的按鈕,檢查不通過會跳出視窗題是哪邊錯誤


#region 簽核流程結束時要處理的事情
protected override void AfterApprove()
{
if (this.BlnCaseClosed && this.AryFlowProperty.SerialSignResult == "2")
{
   //先判斷流程中是否有財會部的人
   string sql_EFdept = @"select * from resdd as D
        where resdd001='表單TABLE' and resdd002='"+this.StrSheetNo+"'
              and exists (
                select '"+"x"+"' from  resak X where X.resak001 =
                    D.resdd008 and (X.resak015 = 'FN3000')
             )";
DataSet set_EFdept = this.m_objDB.CreateRecordset(" use EFNETDB;" +
                   sql_EFdept, this.Session["strProcID"].ToString());
  DataTable temp_EFdept = set_EFdept.Tables[0];
  if (temp_EFdept.Rows.Count == 0)
  {
    //把ERROR存成TXT檔
  }
  else
  {
    StringBuilder builder = new StringBuilder();
    builder.Append(" use EFNETDB;");
    builder.Append(" select saleno ");
    builder.Append(" FROM 表單TABLEb ");
    //表單代號,表單編號
    builder.AppendFormat(" WHERE 表單TABLEb001='{0}' ", this.formID);
    builder.AppendFormat(" AND 表單TABLEb002='{0}' ", this.StrSheetNo);
    DataSet set_saleno=this.m_objDB.CreateRecordset(builder.ToString(),this.Session["strProcID"].ToString());
    DataTable temp_saleno = set_saleno.Tables[0];
    foreach (DataRow saleno_read in temp_saleno.Rows)
    {
       string sql_OGAFILE = @"UPDATE OPENQUERY(ORACLE,'
               select oga903 from TT.oga_file where oga01=''" + saleno_read["saleno"] + "''') SET OGA903='Y'";                      
this.m_objDB.ExecuteSQL(sql_OGAFILE.ToString(),this.Session["strProcID"].ToString());
     }
   }
 }





#region 按下檢查時要做的事情

protected void butcheck_Click(object sender, EventArgs e)
{
    //先清空textbox
    
    
    base.BtnCreateToolSendForm.Attributes.Add("style", "display:none");//隱藏送出的按鈕
    int rows_count = MasterObj.DetailObjs[0].NewRows.Count;//取得grid有幾列資料
    System.Text.StringBuilder sb = new System.Text.StringBuilder();
    string customer_1 = "";//客戶名稱
    string customer_no = "";//客戶編號
    string terms_payment = "";//付款條件
    int customer_i = 1;
    string currency_1 = "";//幣別
    double local_currency = 0.0;//本幣未稅金額NTD
    double org_currency = 0.0;//原幣未稅金額
    double credit = 0.0; //授信額度
    double rate = 0.0; //匯率
    string txt_saleno = "";//銷貨單號
    bool bPass = true;
    if (rows_count == 0)
    {
         MessageBox.Show("請先輸入訂單編號");
    }
    else
    {
        foreach (DscRow dscRow in MasterObj.DetailObjs[0].NewRows)
        {
            string sqlstring_TT = @"Select * From OPENQUERY(ORACLE, '
            select oga01 as 銷貨單號,oga03 as 客戶編號,oga032 as 客戶名稱,oga903 as 是否放行,oga55 as 狀況碼,
                    ogaconf as 核准碼,oga09 as 單據別,oag02 as 付款條件,oga24 as 匯率,
                    oga50 as 出貨金額,OCCG03 as 授信額度,oga23 as  幣別,oga50 as 原幣未稅金額,(
                              select sum(omb16) from TT.omb_file  
                                    where TT.oga_file.oga01=omb31
                            ) as 本幣未稅金額
            from TT.oga_file join TT.oag_file on oga32=oag01
                 join TT.occg_file on OCCG01=oga03
            where oga01=''" + dscRow["saleno"].Value.ToString() + "''')";

           DataSet set_TT = this.m_objDB.CreateRecordset(" use EFNETDB;" +sqlstring_TT,this.Session["strProcID"].ToString());
           DataTable dtTemp_TT = set_TT.Tables[0];
           if (dtTemp_TT.Rows.Count == 0)
           {
               sb.Append("查無'" + dscRow["saleno"].Value.ToString() + "'的訂單資料,請檢查訂單號碼是否錯誤\r\n");
                bPass = false;
          }
          else
          {
              foreach (DataRow s_read in dtTemp_TT.Rows)
              {
                if (s_read["單據別"].ToString().Trim() == "1")
                {
                  sb.Append("單號:" + s_read["銷貨單號"] + "是出貨通知單喔~請再確認\r\n");
                  bPass = false;         
                }
                else if (s_read["核准碼"].ToString().Trim()=="X")
                {
                  sb.Append("單號:" + s_read["銷貨單號"] + "核准碼是作廢'X',請再確認");
                  bPass = false;
                }
                else if (s_read["是否放行"].ToString().Trim()!= "N")
                {
                  sb.Append("單號:" + s_read["銷貨單號"] + "已放行,請再確認.\r\n");
                  bPass = false;
                }
                else
                {
                  if (customer_i == 1)
                {
                customer_1 = s_read["客戶名稱"].ToString();
                currency_1 = s_read["幣別"].ToString();
                double x = 0.0;
                bool flag_1 = double.TryParse(s_read["匯率"].ToString(), out x);
                bool flag_2 = double.TryParse(s_read["原幣未稅金額"].ToString(), out x);
                bool flag_3 = double.TryParse(s_read["授信額度"].ToString(), out x);
                if (flag_1 && flag_2 && flag_3)
                {
                   rate = double.Parse(s_read["匯率"].ToString());
                   org_currency = double.Parse(s_read["原幣未稅金額"].ToString());
                   credit = double.Parse(s_read["授信額度"].ToString());
                }
                customer_no = s_read["客戶編號"].ToString();
                terms_payment = s_read["付款條件"].ToString();
                local_currency = rate * org_currency;
                txt_saleno = s_read["銷貨單號"].ToString();
              }
              else
              {
               if (customer_1 != s_read["客戶名稱"].ToString())               {
                 sb.Append("單號:" + s_read["銷貨單號"] + "公司名稱與其他不同");
                 bPass = false;
               }
               if (currency_1 != s_read["幣別"].ToString())
               {
                 sb.Append("單號:" + s_read["銷貨單號"] + "幣別與其他不同.\r\n");
                 bPass = false;
               }
               if (txt_saleno == s_read["銷貨單號"].ToString())
               {
                 sb.Append("單號:" + s_read["銷貨單號"] + "重複.\r\n");
                 bPass = false;
               }
               org_currency = org_currency + (double.Parse(s_read["原幣未稅金額"].ToString()));
               local_currency = local_currency + (rate * (double.Parse(s_read["原幣未稅金額"].ToString())));
                            }
                        }
                    }
                }
                customer_i++;
            }
        }
string ErrorMsg = sb.ToString();
if (bPass && butcheck.ToolTip != "0")
{
  //Lucas 觸發 refresh 重新解悉流程
  string str_java = "document.getElementById('" +base.BtnCreateToolRefresh.ClientID + "').click();";
  Page.ClientScript.RegisterStartupScript(typeof(string), "jva", str_java, true);
  base.BtnCreateToolSendForm.Attributes.Add("style", "display");
  butcheck.ToolTip = "0";
  butcheck.Text = "已檢查";
  currencymoney.Text = org_currency.ToString();//原幣未稅金額
  orgmoney.Text = local_currency.ToString();
  customerno.Text = customer_no;
  overdue3.Text = terms_payment;
  credit3.Text = credit.ToString();
  }
  else
  {
     base.BtnCreateToolSendForm.Attributes.Add("style", "display:none");
     butcheck.ToolTip = "";
     MessageBox.Show("錯誤訊息如下:\r\n" + ErrorMsg);
  }
} 
#endregion
}

2012年9月20日

[.Net] 如何用 C# 將 LibreOffice 的 ods 轉成 excel 的 xls

首先,當然是要安裝 LibreOffice
接下來要安裝 LiberOffice SDK ,坦白說,這個步驟我找好久,
下圖是下載的位置:


安裝完成之後,開啟 dotNet C# 專案,將 C:\Program files\LibreOffice 3.6\sdk\cli 中的 Dll 全部加進參考。


接下來就可以開始寫程式了:
public static string FilePath2Url(string fname)
{
    return string.Format("file:///{0}", fname.Replace("\\", "/"));
}

internal static XComponent openCalcSheet(string fname)
{
    string url = FilePath2Url(fname);
    XComponentContext oStrap = uno.util.Bootstrap.bootstrap();
    XMultiServiceFactory oServMan = (XMultiServiceFactory)oStrap.getServiceManager();
    XComponentLoader desktop = (XComponentLoader)oServMan.createInstance("com.sun.star.frame.Desktop");
    PropertyValue[] loadProps = new PropertyValue[1];
    loadProps[0] = new PropertyValue();
    loadProps[0].Name = "Hidden";
    loadProps[0].Value = new uno.Any(true);
    XComponent document = desktop.loadComponentFromURL(url, "_blank", 0, loadProps);
    return document;
}

public static XSpreadsheetDocument Open(string fname)
{
    return (XSpreadsheetDocument)openCalcSheet(fname);
}

public static void Save(XSpreadsheetDocument xDoc, string fname)
{
    string url = FilePath2Url(fname);
    PropertyValue[] propVals = new PropertyValue[2];
    /* 另存成 MS Excel 97 的格式,
       格式可以參考:http://www.oooforum.org/forum/viewtopic.phtml?t=3549  (找不到官方的清單 Orz,看官知道在哪的,請通知一聲,感恩ㄚ)
       原理可以參考:http://wiki.openoffice.org/wiki/Documentation/DevGuide/OfficeDev/Integrating_Import_and_Export_Filters
     */
    propVals[0] = new PropertyValue { Name = "FilterName", Value = new uno.Any("MS Excel 97") };
    propVals[1] = new PropertyValue { Name = "Overwrite", Value = new uno.Any(true) };
    ((XStorable)xDoc).storeToURL(url, propVals);
}

public static void Convert2Xls(string fname, string new_fname)
{
    Save(Open(fname), new_fname);
}
於是只要呼叫 Convert2Xls(ods檔案位置, xls輸出檔案位置) 就可以將 ods 轉成 xls 囉。

範例程式:將選到的 ODS 另存成 XLS 並透過 oledb 讀取資料顯示在 DataGridView 上

參考資料:
http://www.oooforum.org/forum/viewtopic.phtml?t=3549
Integrating Import and Export Filters
use openoffice uno cli with c sharp to create a spreadsheet
c-cpp.r3dcode.com


2012年9月17日

[Refactoring] Demo video


原本這程式會分別出兩張報表,新的需求是要併成一張,
看到程式的第一個念頭就是「重構」,
請記住!「重構」不是「重寫」,
我們是懷著讓原本程式更容易閱讀的心情再做「重構」的。

影片不是很清楚,程式不是很容易閱讀,純粹紀錄「重構」的感覺,
如果你也進行過「重構」工作,我想你會懂我的。




2012年8月21日

[Javascript] 實作熱鍵(HotKey)共用程式,以Konami(上上下下左右左右BA)為例

/*
不曉得有沒有人想過像 Konami 那種,按 "上上下下左右左右BA" 後,
會觸發某些事件的功能,如何撰寫,以下是我的實作,除了特定的 Konami 按鍵外,
也允許自訂自己的按鍵事件序列的測試方法,
整個程式的概念,是註冊 document 的 keyup 事件,並紀錄下最近 10 次的按鍵內容,
並提供一個 RegisterTest 方法,由註冊者自行決定目前按下的按鍵是否符合觸發的條件,
若符合要觸發什麼事件; RegisterTest 會先把這兩個 function 紀錄下來,
等到畫面上的 kepup 事件產生時,逐一呼叫 testFun , testFun 回傳 true ,
就呼叫 triggerFun 執行當初註冊的觸發事件。
這個程式需要 jQuery ,
另外這個陽春的版本不提供組合鍵的處理,當然稍微修改一下也可以支援組合鍵。
*/
var HotKeyControler = {
    MAX_QUEUE: 10, /* 保留最後 10 個鍵盤事件 */
    testFuns: new Object(),
    triggerFuns: new Object(),
    KeyQueue: new Array,
    Keyup: function(event) {
        HotKeyControler.KeyQueue.push(event.keyCode);
        if (HotKeyControler.KeyQueue.length > HotKeyControler.MAX_QUEUE) {
            HotKeyControler.KeyQueue.shift();
        }
        for (var key in HotKeyControler.testFuns) {
            if (HotKeyControler.testFuns[key](event,  HotKeyControler.KeyQueue)) {
                HotKeyControler.triggerFuns[key]();
            }
        }
    },
   /*  函數名稱:註冊熱鍵
    *  參數說明:(唯一鍵值, 測試function, 觸發function)
    *  說   明:提供註冊熱鍵的方法,提供目前畫面被按下的按鍵佇列,供AP自行判斷是否觸發執行事件
    */
    RegisterTest: function(key, testFun, triggerFun) {
        if (typeof(testFun)=='function' && typeof(triggerFun)=='function') {
            HotKeyControler.testFuns[key] = testFun;
            HotKeyControler.triggerFuns[key] = triggerFun;
        }
    }
}

$(document).ready(function(){    
    $(document).unbind("keyup",HotKeyControler.Keyup);
    $(document).bind("keyup",HotKeyControler.Keyup);
});

/* DEMO 註冊當按下 Konami(上上下下左右左右BA)時要觸發的事件 */
HotKeyControler.RegisterTest("Konami"
    ,function(event,code) { /* 上上下下左右左右BA */
        if (code.length!=10) return false;
        if (code[0]==38 && code[1]==38 && code[2]==40 && code[3]==40 &&
            code[4]==37 && code[5]==39 && code[6]==37 && code[7]==39 &&
            code[8]==66 && code[9]==65) {
            return true;
        }
        return false;
    }
    ,function() {
        alert('上上下下左右左右BA!!!');
    }
);

2012年8月20日

[RegExp] 正規表示式(Regular Expression) 應用(一)

網路上有 一大堆 關於 Regular Expression (正規表示式),寫得很棒的文章,
所以這裡並不特別介紹,因為怎麼寫也沒有比別人寫得好,
因此我主要的目的是應用的分享,所謂師傅領進門,修行靠個人,
這篇就是把小弟個人的修行成果做個紀錄,
以後就不定期來更新,

HTML DOM Style 物件擁有很多屬性,
我們可以直接用 element.style[styleProperty] 去存取他們,
偏偏有些舊 IE 時代的程式會這麼寫:
element.style.getAttribute(styleProperty);
element.style.setAttribute(styleProperty, value);
糟糕的是,其它的 browser 不甩這種寫法,而是要這麼寫
element.style.getPropertyValue(style-property)
element.style.setProperty(style-property, value)
更慘的是,新舊參雜的風格時,有大量的程式需要調整、測試,在不動舊程式的情況下,
我們希望在其他瀏覽器下模擬出 style.get/setAttribute 的行為,
於是我這麼做:
if (typeof (CSSStyleDeclaration.prototype.getAttribute) == "undefined") {
    CSSStyleDeclaration.prototype.getAttribute = function (propName) {
        return this.getPropertyValue(Me._changePropName(propName));
    }
}
if (typeof (CSSStyleDeclaration.prototype.setAttribute) == "undefined") {
    CSSStyleDeclaration.prototype.setAttribute = function (propName, value) {
        this.setProperty(Me._changePropName(propName), value);
    }
}
但這卻遇到一個問題,當用 get/set Property 時, propertyName 格式是和 CSS 一樣,
中間用 "-" 分隔,但 IE get/set Attribute 的寫法則是第二個字的字首字母大寫(ex: backgroundColor Vs. background-color),所以我們需要處理一下 propName,
處理的方式則用運用 RE 來處理:
    function changePropName(propName) {
        var pattern = /[A-Z]/;
        var c = ("-" + (propName.match(pattern))).toLowerCase();
        var s = propName.split(pattern);
        var result = s[0];
        if (s.length > 1) {
            result += c + s[1];
        }
        return result;
    }
利用 RE 找出字串中的大寫字元,以 backgroundColor為例
mach 會找出 C
split 則會找出 background,olor
然後就把 'background' + '-' + 'c' + 'olor' 就完成了我們要的轉換了。




2012年7月26日

[.Net] Gotcha: Don't use <xhtmlConformance mode="Legacy"/> if you want to cross browser

好吧,我承認,這標題是學 Gotcha: Don't use <xhtmlConformance mode="Legacy"/> with ASP.NET AJAX 的。

不過情況有些不一樣,最近接手一個舊的 ASP.NET 網站,
雖然是 .Net Framework 2.0 的網站,但骨子裡其實是從 .Net Framework 1.1 升級的,
我們的需求不多,只要可以和時下流行的 Browser 相容就好,
也不打算引入 ASP.NET AJAX,
於是著手開始調整相容性相關的程式寫法,
調整得差不多後,開始測試,
沒想到一開始就遇到 Validator 不能正常執行的情況,
追了一下發現一個很特別的現象,

造成 Validator 不正常的問題出在 Asp.Net 的 ValidatorOnLoad 中,
會直接用 DOMObject.Attribute 去取值(如下程式碼片段),
但這在其他 IE 以外的瀏覽器是不允許的(IE9標準模式也不允許)!
function ValidatorOnLoad() {
if (typeof(Page_Validators) == "undefined")
return;
var i, val;
for (i = 0; i < Page_Validators.length; i++) {
val = Page_Validators[i];
if (typeof(val.evaluationfunction) == "string") {
eval("val.evaluationfunction = " + val.evaluationfunction + ";");
} // 略 .....
正確的作法應該要用 getAttribute('name') 才對,
但這不能解釋為什麼相同的程式碼在 .Net Framework 2.0 卻能正常的運作,
我用 evaluationfunction 這個關鍵字找了半天,
完全沒有找到相關的說明,
後來拿一個運作正常的網站所產生的 HTML 來和有問題的比較,
發現:

一般正常的應該要長得像這樣:

但他卻長成這樣:

到這裡,情況已經很明朗了,應該有什麼特別的設定,造成這樣的差異,
於是我改用別的關鍵字搜尋
.Net 1.0 and 2.0 hybrid validator not work
終於勉強找到了
Why might ASP.NET be putting JavaScript in HTML Comment blocks, not CDATA?
裡面提到了關鍵字 <xhtmlConformance mode="Transitional" />
再用 xhtmlConformance 就找到了 Gotcha: Don't use <xhtmlConformance mode="Legacy"/> with ASP.NET AJAX
原來,如果 xhtmlConformance mode="Legacy" 時,
不會處理那些不是標準 XHTML 的屬性,
反之,則會多產生一段 Javascript 把擴充屬性"塞"給 DOM Object ,
確保在不同的瀏覽器下都能夠用 DOMObject.attribute 的方式取到資料。

參考資料:
1. Why might ASP.NET be putting JavaScript in HTML Comment blocks, not CDATA?
2. Gotcha: Don't use <xhtmlConformance mode="Legacy"/> with ASP.NET AJAX
3. xhtmlConformance 項目 (ASP.NET 設定結構描述)
4. KB-由ASP.NET 1.1昇級的網站無法啟用MS AJAX


2012年7月25日

[Software] 分享一個好用的 link file 建立工具

以往傳統 Windows 的使用者,通常只知道所謂的捷徑(而且被捷徑制約很深),
Windows 的捷徑,只是一個文件,描述實際檔案路徑的位置,
透過程式(如Explorer)讀取捷徑文件的內容,再轉譯成實際檔案路徑,
因此,如果程式不是透過 Windows 的機制開啟捷徑所指的檔案,
那開起來的就會是捷徑本身。

Link file 這個概念,有點類似別名,
就好像我們知道 "第三者"、"小三" 指的都是同一件事情,
我們也可以把他想成是捷徑的進階版,而這個進階版,
則是需要作業系統及檔案系統都支持才能達成的,

所謂的檔案系統,就是我們資料儲存在儲存裝置上的方式,
常有人以為,儲存裝置(如硬碟)就像抽屜一樣,把文件通通都疊在裡面,
其實不然,當我們儲存一份文件到硬碟的時候,
作業系統會在一個類似書的目錄頁中,
建立一個索引,指向實際檔案內容的位置,
當我們在列出檔案清單的時候,並不是像在抽屜中一疊一疊的翻找,
而是將建立在目錄頁中的內容列出來,
當我們要開啟某份文件的時候,作業系統才透過該索引取得實際的檔案內容,

符號連結的概念,就是製作一個符號,指向檔案系統建立的目錄頁中的檔案,
而這個符號看起來就像真的指向實際檔案內容一樣,
所以就算一般程式在開啟這個符號連結的時候,
也不用考慮他是不是捷徑,需不需要額外處理,因為他們指的是同一件事情,
概念就介紹到這裡,至於 Symbolic link 、 Hard link 、 Junction 有什麼不同,
有興趣的話可以到以下網站參考
http://en.wikipedia.org/wiki/Hard_link
http://linux.vbird.org/linux_basic/0230filesystem/0230filesystem.php
http://en.wikipedia.org/wiki/NTFS_symbolic_link
http://webcache.googleusercontent.com/search?q=cache:2FHejrp9CZAJ:blog.miniasp.com/post/2011/09/17/How-to-create-NTFS-Reparse-Points-Symbolic-and-Hard-Links.aspx+&cd=2&hl=zh-TW&ct=clnk&lr=lang_zh-TW
http://jdev.tw/blog/729/vista-soft-and-hard-link

現在 Windows 7 之後的版本,提供了 mklink 的指令,
方便我們建立符號連結,但還是不夠直覺,
於是我找到了 Link Shell Extension
他的操作方式很簡單,

首先在 "來源的檔案或目錄" 上按右鍵(如下圖),選擇 Pick Link Source。



接下來在 "目的位置" 按右鍵(如下圖),選擇 Drop As... --> Hardlink(同一磁碟機才會出現) 或 Symbolic Link。

完成後結果如下圖:
選擇 Symbolic link 時,會出現綠色的捷徑符號,佔用的檔案大小為 0 ,
現在這個檔案就代表他的來源檔案,對一般應用程式而言,他們是沒有差別的!

選擇了 Hardlink 的話,來源及目的的檔案都會出現紅色的捷徑符號,
檔案大小也會一樣,但實際只佔一份檔案的硬碟空間,
Hardlink 和捷徑的概念不同,刪掉來源檔案,並不能真正將檔案從磁碟機中刪除,
要把所有 Hardlink 到這份檔案的連結都刪光才會真正的把空間釋放出來。


Link Shell Extension 讓我們就像 Windows 操作檔案的習慣一樣,操作 link file,是一個相當實用的工具,想瞭解更多也可以到 Link Shell Extension 網站上看個明白。

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' 的問題 ~~