November 2007 - Posts

TIPS-NetAdvantage WebDateChooser日期格式設定

最近在Survey Infragistics的NetAdvantage ASP.NET控件組,首先吸引我注意首推WebDateEdit與WebDateChooser:

WebDateEdit可以全手工輸入,也可以利用按上下左右鍵調整年、月、日或完全以滑鼠點選操作。官網的Live Demo

WebDateChooser則有漂亮的DHTML小月曆可以選日期,也支援直接手工輸入,但上下鍵只能用來調前一天後一天,手工輸入年、月後會自動補"-"。Live Demo

WebDateChooser可以選取預先設好的主題格式組合,小日曆就會長得一副Office 2003或是Windows XP的樣子。不過測試的結果有點小插曲:

在測試環境下執行,某些預設主題(例如: Office 2003)會跑出七顆"星"來(上圖左),完全看不出星期幾,但某些主題就不會有這種問題(上圖右)。查了一下,有個 DayNameFormat參數是關鍵:
<igsch:WebDateChooser ID="WebDateChooser1" runat="server" Value="2007-11-26">
    <CalendarLayout CellPadding="5" DayNameFormat="FirstLetter"

不過它比照.NET的DayNameFormat只有FirstLetter, FirstTwoLetter, Full, Short, Shortest幾種選擇,在此只要設定成Full,就可以看到完整的"星期X"。不過依我個人觀點,星期X用三個字實在太冗長,也破壞了原本精巧的外觀,還不如回歸用英文字表示, 但是要怎麼將中文顯示換成英文呢?

找了半天,連DateFormat之類設定日期格式的屬性都沒找到,後來才發現原來WebDateChooser的日期格式由WebDateChooser.CalendarLayout.Culture決定,難怪在中文版Windows環境中就自動跑出中文來。搞清楚這點,就可依下列方式指定格式:

System.Globalization.CultureInfo info = 
    CultureInfo.CreateSpecificCulture("en-USzh-TW");
info.DateTimeFormat.ShortDatePattern = "yyyy-MM-dd";
WebDateChooser1.CalendarLayout.Culture = info;

PS: 圖中兩個小日曆同時拉開也要點小技巧,NetAdavantage ASP.NET控件的Client Side支援做得還不錯,所以在body onload事件用以下的寫法就搞定囉!

    <script type="text/javascript">
    function pageOnLoad() {
        var wdc1 = igdrp_getComboById("WebDateChooser1");
        var wdc2 = igdrp_getComboById("WebDateChooser2");
        wdc1.setDropDownVisible(true);
        wdc2.setDropDownVisible(true);
    }
    </script>
還我國語辭典來

大家都知道我愛咬文嚼字,但可能不知道很多用字遣詞我都是上教育部的國語辭典網站臨時惡補來的。

這兩天,新聞又在炒國語辭典打炮的事。我才在想,這樣也好,眼見現在的小朋友國文程度江河日下,這麼好的網站靠著這個新聞熱潮博點知名度,吸引更多人善用,也是美事一椿。沒想到,不知是怕被找出更多的碴,還是被部裡長官釘到滿頭包,今天想去查字典時,只看到這個:

網頁Title叫做"新增網頁1"加上恢復服務時間不明,猜想這應非例行維護,也不會這麼巧今天剛好網站被駭或硬體掛點吧? 腦海中浮現的畫面是某位被大長官罵到臭頭的可憐小長官,惡狠狠地盯著更可憐的承辦人Elvis用微顫的雙手在下午2:45分用Word做了一張暫停服務網頁...

當部長可以一口氣惹毛這麼多人實在不是件簡單的事,不過開始被顯微鏡檢視後的情節發展早已失控,小蒼蠅都能變成異形怪獸。不過是個簡單的鼻腔排遺清除作業,也被炒上了天成為新聞頭條,更好玩的是還勞煩另一位博士局長跳出來以禪學大師的角色為大家剖析,此乃部長內心無限不屑意念昇華而成之意識流展現... 真是夠了。

政府層峰、立委諸公、記者狗仔、鄉民群眾,你們喜歡為了這點小事吵翻天,就隨你們去吧! 我不介意。但區區為了這個也許只算俚語的"打炮"能不能解釋成放鞭炮,搞到我的最愛關站大吉,就有點超過了。這下好了,原本方便到家的線上字典莫名其妙沒得用了,誰還我國語辭典來?

.NET變數該正名就正名,別再牽拖囉!

今天驚聞有人Copy程式碼,連變數名稱都懶得改。原本的日期欄位,明明已經改放公司統編,居然還沿用txtDate的欄位名稱,隱蔽與欺敵效果十足,接手維護到這等”全方位防駭型”程式碼,真是情何以堪… (老話一句,遇此情況,最好順便檢討一下平時是否有心懷不軌、負心劈腿、濫發好人卡等缺德行徑,才會遭此天譴~~ 無則嘉勉,有則改之)

講到變數正名,有人開始碎碎念: 改變數名稱很麻煩耶! 用Replace All會出亂子,例如: 想把變數名稱user換成customer,若是胡亂全面取代,到時連select userId from table都遭殃,改壞了你要賠嗎? #%^%$^#@

會拿這個理由搪塞的人,表示還不知道VS 2005有一個酷炫功能:

這個Rename功能可不是用文字比對,而是解析程式碼找出變數在專案裡的哪些地方被叫用到,再一一更名,精準度十足,從此變數更名不用怕會濫殺無辜。

所以,變數該正名就正名,不要再苟且囉~~~

【茶包射手專欄】Reporting Service rsExecutionNotFound

User回報Reporting Service檢視報表時,遇到以下錯誤:
找不到執行 'stdeve2cn35pg5bhmblygy45' (rsExecutionNotFound) 
Execution 'stdeve2cn35pg5bhmblygy45' cannot be found(rsExecutionNotFound)

同一時間,其他機器檢視同一報表一切正常,問題只出現在特定機器上。檢視Reporting Service Log(Log位於C:\Program Files\Microsoft SQL Server\MSSQL.3\Reporting Services\LogFiles),發現以下Exception:
w3wp!session!1!2007/11/26-09:00:16:: i INFO: LoadSnapshot: Item with session: stdeve2cn35pg5bhmblygy45, reportPath: , userName: DomainName\AccountName not found in the database
w3wp!library!1!2007/11/26-09:00:16:: e ERROR: Throwing Microsoft.ReportingServices.Diagnostics.Utilities.ExecutionNotFoundException: 找不到執行 'stdeve2cn35pg5bhmblygy45', ;
 Info: Microsoft.ReportingServices.Diagnostics.Utilities.ExecutionNotFoundException: 找不到執行 'stdeve2cn35pg5bhmblygy45'
w3wp!library!1!2007/11/26-09:00:16:: Unhandled exception was caught: Microsoft.Reporting.WebForms.ReportServerException: 找不到執行 'stdeve2cn35pg5bhmblygy45' (rsExecutionNotFound)
   at Microsoft.Reporting.WebForms.ServerReport.GetExecutionInfo()
   at Microsoft.Reporting.WebForms.ServerReport.SetExecutionId(String executionId, Boolean fullReportLoad)
   at Microsoft.Reporting.WebForms.ReportDataOperation..ctor()
   at Microsoft.Reporting.WebForms.HttpHandler.GetHandler()
   at Microsoft.Reporting.WebForms.HttpHandler.ProcessRequest(HttpContext context)
   at System.Web.HttpApplication.CallHandlerExecutionStep.
System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Google結果,看來與Session找不到或過期有關。進一步追查,確認有問題的IE在上週開啟後即未關閉,今天繼續使用,此一狀況符合Session過期的條件,推測就是導致錯誤原因,此時將IE關閉重新開啟即一切正常,Case Closed!

歐巴桑心理測試黑暗版解題法

上回話說同事誤發一題歐巴桑心理測驗得罪了方丈,衍生出名為研究實為找碴的程式練習題。以下便是黑暗版的C#解法。首先,設定一個物件對應出連線關係是一定要的,我用Dictionary<string, Node>建立一個以題號為Key的Node保存容器,用static GetNode() Method自容器中取出Node,不存在時則會即時新增放入容器中。至於連接的部分,是用Dictionary<string, Node>的方式記下選Y或選N會連至哪一個Node。

   1:  class Node
   2:  {
   3:      static Dictionary<string, Node> nodeList = 
   4:             new Dictionary<string, Node>();
   5:      public static Node GetNode(string name)
   6:      {
   7:          if (nodeList.ContainsKey(name))
   8:              return nodeList[name];
   9:          else
  10:          {
  11:              Node node = new Node(name);
  12:              nodeList.Add(name, node);
  13:              return node;
  14:          }
  15:      }
  16:      public Dictionary<string, Node> ToNodes = 
  17:             new Dictionary<string, Node>();
  18:      public string Name = "";
  19:      public Node(string name)
  20:      {
  21:          Name = name;
  22:      }
  23:      public void LinkTo(string choice, Node node) 
  24:      {
  25:          ToNodes.Add(choice, node);
  26:      }
  27:  }

元件搞定了,接下來用類似搜索子資料夾結構的遞迴(Recursive)寫法逐一走過所有可能的路徑(expore() Method),已走過的路徑會串出一個字串向下傳,直到Node不再有連出線就算到終點,算是完成一種走法。

   1:  static void Main(string[] args)
   2:  {
   3:      System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
   4:      sw.Start();
   5:      doIt();
   6:      sw.Stop();
   7:      Console.WriteLine(string.Format("Time: {0:#,##0}ms", 
                                           sw.ElapsedMilliseconds));
   8:      foreach (string result in results)
   9:          Console.WriteLine(result);
  10:      Console.Read();
  11:  }
  12:  static void doIt()
  13:  {
  14:      string data =
  15:      "1:Y4,N2;2:Y3,N5;3:Y9,N6;4:Y7,N3;5:Y8,N9;6:Y12,N14;7:Y10,N6;8:Y11,N12;"
+ "9:Y13,N11;10:Y14,N13;11:YB,NA;12:YC,NA;13:YB,ND;14:YC,ND;A:;B:;C:;D:"
;
  16:      string[] nodeSets = data.Split(';');
  17:      foreach (string nodeSet in nodeSets)
  18:      {
  19:          string nodeName = nodeSet.Split(':')[0];
  20:          string nodePathes = nodeSet.Split(':')[1];
  21:          string[] pathes = (nodePathes.Length > 0) ? 
                                  nodePathes.Split(',') : new string[0];
  22:          Node node = Node.GetNode(nodeName);
  23:          foreach (string path in pathes)
  24:              node.LinkTo(path.Substring(0, 1), 
Node.GetNode(path.Substring(1)));
  25:      }
  26:      explore(Node.GetNode("1"), "");
  27:  }
  28:  static void explore(Node node, string route)
  29:  {
  30:      if (route.Length > 0) route += "->";
  31:      route += node.Name;
  32:      if (node.ToNodes.Count == 0)
  33:          results.Add(route);
  34:      else
  35:          foreach (Node toNode in node.ToNodes.Values)
  36:              explore(toNode, route);
  37:  }
  38:  static List<string> results = new List<string>();

不算上班途中邊騎邊想的時間,Coding大約花了半小時不到,程式執行耗時約9ms。以上的邏輯可以改寫成走迷宮,不過需要再加上防止形成Loop的邏輯。

以上程式雖不足70列,執行速度也夠快,但怎麼都只能算是中規中矩。週四題目貼出不過幾小時,網友Ammon就抛出一則以Javascript撰寫的解法,讓我嚇到尿褲子。沒想到短短幾行Javascript也可以搞出這麼靈活的運用,常自詡對Javascript有一定認識,但見了這番媲美吞劍噴火的花式應用,才驚覺自己恐怕還在幼幼班的水準。網路之大,本當卧虎藏龍;學海無涯,切忌意得志滿。領教了!!

Canon RAW Codec 1.2

之前安德魯的Blog介紹過Canon RAW Codec,可以讓Vista整合RAW檔,不過被文後一句只支援.CR2澆了一頭冷水。最近又有一篇文章似乎提到新版Codec支援RAW檔的事,不過一直以來都抱持照片拍完搬到HD就當沒事的苟且心態,看到這類消息並沒有太大的興奮感,更談不上花功夫研究照片檔。

今天不知吃錯什麼藥,寫了小程式準備要來整理幾年下來累積的22G照片檔。不能直接在Vista檔案總管下檢視RAW檔有點煩,於是想起Canon RAW Codec。找到以下的網址下載:

Canon RAW Codec 1.2
http://software.canon-europe.com/software/0026049.asp

網頁主要說明只提了.CR2,但下方又很矛盾的說10D也支援,反正裝來試試便知,嘿!! 檔案總管可以直接檢視RAW檔了,酷~~~

第一件PowerShell Script作品

PowerShell是未來微軟管理性Script的主流,過去曾稍微看過,今天總算正式開張,寫了我的第一支PowerShell Script,用途是檢查伺服器上某一個檔案是否順利產生。如果沒看到檔案,表示產生檔案的排程出錯,就要發警告信通知相關人員介入處理。

原本以為接觸一個全新語言會有個充滿挫折感的開始,沒想到PowerShell裡可以直接建立.NET元件!! 原本已經做好得在地上慢慢爬的心理準備,這下子等於又把翅膀領回來,開心到差點從椅子上跌下來。(不過,可以密切整合.NET這點一則以喜一則以憂,學習新語言就該善用新語言的特性,如果一昧只知道用.NET蠻幹,對PowerShell一大片桃花源視而不見,恐怕也非好事。)

好了,我的第一個PowerShelll版管理工具Script如下:

###################################
#Check-File.ps1 by Jeffrey Lee (www.darkthread.net) #
###################################
$AppName = "Check-File.ps1"
$AppVer = "v1.0 [2007-11-23]"
$today = Get-Date -format "yyyyMMdd"
$file = "\\remote_server\share_folder\prefix" + $today + ".txt"
if (Test-Path $file)
{ Write-Host "Pass" -foreground "green" }
else
{
  Write-Host "Fail" -foreground "red"
  $smtp = New-Object System.Net.Mail.SmtpClient
  $smtp.Host = "relay.mail.darkthread.com"
  $smtp.Send("sender@darkthread.com", "recipant@darkthread.com", "File Check Warning", $file + " not found!!")
}

不到20列寫完,簡單到出人意料。Get-Date取得今天日期,Test-Path檢查檔案是否存在,而搬出System.Net.Mail.SmtpClient讓原本以為要大興土木的寄信動作三行打死。以後要遇到要寫Script或小型管理工具的場合,又多了一件順手的新兵器,哈!!

底下是一些相關資源:

1.下載PowerShell 1.0 (PowerShell 2.0 CTP也出來了,只可惜不支援Windows 2003)
http://www.microsoft.com/downloads/details.aspx?FamilyId=10EE29AF-7C3A-4057-8367-C9C1DAB6E2BF&displaylang=en

2.PowerShell CheatSheet,撰寫PowerShell時用的小抄,不過內容略嫌簡單、涵蓋範圍不大,有點不夠用的感覺。
http://www.microsoft.com/downloads/details.aspx?FamilyId=DF8ED469-9007-401C-85E7-46649A32D0E0&displaylang=en

3.PowerShell HelpFile。CHM格式的說明文件,寫得很詳細也很淺顯易讀,是佛心級的好文件,其中VBScript與PowerScript的指令對照表更是功德無量!!!
http://www.microsoft.com/downloads/details.aspx?FamilyId=3B3F7CE4-43EA-4A21-90CC-966A7FC6C6E8&displaylang=en

另外,跑ps1 Script檔時,會先遇到此錯誤:
File C:\Program Files\Console2\check-file.ps1 cannot be loaded because the execution of scripts is disabled on this system. Please see "get-help about_signing" for more details.
At line:1 char:15
+ ./check-file.ps1 <<<<

基於安全的理由,ps1 Script預設是不給跑的,記得用以下的指令調整安全設定:
Set-ExecutionPolicy RemoteSigned

Coding For Fun: Routing Problem

同事寄給我這個"心理測驗",雖然不是女人,我還是很無聊地把它做完:

 妳「將來」會是怎樣ㄉ歐巴桑 (男生可以給女朋友、老婆做做看唷!)

( 1) 你曾經被朋友說你是個怪人 YES-- 往 4 NO--往2
( 2) 走路走的非常累的時候,會想直接就坐在路邊 YES-- 往3 NO--往5
( 3) 喜歡在筆記本上畫很多圖案或貼很多貼紙,把它裝飾的色彩繽紛 YES--往9 NO--往6
( 4) 你喜歡一邊唱歌一邊洗澡 YES--往 7 NO--往 3
( 5) 曾經狂戀過漫畫中或卡通影片裡的人物 YES--往8 NO-- 往 9
( 6) 走路或跑步的速度都比別人慢 YES--往12 NO--往 14
( 7) 想跟年紀小的男生談場戀愛 YES--往10 NO--往 6
( 8) 長大後想戴很多的首飾 YES--往 11 NO--往 12
( 9) 朋友做錯事就要明白告訴他,這樣才算真正的朋友 YES-- 往13 NO--往11
(10) 書包和抽屜裡經常亂七八糟的 YES--往 14 NO-- 往13
(11) 曾當過朋友的紅娘 YES--往B NO-- 往 A
(12) 到了蛋糕吃到飽的店,你有自信可以吃下很多蛋糕 YES--往 C NO--往A
(13) 吃早餐時,喜歡麵包勝於飯 YES-- 往B NO-- 往 D
(14) 看雜誌時連廣告頁也不會錯過 YES-- 往C NO--往D

【解析】
A 優雅的貴夫人
> 你將來是位優雅高貴的太太, 講話都很客氣 , 笑聲非常高亢, 很重視打扮,喜歡華麗款式的服飾, 就算是平常日子也都把自己打扮的光鮮亮麗 , 雙手都戴著超大顆的鑽石戒指, 你常然不想做家事, 所以要找個錢的人結婚, 這樣才有錢請佣人 , 你就不用當黃臉婆了!

B 年輕時髦的太太
> 你將來是位年輕時髦的太太, 很重視生活品質與身材保養, 不管是學打網球 ,學游泳, 上瘦身中心等, 你每天都很努力想讓自己青春永駐 , 你的穿著打扮都很年輕, 走在路上還會有男人對你吹口哨 , 你很討厭別人叫你歐巴桑, 就算對方是個小孩子, 你也不會原諒他 !

C 居家的歐巴桑
> 你將來是標準的歐巴桑, 每天早上送先生, 孩子出門後 , 就你的自由時間 ,睡一整天後把肚子都睡出來了, 看電視! 是你唯一的樂趣 , 你只能選鬆垮垮的衣服穿, 到了下午就和附近的太太們一起三姑六婆, 傍晚時再去超市拿免費試吃的菜回家, 這也是你的生活樂之一!

D 討厭的老媽子 !
> 將來你是位頗有鄉土氣息的歐巴桑 , 服裝品味與眾不同, 老是穿緊身的綁腿襪,有時候也會穿名牌衣服, 但就是嗅不到半點名牌氣息 , 搭公車時只要看見有點縫隙, 就用你的大屁股擠出一個位置, free now!

啥?? 敢說我是老媽子!!

惱羞成怒之餘,我就開始找這個題目的麻煩(總是要試一下選舉無效之訴咩!),首先我挑剔它有些題目對沒有自知之明的人無效。例如: 第一題"曾經被朋友說成怪人",說不定有些怪人一輩子都覺得自己很正常,甚至還因為自己太怪,根本沒朋友有膽子開口說他怪(或者根本沒朋友?);而第一題選了YES,說不定就萬刧不復,永遠沒機會變成A或B。我氣得有點想把用Visio畫成流程圖來分析,證明我的論點是對的。不過,這樣做好像真會變成窮極無聊的老媽子...

早上騎車上班時,忽然想到一個有趣的點子。何不來做寫個分析程式,哪些選項選YES最有可能變成D的,而要做到這一點,最好的方法是把所有的路徑先全部列出來,而展開的過程不就是電腦鼠走迷宮的演算法邏輯? 頓時,它忽然變成一個很有趣的程式設計練習題,OK,既然Blog小站上來來往往的程式設計高手如雲,不妨邀請有興趣的人來玩玩,規則如下:

1) 撰寫一隻程式,列出以上心理測驗所有可能的路徑,每條路徑的表示法為 1->2->5->9->13->B
2) 程式語言不限,For Windows的程式倒可以放在同一台機器上比賽誰的程式跑得快
3) 程式碼愈短愈好
4) 執行速度愈快愈好
5) 記錄一下自己開發+測試所花的時間

想跟大家分享自己成果的人可以貼在討論區的這條Thread裡(記得內文要署名,換CS2007後,我還沒加回署名功能),一方面討論區的編輯介面比較豐富,一方面不要提前"透露劇情",破壞大夥玩遊戲的樂趣。我會在下週一公佈我的黑暗版演算法程式。

KB-Keep Your Code Thread-Safe!

前幾天幫同事看一個WinForm問題,明明有Primary Key限制的DataTable,卻冒出數筆PK相同的資料,Grid還會發狂似地不斷捲動。

由於資料的更新動作來自非UI Thread,我們首先懷疑的就是Threading Issue,不過該問題只出現在尖峰時刻資料量爆多的情境下,在測試台中怎麼都無法模擬出來,於是我設計了以下的實驗,證明忽略Thread-Safe Issue時的確會衍生類似的問題。

程式是這樣寫的,Form_Load時建立一個DataTable,並以Symbo欄位l為Primary Key,接著把它指定成C1FlexGrid.DataSource。按下Button時,啟動五條Thread在DataTable中塞入亂數產生的資料,並利用DataTable.Rows.Find的方式檢查該Symbol是否已有資料,決定採取新增或是更新。有兩個重點:
1) Symbol是PK,所以任何兩筆資料的Symbol不可能相同,也不可為NULL
2) DataTable.DefaultView.Sort="Symbol",所以我們看到的資料應該是依Symbol由小到大排序

private DataTable dt = new DataTable();
private string[] symbolPool = new string[100];
 
private void Form1_Load(object sender, System.EventArgs e)
{
    //建立資料表
    dt.Columns.Add("Symbol", typeof(string));
    dt.Columns.Add("Price", typeof(float));
    dt.Columns.Add("Quantity", typeof(int));
    dt.Columns.Add("Amount", typeof(int));
    //Symbol是Primary Key
    dt.PrimaryKey = new DataColumn[] { dt.Columns["Symbol"] };
    //建立代號清單
    for (int i = 0; i < symbolPool.Length; i++)
        symbolPool[i] = i.ToString("0000");
    //依代號排序
    dt.DefaultView.Sort = "Symbol";
    c1.DataSource = dt;
}
 
private void btnGo_Click(object sender, System.EventArgs e)
{
    if (btnGo.Text=="GO") 
    {
        bStop=false;
        //開啟五條Thread同時塞資料
        for (int i=0; i<5; i++) 
        {
            System.Threading.ThreadPool.QueueUserWorkItem(
                new WaitCallback(updateTrade));
        }
        btnGo.Text="Cancel";
    }
    else 
    {
        bStop = true;
        btnGo.Text = "GO";
    }
 
}
//利用bStop旗標要求停止測試
private bool bStop = false;
 
private void updateTrade(object args) 
{
    Random rnd = new Random();
    while (!bStop) 
    {
        //用亂數產生測試資料
        string symbol = rnd.Next(100).ToString("0000");
        float prz = Convert.ToSingle(
            Math.Round(rnd.NextDouble()*100, 2));
        int qty = rnd.Next(50) * 1000;
        int amt = Convert.ToInt32(prz * qty);
        try 
        {
            //先找看看是否已有該筆資料
            DataRow row = dt.Rows.Find(symbol);
            bool bNew = false;
            //沒有時則準備新增
            if (row==null) 
            {
                row = dt.NewRow();
                bNew = true;
            }
            row["Symbol"]=symbol;
            row["Price"]=prz;
            row["Quantity"]=qty;
            row["Amount"]=amt;
            if (bNew)
                dt.Rows.Add(row);
        }
        catch (Exception ex) 
        {
            Console.WriteLine(ex.Message);
        }
        //等待不定長度的時間
        Thread.Sleep(rnd.Next(200));
    }
}

在.NET 1.1下執行程式,我們得到了這樣的結果

0024, 0030出現兩筆,0066後方排了一筆0030,再下方有一筆完全沒數字的資料。PK重複、PK=NULL、排序反覆,這... 程式造反了嗎? 是的,當程式涉及了多條Thread時,只要稍不留神,就會爆出各式各樣想都想不到的結果。程式再跑一段時間,也許還有機會遇見曾讓我灰頭土臉的Null Reference Exception。

在Multithread環境下使用沒有非Thread-Safe的Class/Method,產生的結果無法預期,而這也不是元件開發者的責任。當使用多個Thread去存取同一個物件時,切記要考慮彼此間的交互影響,而在上面的例子中,我們只需要加一個lock(this)就可解決問題。強制同步化會影響效能,但跑出錯誤的結果,再怎麼快也是快心酸的,二者輕重,不言而喻。

lock (this) 
{
    try 
    {
        DataRow row = dt.Rows.Find(symbol);
        bool bNew = false;
        if (row==null) 
        {
            row = dt.NewRow();
            bNew = true;
        }
        row["Symbol"]=symbol;
        row["Price"]=prz;
        row["Quantity"]=qty;
        row["Amount"]=amt;
        if (bNew)
            dt.Rows.Add(row);
    }
    catch (Exception ex) 
    {
        Console.WriteLine(ex.Message);
    }
}

享受Multithread Coding樂趣之餘,千萬記住: Keep Your Code Thread-Safe!

延伸閱讀: UI Thread Issue

(C)Brain Age Is Coming Back?

又到了中年人講古的時間了...

二十多年前,以帥氣的吊車尾姿勢,擠上工專錄取的最後一個名額。心想,我這輩子再也不要參加聯考了(造化弄人,後來還是又考了二技),於是央求家裡買了朝思暮想多年的第一台電腦,宏碁小教授MPF 500,Intel 8088 CPU,4.77MHz,640M640K RAM,兩台360K軟碟機FDD(嘖嘖嘖,兩台軟碟當年也算豪華配備呢! 只有一台FDD的機器,整張磁片複製時要抽換三四次哩!),從此跳入火坑,沒再爬出來過。

那幾年,軟體、資料都是放在賀年卡大小的5.25吋軟碟片帶來帶去,360K的大小,差不多是20秒MP3的大小,連一首歌前奏都裝不下。有一天,發現一件奇怪的事,手上有好幾片磁片DIR時,看到的Label都變成了(c)Brain,而且數量一天比一天多。沒多久,才知道原來這就是電腦病毒--(c)Brain,一對巴基斯坦兄弟的傑作,不具有任何殺傷力,甚至還留下地址電話請大家跟他們連絡,這麼純真無邪的"病毒"行徑,足以讓當代的病毒/木馬作者笑倒在地,不斷抽搐,但這的確是普遍公認的第一隻病毒。

(c)Brain這種Boot Sector病毒的可怕之處在於,就算你深具戒心,絕不執行任何來路不明的程式,自家帶來的乾淨磁片,只要一插入中毒機器就立即中鏢。帶著同一張磁片回家開機(當年大部分的電腦還沒有硬碟,所以開機都要先插入一張可開機的磁片),家中的電腦頓時也變毒窟,換一片磁片讀資料就感染一片資料片。感染的資料片再借給只有一台軟碟的同學,開完機換上有毒磁片讀資料,一時忘了換回開機片就重開機,開機的當下讓毒磁片留在磁碟機中,噹啦~~~ 一座新的毒窟又誕生了。

之後,病毒愈來愈多,毒性愈來愈強,接著木馬、蠕蟲也接連冒了出來,但另一個產業---防毒軟體也開始蓬勃發展,二者間的戰爭未曾休止。

不過,自後Harddisk變成開機的主要媒體後,Boot Sector Virus那種不小心開機放錯磁片就會中鏢的恐怖經驗倒少了。只要恪守勤做Windows Update,不點選來路不明程式的原則,中毒的機率可以降到很低。

直到今天,可憐的同事因為借某長官插一下USB行動碟查看檔案,不久後就發現身中木馬,花了一整天掃毒、重開(據了解,今天一天重開機的次數就比今年一整年的總和還多)。然後還發現其中有個病毒還會阻止你將"檔案總管"設成可以檢視系統檔案,讓你從 GUI介面一輩子也看不到被種下去的autorun.inf。最後,我下場幫忙看了一下,靠老骨頭腦中的古老指令: attrib -r -h -s autorun.inf才讓它現形。

問題解決了,我卻猛然驚覺,那個一插磁片當場中鏢的恐怖(c)Brain時代又回來了,而且這次還不需苦等重新開機,在插上行動碟的那一瞬間病毒就能登堂入室了。想一想,AutoRun帶給我的威脅顯然大於便利性,於是趕緊修改Registry,將Windows的AutoRun功能全部關起來,而以後要插入行動碟前,我也會特別提高警覺。

TIPS-C1FlexGrid Header Cells Merging

為了要在C1FlexGrid中做出如下Header Cell合併的效果,煞費苦心...

簡單來說,這類的合併效果必須繼承C1FlexGrid再做一顆自訂元件,並Override GetMergeCell這個函數,判斷傳入的Row/Col數是否屬於合併範圍決定傳回值。而GetMergeCell在UI重畫過程中會被高速連續呼叫,函數中的邏輯要盡可能簡化,並避免掉所有不必要的運算,否則就等著在執行階段看你的CPU破表吧!

public class CustC1FlexGrid : C1FlexGrid
{
    //用來儲存合併範圍Index的物件
    private class CellRangeIndex 
    {
        public int StartRow, StartCol, EndRow, EndCol;
        public CellRangeIndex(
            int startRow, int startCol, 
            int endRow, int endCol)
        {
            StartRow = startRow;
            EndRow = endRow;
            StartCol = startCol;
            EndCol = endCol;
        }
    }
 
    #region Constructor & Dispose(略)
    #endregion
    
    //儲存合併設定
    CellRangeIndex[,] mergeMapping = null;
    //是否需要處理合併
    bool needMerging = false;
    //指定哪幾個Cell合併成一個
    public void MergeCells(int startRowIdx, int startColIdx, 
        int endRowIdx, int endColIdx) 
    {
        needMerging = true;
        CellRangeIndex cri =
            new CellRangeIndex(startRowIdx, startColIdx,
            endRowIdx, endColIdx);
        for (int i=startRowIdx; i<=endRowIdx; i++) 
            for (int j=startColIdx; j<=endColIdx; j++)
                mergeMapping[i, j]=cri;
    }
    //Header的Row數
    private int HEADER_ROW_COUNT = 2;
    //設定必要的合併用參數及變數
    public void InitMergeSetting() 
    {
        this.AllowMerging = AllowMergingEnum.FixedOnly;
        for (int r = 0; r < HEADER_ROW_COUNT; r++)
            Rows[r].AllowMerging = true;
        for (int c = 0; c < Cols.Count; c++) 
            Cols[c].AllowMerging = true;
        mergeMapping = 
            new CellRangeIndex[HEADER_ROW_COUNT, Cols.Count];
    }
    //自訂欄位合併的邏輯
    public override CellRange GetMergedRange
(int row, int col, bool clip)
    {
        if (needMerging && 
            row < HEADER_ROW_COUNT && 
            mergeMapping[row, col]!=null)
        {
            CellRangeIndex cri = mergeMapping[row, col];
            return base.GetCellRange(
                cri.StartRow, cri.StartCol,
                cri.EndRow, cri.EndCol);
        }
        return base.GetCellRange(row, col);
    }
 
    #region Component Designer generated code
    #endregion
}

看似簡單的一個需求,不能用設定解決而要另寫繼承元件,讓整件事變得複雜,所幸元件寫好後,呼叫端看來還算簡單。再補充一點,你必須在要合併的兩個Cell中都指定同樣的值,兩個Cell才會合併在一起。

private void Form1_Load(object sender, System.EventArgs e)
{
    c1.Rows.Count=10;
    c1.Cols.Count=6;
    c1.InitMergeSetting();
    c1.MergeCells(0,1,0,2);
    c1.MergeCells(0,3,0,4);
    c1.MergeCells(0,0,1,0);
    c1.MergeCells(0,5,1,5);
    c1[0, 0]=c1[1, 0]="Name";
    c1[0, 1]=c1[0, 2]="PC";
    c1[1, 1]="CPU";
    c1[1, 2]="RAM";
    c1[0, 3]=c1[0, 4]="NB";
    c1[1, 3]="CPU";
    c1[1, 4]="RAM";
    c1[0, 5]=c1[1, 5]="Remark";
 
}
Why Master Page, But Not Frameset?

跟同事討論新網站如何讓所有網頁都保持一致的Header/Menu/Footer,我的看法是回歸ASP.NET 2.0建議使用的Master Page、同事則覺得這樣比較笨重,不如保持用FrameSet切割出一塊Frame切換內容的傳統做法即可。想了想,到ASP.NET 2.0後,看到的幾乎都是用Mater Page解決,VS 2005 IDE甚至會在你使用FrameSet時發出警告;另外一方面,除了ASP.NET之外,印象中現在Internet中遊歷到的大小網站,除了一些上了年紀的簡單小網站,已經很難看到用Frame切割Header/Menu的做法。雖然腦中大致了解二者間的差異,但Frame式的設計似乎已快從地球上絕跡了,為什麼?

好奇地針對這個問題,Google了一下,以下是我的心得。

首先,要做出全站一致的Header/Footer,有幾種做法:

  • Copy & Paste大法! 把Header/Menu/Footer的Code複製到每一個網頁。
    (你瘋了嗎? 誰敢給我用這種方法設計網站,我一定用鍵盤打爆他的頭!!)
  • Frameset: 每次只更換Content Frame網頁,缺點是會有Cross-Frame Scripting的Issue,而且有可能Frame間更新狀態不同步。
  • 利用Server-Side Include: 可用在ASP,但每一頁都要配合排版位置插入。
  • 利用User Control: 可用在ASPX,但每一頁要配合排版位置擺放。
  • Master Page: 利用類似繼承的概念,內容頁不必過問Header/Footer的排版設計,但每次執行都要重新產生Footer/Header元素。

以上這些做法,在ASP.NET 2.0中,大概只剩下Master Page及Frameset兩種要抉擇。Master Page可以確保直接使用內容頁的URL也會看到完整的版面設計,用Frameset則可能瞧見沒有Header/Footer的裸體版內容頁,這在Search Engine點選查詢結果時最可能發生。但Master Page的每一頁都要重新載入、產生及傳輸Header/Footer也是不爭的事實,比Frameset有更多無謂的消耗。

找到一篇不錯的剖析,列舉了採用各架構的最佳實務:

Master Page

  • 不介意每次全頁更新
  • 需要彈性化的繼承式版型設計,且使用者不在意全頁更新
  • 希望每一頁都被獨立檢視時,都可以完整呈現

ICallbackEventHandler

  • 只想局部更新頁面的部分資料或圖片
  • 只想局部更新頁面上一些簡單的HTML元素(不含asp: Controls)

XML Data Islands

  • 在Client建立資料儲存,建少網頁點擊更新次數

FrameSet

  • 不希望全頁更新
  • 具有複雜的網頁元件,必須從Server端產生更新(不能只換Data)
  • 網頁上不同的區域需要用不同的頻率更新

不過,由這些分析,我還是無法理解大部分網站捨棄Frame的理由,看到不少人說"Frames Are Evil"(相信嗎? 居然有個"我恨Frames俱樂部"!),卻沒足夠的理由讓我完全信服。我所知道Frame的缺點包含四點:

  1. Cross-Frame Scripting比較曲折,但只要不是Cross-Site,並不難解決。
  2. Frame間可能發生更新不同步,例如: Header Frame的Logon User與Content Frame的不是同一人。
  3. Browser顯示的URL無法準確反應內容Frame的變動,譹Brower的標籤功能(IE我的最愛)頓成廢材。
  4. 當使用者使用Content Frame用的URL連上網站,看到的不是完整網站呈現版型。目前Search Engine記錄及Index的特性,這種狀況挺常發生的,但我不敢斷定這就是Frame漸漸被揚棄最主要的理由。

於是我又做了些挖掘,看看可否找出Frame有更多我不知道的黑暗面? 以下是我又挖到的一些補充:

  • Frame讓Browser的列印功能變得不直覺
  • Frame導致Browser的Refresh行為與使用者的預期有出入

不過,歸納了以上的種種剖析,我認為Frame有些缺點,但罪不至死,不需反應過度。在某些情境下,用Frame仍OK,如果:

  • 網站為內部使用,不在乎Search Engine Friendly
  • Header/Menu/Footer的演算及HTML很複雜,值得省下這個資源成本
  • User連線頻寬有限,需要儘可能減少資料傳輸量(Update 2007-11-19)

至於我,應該還是會回歸Master Page,理由是盡量用別人在用的方法做事,出問題時可以多些難兄難弟,也多點相關討論可以參考;萬一被Challenge時還可以說,"嘿! 別怪我,我是照著微軟建議的方式處理的",然後把頭埋到沙子裡繼續睡午覺,哈!!

【茶包射手專欄】Weird Invalid Viewstate Exception

已經好長一段時間,有某幾隻程式不定期會冒出以下的錯誤訊息:
Event code: 4009
Event message: Viewstate verification failed. Reason: The viewstate supplied failed integrity check.

這些程式在測試環境及平常使用狀況都很良好。第一直覺是有人更動了ViewState的資料,但推測不會有人無聊到做這種沒意義的駭客工程。今天特地花了點時間去追這個問題,我的構想是從IIS Log中找出該使用者的全部操作歷程,想像一下他可能做了哪些事。

發現一件有趣的事,今早09:10只出現一筆POST記錄,但追溯00:00為止都看不到任何GET,再往前追一天,才看到同一User早上有一個GET及多個POST的記錄。

我推測是User在昨天早上開了IE連上網頁,按了幾次查詢,然後IE沒關放著,一直到今天早上再按一下查詢,結果就出錯。

ViewState有保存期限嗎? 印象中沒聽過,但用力Google的結果,找到了以下的KB,發現原來Worker Process Restart也會導致ViewState被判定無效!

http://support.microsoft.com/default.aspx?scid=kb;en-us;829743

Determine whether the problem is caused by worker process recycling
Consider the following scenario.
  • You are running ASP.NET under Microsoft Internet Information Services (IIS) 6.0.
  • The application pool is running under an identity other than the Local System account, the Network Service account, or an administrative-level account.
  • The validationKey attribute of the <machineKey> element is set to AutoGenerate in the configuration file.

In this scenario, the following procedure will cause a view state error to occur:

  1. A user browses a page.
  2. The worker process that hosts the ASP.NET application recycles.
  3. The user posts back the page.

我把會出問題的程式跟機器做了簡單的統計... 哈! 問題會固定出在幾台機器上,而這幾台機器有排定每天凌晨IISRESET(咳... 鋸箭法被發現了,不准再問細節)。

答案揭曉! Case Closed!

How I got tortured by try-catch block and escaped from it.

.NET的開發效能提示中,有一條原則是"不要把try...catch當成正常流程"。今天在修改前人程式時,看到一個挺經典的真實案例,跟大家分享一下。

   1:  static void avoidTryCatchDemo(string file)
   2:  {
   3:      StreamReader sr = 
   4:          new StreamReader(file);
   5:      System.Diagnostics.Stopwatch sw = 
   6:          new System.Diagnostics.Stopwatch();
   7:      sw.Start();
   8:      string line = "";
   9:      while ((line = sr.ReadLine()) != null)
  10:      {
  11:          string[] p = line.Split(';');
  12:          //...略...
  13:          try
  14:          {
  15:              dt = (MyNamespace.DataType)Enum.Parse(
  16:                  typeof(MyNamespace.DataType),
  17:                  p[4]);
  18:          }
  19:          catch
  20:          {
  21:          }
  22:          //...略...
  23:      }
  24:      sw.Stop();
  25:      sr.Close();
  26:      Console.WriteLine(
  27:          string.Format("Load {0} in {1:#,##0}ms", 
  28:          file, sw.ElapsedMilliseconds)
  29:          );
  30:  }

我先是發現程式每次啟動的時間都長達數十秒到一分鐘,忍耐了一陣子,最近終於有空檔,就鑽進去看個究竟,找看看有無可調整之處。我抓這個問題的做法是在用VS 2005的Debug模式啟動程式,在啟動等待期間幾次按下中斷,統計一下程式都停在哪裡,應該就是主要耗時的地方。試了幾次,程式都停在第15列,仔細一看,唷! try...catch,該不會是這裡造成效能狂降? 於是我將Code改成:

        try
        {
            dt = (MyNamespace.DataType)Enum.Parse(
                typeof(MyNamespace.DollarType),
                p[4]);
        }
        catch
        {
            Console.WriteLine("Damn! I's wasting time!");
        }

理論上,只要進入catch block,就意味著已經付出昂貴的效能代價。我還加上了Stopwatch(先前有介紹過)來計算耗用時間,切記,建立衡量基準是所有效能調校工作的第一步,也是要享受成就感不可或缺的關鍵步驟。

先跑了一次,Wow... Console印出一大堆程式為了浪費時間的懺悔,然後計時數據是

Load FileSYS in 79ms
Load FileE in 14,766ms
Load FileA in 2,907ms
Load FileB in 1,609ms
Load FileC in 2,708ms
Load FileD in 3,121ms

進一步追查,因為資料不嚴謹,有很多p[4]是空字串而轉換失敗。依照效能法則,我應該要避開可預防的Exception,於是把程式改成:

        try
        {
            if (p[4].Length>0)
                dt = (MyNamespace.DataType)Enum.Parse(
                typeof(MyNamespace.DollarType),
                p[4]);
        }
        catch
        {
            Console.WriteLine("Damn! I's wasting time!");
        }

再跑一次,改過之後的效能數字:

Load FileSYS in 74ms
Load FileE in 5,514ms
Load FileA in 22ms
Load FileB in 12ms
Load FileC in 23ms
Load FileD in 25ms

不多不多,不過差了100倍而已!

由以上的實例,提醒大家,不要把try...catch視為合理的流程,每次進入catch Block都要想成我們是在打破玻璃,拉下拉捍讓疾駛中的捷運電車停下來,沒事亂拉一通,就等著接罰單吧!

Community Server 2007.1!!

有些事現在不做,一輩子都不會做了!

電影"練習曲"

Community Server 2007出來好一陣子了,由於每天分配給這些543的時間有限,偏偏自己又是少不了睡眠的中年人(最近的新體認---所謂的技術強者,意指既聰明又不用睡覺的超人,Scott Hanselman, ScottGu, Rex都是血淋淋活生生的例子!),Blog的平台CS2.1就這麼一天拖過一天,眼見2007就要過去。前幾天還在想,說不定直接等CS2008算了(如果有的話)...

這陣子忽然發現自己好久沒到溪邊看小魚,"有為"程度大不如前,就偷偷地立下了志願,要開始少睡點覺,多花些時間學新東西追一下進度。昨天晚上不知吃錯了什麼藥,忽然就想把Blog平台從2.1升到2007.1,勇氣冒出來的一瞬間,我想到了練習曲那句經典Slogan--有些事現在不做,一輩子都不會做了, 那...那...那... 就衝吧!

在本機做了小測試,利用2007附的SQL指令檔,可以輕鬆將2.1的DB升級成2007版,然後將CS2007 Web的connectionString.config(連線字串從web.config拉出來了)指向DB,整個Blog就升級完成,順利得不得了。最後,再把我在Theme上做的一些手腳,例如: Accordian、Google AdSense、CAPTCHA... 等移植到CS2007上,工程不算大,但也花去近兩個小時。

不過,這幾件簡單的小事,要在ASPNix Hosting的環境上實作卻沒這麼簡單...

第一,我沒有SQL Admin的權限,所以不能利用Backup/Restore的技巧為升級失敗留後路。
第二,遠在美國的主機Ping起來就有200ms+的Delay,要將Server上現有的2.1舊檔備份或上傳2007新檔都很耗時間。
第三,同上,遠距執行SQL,執行與回應的時間都拉長數十倍,徒增等待時的刺激感。
第四,由於是Shared Web Hosting,我沒法直接管理IIS及SQL,只能用廠商給的管理介面執行簡單操作並用FTP上傳檔案,一旦有狀況,並不好Trouble-Shooting。

最後,我決定用比較保險的做法,在ASPNix的SQL上建了第二個DB,利用SQL Server Management Studio產生建立DB的Script,先Remark掉建立Foreign Key的部分,在新DB上跑一次,同樣的Schema(但不含Foreign Key)就在新DB上重生了。接著,利用INSERT INTO NewDB.dbo.csTable (Col1, Col2...) SELECT Col1, Col2... FROM OrigDB.dbo.csTable的做法,將舊DB的資料搬來一份。其實這番苦工,用Backup/Restore一下就可以搞定,在沒有足夠DB權限的狀況,又怕胡亂試把"Production環境"搞爛(我已經有100名訂戶,每天超過500人次到訪,說什麼也要付起社會責任,謎之聲: 你可不可以不要再往臉上貼金了?),只好土法鍊鋼用最穩當的做法。

因為先不設Foreign Key,各Table的資料才可以不依相依順序一一塞入,另外,有些IDENTITY欄位的Table,還要開啟IDENTITY_INSERT旗標,並明列欄位清單(也就不能用SELECT *混過去)。我另外試過MS的Database Publishing Wizard及SSMS的Export Data功能,一個是部分資料未匯入,一個是Identity會亂掉,所以只好自己來。

資料都順利塞完了,此時才加上FK、跑升級2007的SQL Script,上傳2007的Web檔案,接著第二個Blog就建立起來了。看到新Blog運作順利,才敢用新Blog取代原Blog。

看似只有幾句話帶過,實際上卻因為遠距操作,做什麼都很慢,耗掉近10個小時的時間才完成,也就是說我眼睜睜看著週日的早晨天空漸亮,才去瞇了不到四個小時,又繼續扮演在現實生活中的真實身份。

帶著一身的疲憊,看著一夜之前換新的Blog,漸漸又有點熱血的感覺。大家如果發現新版Blog什麼地方有問題的,請回應給我,我再來慢慢改。

More Posts Next page »

Search

Go

<November 2007>
SunMonTueWedThuFriSat
28293031123
45678910
11121314151617
18192021222324
2526272829301
2345678
 
RSS
【工商服務】