2/19/2010 4:05:21 AM

升級到 BlogEngine.NET 1.6.0.0 了!

543 | BlogEngine.NET

1.6.0.0 出來一陣子了,不過到過年才有空升級... 主要的原因只有一個,就是最近 spam comments 實在太多了 =_=,新版對於這類問題的處理比較像樣一點..

其它改進還有 nested comments 跟其它一堆改進,就不一一列出來了,有興趣的人可到官方網站去看看。

試了一下,升級後沒啥大問題,除了 CSS 有點走樣之外... 如果各位有發現什麼地方漏掉了,請再通知我 :D

祝大家新年快樂 :D



9/10/2008 4:44:00 AM

莫明奇妙的錯誤訊息: 找不到 VJSharpCodeProvider ?

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET | 小技巧 | 技術隨筆

話說前陣子處理了 BlogEngine.NET 升級到 1.4.5.0,另外也寫了 SecurePost.cs 這個 extension, 其時都碰過這個鳥問題,只是一直沒去理它而以。接下來為了要改 PostViewCounter.cs (BE extension, too), 又碰到... 於是就認真的研究了一下...。

過程是這樣,為了建立 BlogEngine 的開發環境,首先我從官方網站下載了 source code, 解開後編譯都沒問題,OK。

接下來 WEB 的部份我把網站上的 source code 搬過來 (不包含 ~/App_Data,太大了),編譯也 OK。

不過我要改 Counter 的 Code 啊,沒有一些 SAMPLE DATA 很難測試,只好把資料檔也搬過來.. 結果 Visual Studio 2008 就冷冷的回了這訊息給我:

(0): Build (web): The CodeDom provider type "Microsoft.VJSharp.VJSharpCodeProvider, VJSharpCodeProvider, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" could not be located.

 

我沒有漏貼前面的訊息... 的確是沒有檔名,也沒有行號(0)。我最不能忍受的就是沒頭沒尾的 ERROR MESSAGE 了。除了告訴你 "掛掉了" 之外,無頭無腦的對於追查問題實在沒什麼幫助。只好靠自己了...。雖然這是個 compile error message,不過我要 RUN 的畢竟是個 web site, 不編譯也是可以跑,除了那個惱人的錯誤訊息之外,要執行倒是沒問題。只不過編譯失敗,我就不能設中斷點,直接 F5 執行測試。雖然可以另外手動 Attach Process 的方式來除錯,不過每次都要這樣搞實在是很煩..

 

仔細想了想,沒錯,我是沒裝 Visual J#。不過我的確沒要用 Visual J# 啊,如果真的用到 J# 的話,出這訊息是應該的。訊息沒有原始檔? 也沒有錯誤行號? 那問題應該是 Global 的範圍,第一個想到的就是 web.config 是不是定義了 CodeDom 或是指定了相關的 CodeProvider ? 無奈查了一遍沒看到,VS2008 的 PROJECT 設定也沒看到引用任何 J# 相關的 LIB...

 

已經到了死馬當活馬醫的地步... 開始亂找一通碰碰運氣。搜尋了一下有沒有 *.java 的檔? OUCH,還真的有... 在 ~/App_Data/files 下找到我古董檔案,研究所時代寫的 Java Applet .... 順手試一下,刪掉後還真的就過了? 這個無頭無腦的問題,就在不知不覺中找到 solution, case closed!

 

怒... 這樣也算? 找到 .java 的程式碼,去找 VJ# 來編譯還說的過去,不過找 "source code" 找到 ~/App_Data 實在是太超過了一點... 好歹也列個要編譯那個檔案,然後找不到對應的 CodeProvider,這樣要排除問題也簡單一點...

 

結論是: 各位別太鐵齒,看來 ~/App_Data 下的檔案也是不能亂塞的...



9/6/2008 3:28:00 AM

BlogEngine Extension: Secure Post v1.0

Microsoft.NET | ASP.NET | BlogEngine.NET | 我的作品 | 技術隨筆 | BlogEngine Extension

因為家裡大人開出條件,除非新的 BLOG 系統 (就是我在用的 BlogEngine 啦) 有特定文章要輸入密碼才能看的功能,否則她就不想換系統了 (原來是用 CommunityServer 2007)。要弄密碼其實很簡單,不過過去試過 IIS 加上整合式驗證... 弄到最後該看的人看不到,也沒擋到該擋的人而作罷...。

 

仔細想了想大人的需求,要的就是簡單的控制機制。不需要先建立帳號,也不需要登入,就是特定幾篇文章要輸入暗號才能看到內容,就這樣而以。無耐 BlogEngine 還算很年輕,替它寫的 Extension 也還不多,官方網站提供了幾個 Extension 列表,找到最接近的是這個: Password Protected Post... 不過它是以登入 BE 為使用者認證的方式,再依照 ROLE 跟 CATEGORY 的配對為授權方式,來控制那些讀者能看到那些文章...。就是不想要替每個人建帳號啊,看來只好自己寫了... Orz。

 

以往都是想要作什麼很簡單,難是難在把它作出來..。現在都反過來了,工具越來越強,系統也越來越完整,難的反而是思考要怎麼作,程式碼沒幾行就搞定了。之前的文章介紹過 BlogEngine 的 Extension 機制,這次就實際來試看看。我要寫的東西很簡單,就一組密碼就好,要有夠簡單的方式讓大人能夠指定那幾篇文章是要保護的,而所有的人 (已登入的除外) 只能看到提示輸入密碼的訊息,密碼打對了才會顯示文章內容。至於密碼要不要加密? 會不會被竊聽? 不重要啦,只要保護不要遜到按右鍵簡示原始碼,密碼跟內容都看光光了就好。

 

順手寫了幾行 CODE,先驗證一下最基本的動作做不做的到 (POC: Prove Of Concept)。第一步是先把顯示內容的動作攔下來,換成制示的輸入密碼訊息... 這個簡單,沒幾行就搞定了:

 

image

直接從 CodePlex 抓下來的 Source Code, 解壓縮完就可以寫了。加上這段 CODE 並不難,整個 Extension 只有這樣而以:

 

修改 POST 內容,改成提示輸出密碼的畫面[copy code]
[Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]public class SecurePost{    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);    }    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        StringBuilder bodySB = new StringBuilder();        {           // 略。透過 bodySB 輸出 HTML        }        e.Body = bodySB.ToString();    }}
   1:  [Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]
   2:  public class SecurePost
   3:  {
   4:      static SecurePost()
   5:      {
   6:          Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
   7:      }
   8:   
   9:      private static void Post_Serving(object sender, ServingEventArgs e)
  10:      {
  11:          Post post = sender as Post;
  12:          StringBuilder bodySB = new StringBuilder();
  13:          {
  14:             // 略。透過 bodySB 輸出 HTML
  15:          }
  16:          e.Body = bodySB.ToString();
  17:      }
  18:  }

 

 

看起來 CODE 還不少,不過算一算真正在作事的都是在湊那堆 HTML ... 關鍵只有一開始去攔 Post.Serving 事件,接到自己的事件處理器 Post_Serving( ) 上,之後所有會輸出 Post 內容的地方,都會觸發這個事件。然後只要在事件處理器內去調整 Post 內容就可以了。

 

好,好的開始是成功的一半,已經完成 1/3 了 (什麼???) 第一部份的 CODE 產生的 HTML,會引導使用者輸入密碼,按下 [GO] 之後,就會連到 POST 的網址了。不過除了原本網址之外 (post.AbsoluteLink) 後面還要加上 "?pwd=xxxxxx" 帶上使用者輸入的密碼。前面講過我只要最基本的防護,其它進階的安全問題就不理它了。我只要掌握兩個原則:

  1. 密碼一定要在 SERVER 端確認 (不能讓不知道密碼的人 view source 就找到密碼)
  2. 沒輸入密碼前不能在 CLIENT 端出現 POST 內容 (不能單純的用 DHTML 把文章內容 "藏" 起來)

另外補一件事,我也不要讓全部的文章都用這種機制保護。只要有特別標示的 POST 要密碼就好。看到 BlogEngine 內建的 BreakPost 這個擴充程式,我就仿照它的作法,內文找到特定字串就啟用。我定的規則是整篇 POST 內容開頭一定要是 "[password]" 才會啟用密碼保護機制。

既然這樣,第二步也很簡單。如果密碼對,一切照原狀顯示內容。密碼不對的話就一樣攔下來...。程式碼.... 只是在第一步的程式碼多了... 兩行...

 

加上檢查密碼的 CODE[copy code]
    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        if (HttpContext.Current.Request["pwd"] == Password) return;        if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;        StringBuilder bodySB = new StringBuilder();        {           // 略。透過 bodySB 輸出 HTML        }        e.Body = bodySB.ToString();    }
   1:  private static void Post_Serving(object sender, ServingEventArgs e)
   2:  {
   3:      Post post = sender as Post;
   4:      if (HttpContext.Current.Request["pwd"] == Password) return;
   5:      if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;
   6:      StringBuilder bodySB = new StringBuilder();
   7:      {
   8:         // 略。透過 bodySB 輸出 HTML
   9:      }
  10:      e.Body = bodySB.ToString();
  11:  }

 

啥米? 就是第一部份的 CODE 加上第四及第五行就搞定了? 程式不挑的話,現在已經寫完了... 哈哈! 上面的輸入密碼畫面,輸入正確密碼後就可以看到文章內容了。我特地連網址列一起複製下來,在網址列上會看到密碼明碼。照道理應該是要先 HASH 啦,不過 CLIENT SIDE 跟 SERVER SIDE 都要有同樣的 HASH 機制才行,想用 MD5 / SHA256 之類的來算,無耐 CLIENT 要弄這些也是很煩,就決定不理它了...。明碼就明碼吧,執行後的畫面像這樣:

 

image

 

剩下的部份就沒什麼了,想想加上去好了。就是透過 BlogEngine 的 Extension Manager,讓使用者可以簡單的調整參數。要讓使用者自定的參數只有三個:

  1. 文章內容被保護時,要顯示的訊息
  2. 密碼提示
  3. 真正的密碼

這些東西自己做的話,就還得想要開檔案或寫資料庫,有點小囉唆,不過已經有 Extension Manager 了,只要在原本的 static constructor 再加幾行就搞定:

 

加上 Extension 接受的設定參數,及初始值[copy code]
    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);        ExtensionSettings settings = new ExtensionSettings("SecurePost");        settings.AddParameter(            "SecurePostMessage",            "顯示訊息:");        settings.AddParameter(            "PasswordHint",            "密碼提示:");        settings.AddParameter(            "PasswordValue",            "指定密碼:");        settings.AddValues(new string[] {            "本篇文章已受密碼保護,請依照題示輸入密碼。",             "一二三四",            "1234"});        settings.IsScalar = true;        settings.Help = "用密碼保護文章的內容。";        ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("SecurePost");    }
   1:  static SecurePost()
   2:  {
   3:      Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
   4:      ExtensionSettings settings = new ExtensionSettings("SecurePost");
   5:      settings.AddParameter(
   6:          "SecurePostMessage",
   7:          "顯示訊息:");
   8:      settings.AddParameter(
   9:          "PasswordHint",
  10:          "密碼提示:");
  11:      settings.AddParameter(
  12:          "PasswordValue",
  13:          "指定密碼:");
  14:      settings.AddValues(new string[] {
  15:          "本篇文章已受密碼保護,請依照題示輸入密碼。", 
  16:          "一二三四",
  17:          "1234"});
  18:      settings.IsScalar = true;
  19:      settings.Help = "用密碼保護文章的內容。";
  20:      ExtensionManager.ImportSettings(settings);
  21:      _settings = ExtensionManager.GetSettings("SecurePost");
  22:  }

 

我已經很努力的多撐幾行了... 不過也只有這廿行,寫完了...。整個 .cs 檔案直接丟到 ~/App_Code/Extension 就算安裝完成。用管理者身份登入 BE 後,在 Extension 那頁可以看到:

 

image

不錯,SecurePost 已經出現在 Extension Manager 裡了。因為有加上 settings 的程式碼,所以右邊有 [編輯] 的字樣出現。點下去之後會到這個畫面:

 

image

嗯,看起來真專業,沒想到從頭到尾所有的 CODE 還不到一百行...。幾十行 CODE 寫出來的 Extension 就可以唬人了.. :D,試看看還真的會動耶 (廢話)。早知道寫起來那麼快,當初就不花那麼多時間去找人家寫好的了...。最後附上整段完整的程式碼,有需要的人就拿去用吧! 用法很簡單,全部複製下來 (可以按 [COPY CODE] 就好),存檔,把檔案放在 ~/App_Code/Extension/SecurePost.cs 下,然後用管理者身份進入 BlogEngine Extension Manager 改一改就好了!

 

大功告成! 這個 Extension 如果對你有用的話就拿去用吧,要散佈也歡迎,不過只有個小要求,請不要把程式碼存到別的地方供人下載,請直接提供我這篇文章的網址就好。覺的好用就留個話給我,要幫我推一下文或讚助就更好了 :D,謝謝收看!

 

 

--

完整的 SecurePost.cs 程式碼[copy code]
using System;using System.Web;using System.Web.UI;using BlogEngine.Core.Web.Controls;using BlogEngine.Core;using System.Text;[Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]public class SecurePost{    private static string SecurePostMessage { get { return _settings.GetSingleValue("SecurePostMessage"); } }    private static string Password { get { return _settings.GetSingleValue("PasswordValue"); } }    private static string PasswordHint { get { return _settings.GetSingleValue("PasswordHint"); } }    private static ExtensionSettings _settings = null;    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);        ExtensionSettings settings = new ExtensionSettings("SecurePost");        settings.AddParameter(            "SecurePostMessage",            "顯示訊息:");        settings.AddParameter(            "PasswordHint",            "密碼提示:");        settings.AddParameter(            "PasswordValue",            "指定密碼:");        settings.AddValues(new string[] {            "本篇文章已受密碼保護,請依照題示輸入密碼。",             "一二三四",            "1234"});        //settings.ShowAdd = false;        //settings.ShowDelete = false;        //settings.ShowEdit = true;        settings.IsScalar = true;        settings.Help = "用密碼保護文章的內容。";        ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("SecurePost");    }    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        if (HttpContext.Current.User.Identity.IsAuthenticated == true) return;        if (HttpContext.Current.Request["pwd"] == Password) return;        if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;        StringBuilder bodySB = new StringBuilder();        {            bodySB.AppendFormat(                "<b>{0}</b><p/>",                HtmlEncode(SecurePostMessage));            if (e.Location == ServingLocation.Feed)            {            }            else            {                bodySB.Append("<div>");                bodySB.AppendFormat(                    @"請輸入密碼(提示: <b>{0}</b>): <input id=""postpwd"" type=""password""/><button onclick=""document.location.href='{1}'+'?pwd='+escape(this.parentNode.all.postpwd.value);"">GO</button>",                     PasswordHint,                    post.AbsoluteLink);                bodySB.Append("</div>");            }        }        e.Body = bodySB.ToString();    }    private static string HtmlEncode(string text)    {        return HttpContext.Current.Server.HtmlEncode(text);    }}
   1:  using System;
   2:  using System.Web;
   3:  using System.Web.UI;
   4:  using BlogEngine.Core.Web.Controls;
   5:  using BlogEngine.Core;
   6:  using System.Text;
   7:   
   8:   
   9:   
  10:   
  11:  [Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]
  12:  public class SecurePost
  13:  {
  14:      private static string SecurePostMessage { get { return _settings.GetSingleValue("SecurePostMessage"); } }
  15:      private static string Password { get { return _settings.GetSingleValue("PasswordValue"); } }
  16:      private static string PasswordHint { get { return _settings.GetSingleValue("PasswordHint"); } }
  17:   
  18:      private static ExtensionSettings _settings = null;
  19:   
  20:      static SecurePost()
  21:      {
  22:          Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
  23:   
  24:          ExtensionSettings settings = new ExtensionSettings("SecurePost");
  25:   
  26:          settings.AddParameter(
  27:              "SecurePostMessage",
  28:              "顯示訊息:");
  29:          settings.AddParameter(
  30:              "PasswordHint",
  31:              "密碼提示:");
  32:          settings.AddParameter(
  33:              "PasswordValue",
  34:              "指定密碼:");
  35:   
  36:          settings.AddValues(new string[] {
  37:              "本篇文章已受密碼保護,請依照題示輸入密碼。", 
  38:              "一二三四",
  39:              "1234"});
  40:   
  41:          //settings.ShowAdd = false;
  42:          //settings.ShowDelete = false;
  43:          //settings.ShowEdit = true;
  44:          settings.IsScalar = true;
  45:          settings.Help = "用密碼保護文章的內容。";
  46:   
  47:          ExtensionManager.ImportSettings(settings);
  48:   
  49:          _settings = ExtensionManager.GetSettings("SecurePost");
  50:   
  51:      }
  52:   
  53:      private static void Post_Serving(object sender, ServingEventArgs e)
  54:      {
  55:          Post post = sender as Post;
  56:   
  57:   
  58:          if (HttpContext.Current.User.Identity.IsAuthenticated == true) return;
  59:          if (HttpContext.Current.Request["pwd"] == Password) return;
  60:          if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;
  61:   
  62:   
  63:          StringBuilder bodySB = new StringBuilder();
  64:          {
  65:              bodySB.AppendFormat(
  66:                  "<b>{0}</b><p/>",
  67:                  HtmlEncode(SecurePostMessage));
  68:   
  69:              if (e.Location == ServingLocation.Feed)
  70:              {
  71:              }
  72:              else
  73:              {
  74:                  bodySB.Append("<div>");
  75:                  bodySB.AppendFormat(
  76:                      @"請輸入密碼(提示: <b>{0}</b>): <input id=""postpwd"" type=""password""/><button onclick=""document.location.href='{1}'+'?pwd='+escape(this.parentNode.all.postpwd.value);"">GO</button>", 
  77:                      PasswordHint,
  78:                      post.AbsoluteLink);
  79:                  bodySB.Append("</div>");
  80:              }
  81:          }
  82:          e.Body = bodySB.ToString();
  83:      }
  84:   
  85:      private static string HtmlEncode(string text)
  86:      {
  87:          return HttpContext.Current.Server.HtmlEncode(text);
  88:      }
  89:  }


8/29/2008 1:52:00 AM

升級到 BlogEngine.NET 1.4.5.0 了

543 | BlogEngine.NET

哈,升級還蠻簡單的,一下子就搞定了... 特此留念 :D

 

枉我還大陣仗的對付它,VSS (Visual Source Safe), VSS (Volume Shadow copy Service) 都搬出來用了,結果只是目錄搬一搬就好... 咳咳。

除了 Bot Checker 沒有搬過來之外,其它應該都搞定了吧? 如果有發現我的網站有那裡沒弄好,記得留言跟我講一下 :D



7/7/2008 1:48:44 AM

為什麼一堆推文的按鈕都不見了?

Microsoft.NET | BlogEngine.NET

 

image

前兩天突然發現,怎麼一堆文章原本有推文數字的,怎麼都不見了?

 

image

網站有問題怎麼可以不追查個水落石出? 連到推推王找一下當時的推文... 耶? 還在啊,旁邊還有推文記錄...。

 

image

怪的是從推推王點回來看我的文章,Oops!

 

 

越看越不對,把網址印出來比對一下才發現,我這邊的網址已經不一樣了!! 

http://columns.chicken-house.net/post/FlickrProxy-1---Overview.aspx (推推王那邊的網址)

http://columns.chicken-house.net/post/FlickrProxy-1-Overview.aspx (我這邊實際的網址)

 

 

真妖獸,馬上聯想到前幾天升級 BE1.4 可能會有影響,就搬出 VSS 來比對一下,果然 1.3 跟 1.4 在自動產生 SLUG (SLUG 就是指 POST 網址後面那一串) 的規責有調整過:

image

 

 

嗯,肉眼看的出的調整,包括逗號被移掉,連續多個 -- 也會被替換成 - ,這個案例就是原網址的 "---" 換成 "-" 後就連不到了 :@

 

本來想寫個程式修一下,後來想想跟本沒幾篇,就直接到推推王改掉了事,哈哈... 結案!



7/6/2008 4:13:00 AM

[BlogEngine Extension] PostViewCount 1.0

Microsoft.NET | ASP.NET | BlogEngine.NET | 我的作品 | 技術隨筆 | BlogEngine Extension

這篇拖好久了,本來上禮拜要寫,結果正好碰到 BlogEngine 1.4 RELEASE,就一直拖到現在...。之前找到一個給 BlogEngine 用的 Counter Extension,以功能來說還不錯用,不過用久了就開始不滿足了。正好翻到這篇教學文章,算是官方文章了吧 (BlogEngine 作者之一寫的教學文)? 所以就動起自己寫的念頭。舊的其實沒什麼不好,不過缺了這幾項我想要的功能:

  1. 只有計 Total Count (謎: 不然你要 counter 記什麼?)
  2. 資料檔的結構及 I/O 的設計有點 Orz...
    1. 讀寫 XML 的 CODE 寫的很... Orz
    2. 沒有處理同時讀寫的問題 (後面寫的資料可能會蓋掉前面的,我的點閱率不知道少了幾百次 :D)
    3. 要有 CACHE 來加速處理速度

 

既然要重寫,當然要寫個合用的. 底下是我對於新的 COUNTER 期望:

  1. 要能記流水帳。
    流水帳就是不只要記總數,我還要知道每次點閱的時間,來源 IP ... 等等
    ( darkthread 指示: 當你流量大的時後就不會去在意這個了... Orz, 真是一針見血... )
  2. 要處理多執行緒下讀寫資料檔的問題,這部份 Code 必需為 ThreadSafe。
  3. 妥善利用 CACHE,降低 (2) 的複雜度。
  4. COUNTER COMPACT
    配合 (1) 的需求,流水帳記錄太多的話也會造成問題,COUNTER要能適當的刪除舊的 HIT RECORD。

 

決定好後就動工了! 既然問題都圍繞在 data storage 上,先來看看原來的檔案格式:

原有的 ~/App_Data/PostViews.xml 片段:[copy code]
<?xml version="1.0" encoding="utf-8" standalone="yes"?><posts>    <post id="b43ec49e-e9a2-4696-bcc7-2ba1667ecda9">781</post>    <post id="f1411c11-11ed-4f35-b383-0c6c8b2b963a">603</post>    <post id="e7b57492-652b-4247-bcd4-bc3ac2e56318">589</post>    <post id="7e2c2c88-240c-40ea-8477-2c96880adc8e">556</post>    <post id="0fda9c32-d294-4f09-85cd-41dab8e677cb">678</post>    ......</posts>
   1:  <?xml version="1.0" encoding="utf-8" standalone="yes"?>
   2:  <posts>
   3:      <post id="b43ec49e-e9a2-4696-bcc7-2ba1667ecda9">781</post>
   4:      <post id="f1411c11-11ed-4f35-b383-0c6c8b2b963a">603</post>
   5:      <post id="e7b57492-652b-4247-bcd4-bc3ac2e56318">589</post>
   6:      <post id="7e2c2c88-240c-40ea-8477-2c96880adc8e">556</post>
   7:      <post id="0fda9c32-d294-4f09-85cd-41dab8e677cb">678</post>
   8:      ......
   9:  </posts>

 

很普通的格式,配合我的需求,新的檔案結構我打算改成這樣:

新的 ~/App_Code/counter/{post-id}.xml 檔的片段內容:[copy code]
<?xml version="1.0" encoding="utf-8"?><counter base="8828">  <hit time="2008-06-29T12:42:51" referer="" remote-host="66.249.73.185" user-agent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" />  <hit time="2008-06-29T13:04:15" referer="http://www.google.com.tw/search?complete=1&amp;hl=zh-TW&amp;cr=countryTW&amp;rlz=1B3GGGL_zh-TWTW237TW238&amp;q=%E9%A6%99%E6%B8%AFg9&amp;start=30&amp;sa=N" remote-host="124.10.1.162" user-agent="Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9) Gecko/2008052906 Firefox/3.0" />  <hit time="2008-06-29T13:04:20" referer="" remote-host="66.249.73.185" user-agent="Mediapartners-Google" />  ......</counter>
   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <counter base="8828">
   3:    <hit time="2008-06-29T12:42:51" referer="" remote-host="66.249.73.185" user-agent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" />
   4:    <hit time="2008-06-29T13:04:15" referer="http://www.google.com.tw/search?complete=1&amp;hl=zh-TW&amp;cr=countryTW&amp;rlz=1B3GGGL_zh-TWTW237TW238&amp;q=%E9%A6%99%E6%B8%AFg9&amp;start=30&amp;sa=N" remote-host="124.10.1.162" user-agent="Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9) Gecko/2008052906 Firefox/3.0" />
   5:    <hit time="2008-06-29T13:04:20" referer="" remote-host="66.249.73.185" user-agent="Mediapartners-Google" />
   6:    ......
   7:  </counter>

 

其中, /counter/@base 是給你作弊用的,這數字你填多少就是多少,然後再加上底下有幾筆 <hit /> 記錄,總數就出來了。很簡單的格式,而流水帳就是記在 <hit /> 這個 XML ELEMENT 的 ATTRIBUTES 上。目前記的有時間,IP,REFERRER 而以。等到網站人數暴增 (幻想嘛?),COMPACT的動作就免不了了,預留的 /counter/@base 就派上用場了。COUNTER 在必要時就能刪掉多餘的 <hit /> 記錄,再把刪掉的筆數加到 /counter/@base 上,讓最後點擊總數不變,又能控制記錄檔的大小。

 

邏輯確定後就可以開始動工了,不過另外要解決技術問題,就是 ThreadSafe 的部份。這部份讓我煩惱了一下,因為在 File System 就提供了 FILE LOCK 的機制可以用,不過取得 LOCK 失敗是會引發 EXCEPTION,而不是像其它的 THREAD 控制機制會 WAIT。因此最後我決定 FILE LOCK 的機制做第二層保障 ,主要的 LOCK 機制還是自己來,用基本的 Monitor 來實作。要實做 LOCK 的前題,是要有明確的 LOCK "對像"。兩個 thread lock 同一個物件,後面 lock 的 thread 會被 block 暫停執行,直到前一個先 lock 同一個物件的 thread 釋放之後才會被喚醒,因此要先解決兩個同 ID 的 COUNTER 能拿到同一個物件,才能實作 LOCK 機制。這物件就好幾位合適的候選人,第一種方法是 COUNTER 本身,第二種方法則是替每個 counterID 產生一個無用的物件,就單純拿來 LOCK 用。

 

要用 (1) 成本太高,一來就必需實作 Flyweight 這個 design pattern,這個設計模式並不難,只要實作 factory pattern再搭配個 Dictionary<string, Counter> 物件就可以搞定,但是我最後沒有選擇這個作法。因為最糟的情況下有可能會讓整個系統可能會用到的 counter 通通被放到這個 dictionary ,沒有被釋放回收的機會,因為沒有明確的時間點可以把這物件從 dictionary 移掉,移不掉的話永遠就會留著一個 object reference 指向這 COUNTER 物件,reference 只要存在,它就永遠不會被 GC 收掉...。不過這還是有解,只要用 WeakReference 就可以解決了,不過我只是要作個簡單的 Counter,搬出這一堆東西會不會太過頭了?

 

因此我選擇了第二個方法。一樣用 flyweight pattern,只不過我放的是拿來 lock 的物件。我是直接 new object() 就拿來用了,物件很小我就不用耽心建立太多個又不能回收的問題...,而 counter 就讓它回歸最簡單的用法,需要就 new 一個出來,用完就丟著等著被回收。

 

每次都是講的話比 CODE 還多,來看看程式碼:

 

COUNTER 物件的 SYNC 機制[copy code]
        // 所有 COUNTER 用的 SYNC 物件 DICTIONARY        private static Dictionary<string, object> _counter_syncroot = new Dictionary<string, object>();        // 取得這個 COUNTER 用的 SYNC 物件        private object SyncRoot        {            get            {                return Counter._counter_syncroot[this._counterID];            }        }        private Counter(string counterID)        {            this._counterID = counterID;            //            //  建立 SYNC 物件 (如果沒有的話)            //            lock (Counter._counter_syncroot)            {                if (Counter._counter_syncroot.ContainsKey(this._counterID) == false)                {                    Counter._counter_syncroot.Add(this._counterID, new object());                }            }            //  略 ....        }        public void Hit()        {            lock (this.SyncRoot)            {                //                //  LOCK 後再開始更新檔案內容。 程式碼 略...                //            }        }
   1:  // 所有 COUNTER 用的 SYNC 物件 DICTIONARY
   2:  private static Dictionary<string, object> _counter_syncroot = new Dictionary<string, object>();
   3:  // 取得這個 COUNTER 用的 SYNC 物件
   4:  private object SyncRoot
   5:  {
   6:      get
   7:      {
   8:          return Counter._counter_syncroot[this._counterID];
   9:      }
  10:  }
  11:  private Counter(string counterID)
  12:  {
  13:      this._counterID = counterID;
  14:      //
  15:      //  建立 SYNC 物件 (如果沒有的話)
  16:      //
  17:      lock (Counter._counter_syncroot)
  18:      {
  19:          if (Counter._counter_syncroot.ContainsKey(this._counterID) == false)
  20:          {
  21:              Counter._counter_syncroot.Add(this._counterID, new object());
  22:          }
  23:      }
  24:      //  略 ....
  25:  }
  26:  public void Hit()
  27:  {
  28:      lock (this.SyncRoot)
  29:      {
  30:          //
  31:          //  LOCK 後再開始更新檔案內容。 程式碼 略...
  32:          //
  33:      }
  34:  }

 

這問題解決掉後,剩下的就是單純把我要的邏輯實作出來,這部份我相信讀者的程度都不用我多講了,有需要的看 CODE 就看的懂。請直接下載最後面的程式碼就好。

 

另外一個要提一下的是跟 BlogEngine 本身 Extension 相關的,這部份也花了點時間研究該怎麼寫。BlogEngine 的 Extension 寫法比較特別一點,一般這種外掛都是採 Provider 的方式實作 ( factory pattern 加上 abstract class ),先定義好這個 Provider 能作什麼事,然後每個寫 Extension 的人就自己繼承這類別來修改,再靠 Factory 動態的建立正確的 Extension 物件來使用。不過在這部份 BlogEngine 採用完全不同的作法來設計它的架構: Event Handler。

 

Provider 依賴的是事先定義好的 ProviderBase (abstract class) 類別。這個類別定義了多少東西給底下的人覆寫,就決定了寫外掛的人能處理多少事。好處是簡單,架構清楚。缺點是能讓你擴充的功能,在 DESIGN TIME 就決定了,要多一個能 "擴充" 的地方,就得改 ProviderBase 類別定義,這很有可能會讓現有的 Extension 不能跑...。換成 EVENT 的方式就沒有跟程式碼綁的那麼緊了。多了新的功能,多定義一些事件就夠了。BlogEngine 就是採這種方式來實作它的 Extension .. 

 

另外比較特別的是,BlogEngine 替每個 Extension 規劃好存放設定的地方。 1.3 版是所有的 Extension 共用一個設定檔, 1.4 則是有獨立的設定檔可以用。不過這些改變倒是沒有影響到它提供的 API。對於 API 來說,設定檔提供每個 Extension 一個像是 DataTable 那樣的 data storage, 讓你自訂欄位名,型別,然後能讓你一筆一筆的加進去,可以有多筆資料,而 BlogEngine Runtime 會負責幫你管理好這些設定。

 

這部份帶入門就好,用法還是去查官方文件比較快。我簡單貼一下這部份 CODE 跟畫面上提供的設定頁面給大家看看:

準備設定值的 SCHEMA 及載入目前的設定值[copy code]
    public PostViewCounter()	{        Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);        ExtensionSettings settings = new ExtensionSettings("PostViewCounter");        settings.AddParameter(            "MaxHitRecordCount",             "最多保留筆數:");        settings.AddParameter(            "HitRecordTTL",             "最長保留天數:");        settings.AddValues(new string[] { "500", "90" });        //settings.ShowAdd = false;        //settings.ShowDelete = false;        //settings.ShowEdit = true;        settings.IsScalar = true;        settings.Help = "設定 counter hit records 保留筆數及時間。只有在筆數限制內且沒有超過保留期限的記錄才會被留下來。";                ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("PostViewCounter");    }
   1:    public PostViewCounter()
   2:  {
   3:        Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);
   4:        ExtensionSettings settings = new ExtensionSettings("PostViewCounter");
   5:        settings.AddParameter(
   6:            "MaxHitRecordCount", 
   7:            "最多保留筆數:");
   8:        settings.AddParameter(
   9:            "HitRecordTTL", 
  10:            "最長保留天數:");
  11:        settings.AddValues(new string[] { "500", "90" });
  12:        //settings.ShowAdd = false;
  13:        //settings.ShowDelete = false;
  14:        //settings.ShowEdit = true;
  15:        settings.IsScalar = true;
  16:        settings.Help = "設定 counter hit records 保留筆數及時間。只有在筆數限制內且沒有超過保留期限的記錄才會被留下來。";
  17:        ExtensionManager.ImportSettings(settings);
  18:        _settings = ExtensionManager.GetSettings("PostViewCounter");
  19:    }

 

 

對應的設定頁面:

image

 

 

 

最後講了半天,真正想自己動手寫的人應該不多吧 :D,只是想下載回去裝來用的人就不用聽我前面廢話一堆了,只要下載這檔案,放到 ~/App_Code/Extension 下,就安裝完成了... 咳咳,連安裝手冊都省了。檔案 COPY 好後就會在 Extension Manager 裡看到我寫的外掛,就可以開始用了。有任何意件歡迎留話給我 :D

 

檔案下載: http://columns.chicken-house.net/file.axd?file=PostViewCounter.cs



7/2/2008 3:08:02 AM

[BlogEngine.NET] Widgets

543 | BlogEngine.NET | 技術隨筆

原本只是想找找有沒有 BlogEngine.NET Extension 存放自訂設定值的說明,無意間找到這個令人 Orz 的東西,BlogEngine.NET 專用的 Widgets ...

Widgets 這名字到處都有人用,指的不外乎是能留在桌面或是網頁的 "小工具"。拜物件技術所賜,這種東西越來越好作了。當年還在用 ET 的時代,一篇報告要插張圖,就有一堆技巧得拿出來用... 先下好控制碼把位子空出來,列印... 然後再印張圖,貼上去..

中間年代有些工具可以直接插圖的就不管了,真正有革命性的改變,是 WIN3.1 時代的 OLE (Object Linkage and Embedded) ,其實這也不是 M$ 先發展出來的,較早是 APPLE 的 OpenDoc ... 這些物件的技術開始可以讓兩種獨立的 AP,在同一份文件或是畫面上共同處理一樣的資料...。

幹嘛扯這些? 因為我碩士論文就是弄這些五四三的物件導向技術,哈哈...。沒什麼,只是看到 BlogEngine Widget 這麼好寫,沒幾行 CODE 就搞定,那當年我的研究論文跟本是寫好玩的.. @_@

 

http://rtur.net/blog/post/2008/03/24/BlogEngine-Widgets-Tutorial.aspx

原本要找 Extension, 後來逛到這位強者的網站,它擺了一段很簡單的 CODE,就是之前研究過一陣子的 FlickrNet。它寫了一個簡單的 USER CONTROL,透過 FlickrNet 抓幾張圖回來,顯示在 User Control 的範圍內。

另外他寫了另一個 User Control,用來編輯第一個 User Control 可能會用到的設定值,畫面上幾個 Text Box 也搞定了。

然後,放到 BlogEngine.NET 的 ~/Widgets/ 目錄下就好了? 簡單到想打人...

 

現在回頭來看看,這樣做出來的 widget 能幹嘛? 我現在用的版面,右側有一堆 "BOX",有 Google 的廣告,有 "安德魯是誰?",有最新回應...,沒錯,在 BlogEngine 1.4 之後,這些就變成真正的元件了。大概就像 M$ 的 Web Parts 一樣。網頁的主人可以很簡單的在畫面上拉一塊新的 "widget" 出來,直接用拖拉的方式放到喜歡的位子,按下 [EDIT],畫面就會切到編輯用的 User Control,OK就存檔...

要做到這堆事,你只要會寫 USER CONTROL,照 BE 的規舉,繼承指定的 CLASS,把檔案放到指定的目錄就好了... Orz

 

有時後東西弄的太簡單也很令人洩氣,尤其是 BLOG 剛搬完家,差不多要完工時 @_@,才發現 BlogEngine 在 2008/06/30 推出 1.4 版,主要就是加上 widget 的功能... 哈哈,那個小熊子應該比我更想哭吧 (H)

 

不過事情倒還很順利,之前有作好準備,花了一兩個小時試一下就動工了,動啥工? 看看底下的版本... 已經搬到 1.4 版了 :D,唯一美中不足的是,過去花太多工夫在這個樣板上面,所以只好連樣板一起搬過來... 而這個樣版並不支援 Widget 的功能。看來又有得忙了...



6/29/2008 11:04:00 PM

[BlogEngine.NET] 改造工程 - 整合 FunP 推推王

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET

古早以前,曾替我的 BLOG 加上推推王的小貼紙,不過當時也僅止於把 CODE 加上去而以,成效不大好...。這次搬家搬到 BlogEngine 後,又開始一樣的循環了..,要不要加上這些共用書籤? 要加那一套? 目前台灣用的最多就是黑米推推王了。

原本挑了黑米,只因為它有提供 [黑米卡],正好取代掉 BlogEngine 右邊那塊 [關於作者] .. 不過試用的情況不怎麼理想,除了速度有點慢之外,同一頁放太多 (幾十個) 的速度也很慢,也許跟 BlogEngine 我選用的樣板有點不合,速度太慢時有時整個版面就毀了,下載到一半就掛掉...

相對之下,看了看 FunP 提供的 SCRIPT,看起來 CODING STYLE 比較合我的胃口,速度也快一些,沒碰到會讓我版面掛掉的問題。另外使用上的流程 FunP 也簡單一點,本來想兩家的書籤都放的,到最後就決定支持一下交大的學弟,就全力跟 FunP 推推王整合好了。

動手前先計劃了一下,毫無目的的把一堆 CODE 加上去,我最忌誨這樣弄了,看起來一點主題都沒有。常看到別人的 BLOG 滿滿一堆標籤,從國內的 FunP,黑米,MyShare,到國外的DELICIUS,還有一堆叫不出名字的,一字排開落落長...

BlogEngine 原本也有內建一些,不過被我拿掉了。底下列出我調整的前後差異:

  1. 捨棄內建的 Rating 機制,直接用推文就好。
  2. 版面我希望類似原有 CS 的樣式,正好拿推文按鈕來取代原本的計數器
  3. 就鎖定一個共用書簽就好,把原有左下方的其它都拿掉
  4. Tags 我也決定捨棄不用,以分類為主。因此左下的 Tags 就移掉了
  5. 分類放右下很礙眼,移到右上
  6. 不爽被盜文,加上一段智財權的聲明
  7. 加上自己補的計數器... (說來話長,請見下一篇)
  8. 推文時自動帶出文章的基本資訊,如標題,內文,標簽等等

 

原 CS 的樣式:

image

 

 

修改前:

image

修改後:

image

 

看了一下推推王的工具,不外乎都是插入一段 <SCRIPT> 標簽,然後用 document.write( ) 或是 eval 等等 client side script 的方式產生片 HTML Code, 缺點就是繞了一大圈,出了問題也常讓人搞不清楚問題在那裡。花了點時間追一下,追出最後插在網頁的 HTML CODE 長這樣:

[copy code]
    <IFRAME	width=60 	height=55	marginWidth=0 	marginHeight=0 	frameBorder=0 	scrolling=no 	src="http://funp.com/tools/buttoniframe.php?url=xxxxxxxxxxxxxx&s=1" mce_src="http://funp.com/tools/buttoniframe.php?url=xxxxxxxxxxxxxx&s=1">    </IFRAME>
   1:    <IFRAME
   2:  width=60 
   3:  height=55
   4:  marginWidth=0 
   5:  marginHeight=0 
   6:  frameBorder=0 
   7:  scrolling=no 
   8:  src="http://funp.com/tools/buttoniframe.php?url=xxxxxxxxxxxxxx&s=1">
   9:    </IFRAME>

 

 

 

看起來就是直接產生一個帶著指定參數的 <IFRAME ...>,於是我在 BlogEngine Themes 版面就直接產生 <IFRAME> ...底下是 BlogEngine THEME 目錄下的 PostView.ascx 片段:

在 PostView.ascx 顯示推文按鈕的片段[copy code]
    <%        Regex _stripHTML = new Regex("<[^>]*>", RegexOptions.Compiled);        string PostTextContent = _stripHTML.Replace(Post.Content, "");        int maxLength = 70;                    string EncodedAbsoluteLink = Page.Server.UrlEncode(Post.AbsoluteLink.ToString());        string EncodedPostTitle = Page.Server.UrlEncode(Post.Title);        string EncodedPostBody = Page.Server.UrlEncode((PostTextContent.Length > maxLength) ? (PostTextContent.Substring(0, maxLength) + "...") : (PostTextContent));        string TagsQueryString = "";        foreach (BlogEngine.Core.Category cat in Post.Categories)        {            TagsQueryString += string.Format("&tags[]=" + Page.Server.UrlEncode(cat.Title));        }    %>        <IFRAME	width=60 	height=55	marginWidth=0 	marginHeight=0 	frameBorder=0 	scrolling=no 	src="http://funp.com/tools/buttoniframe.php?url=<%=EncodedAbsoluteLink %>&s=1">    </IFRAME>
   1:    <%
   2:        Regex _stripHTML = new Regex("<[^>]*>", RegexOptions.Compiled);
   3:        string PostTextContent = _stripHTML.Replace(Post.Content, "");
   4:        int maxLength = 70;
   5:        string EncodedAbsoluteLink = Page.Server.UrlEncode(Post.AbsoluteLink.ToString());
   6:        string EncodedPostTitle = Page.Server.UrlEncode(Post.Title);
   7:        string EncodedPostBody = Page.Server.UrlEncode((PostTextContent.Length > maxLength) ? (PostTextContent.Substring(0, maxLength) + "...") : (PostTextContent));
   8:        string TagsQueryString = "";
   9:        foreach (BlogEngine.Core.Category cat in Post.Categories)
  10:        {
  11:            TagsQueryString += string.Format("&tags[]=" + Page.Server.UrlEncode(cat.Title));
  12:        }
  13:    %>
  14:    <IFRAME
  15:  width=60 
  16:  height=55
  17:  marginWidth=0 
  18:  marginHeight=0 
  19:  frameBorder=0 
  20:  scrolling=no 
  21:  src="http://funp.com/tools/buttoniframe.php?url=<%=EncodedAbsoluteLink %>&s=1">
  22:    </IFRAME>

 

這是推文的部份,如果要張貼的話就不一樣了,要放的是把文章的預設資訊都帶過去,免的到時要重新輸入一次... 這部份的 CODE 比較囉唆,不過產生出來的 CODE 比較單純,就是個 <A> LINK 而以,不過因為帶的資訊比較多,所以部份 CODE 是由上面的 CODE 事先產生好,這裡才拿來用的:

產生推文按鈕的部份[copy code]
   1:    <a href="http://funp.com/push/submit/add.php?url=<%=EncodedAbsoluteLink %>&s=<%=EncodedPostTitle %>&t=<%=EncodedPostBody %><%=TagsQueryString %>&via=tools" title="貼到funP">
   2:  <img src="http://funp.com/tools/images/post_03.gif" border="0"/>
   3:    </a>

 

 

 

 

果然效果好多了,也不會再碰到版面掛掉等等鳥問題,只不過載入 [封存] 頁面時,一次四五百個 <IFRAME> 同時在跑,IE也是跑的很吃力....

image

 

 

 

同樣的技巧也拿來修改 ~/archive.aspx 這頁。這頁原本是把所有的文章按照分類一篇一篇列出來,捨棄原有的 RATING 機制不用,直接用推文的機制取代。因此這頁原本顯示 RATING 分數的地方就被我改成推推王的推薦次術了。我的文章有兩百多篇,出現過的地方都列一次,加一加總共會出現近五百個推文按鈕 @_@,自然也不可能用原本官方的作法產生按鈕,直接用上面挖出來的方法,修改 archive.aspx.cs:

~/archive.aspx.cs 顯示推文按鈕的片段程式[copy code]
          if (BlogSettings.Instance.EnableRating)          {              HtmlTableCell rating = new HtmlTableCell();              rating.InnerHtml = string.Format(                @"<IFRAME   marginWidth=0   marginHeight=0   src='http://funp.com/tools/buttoniframe.php?url={0}&amp;s=12'   frameBorder=0   width=80   scrolling=no   height=15></IFRAME>",                 (post.AbsoluteLink.ToString()));              rating.Attributes.Add("class", "rating");              row.Cells.Add(rating);          }
   1:            if (BlogSettings.Instance.EnableRating)
   2:            {
   3:                HtmlTableCell rating = new HtmlTableCell();
   4:                rating.InnerHtml = string.Format(
   5:                  @"
   6:  <IFRAME 
   7:    marginWidth=0 
   8:    marginHeight=0 
   9:    src='http://funp.com/tools/buttoniframe.php?url={0}&amp;s=12' 
  10:    frameBorder=0 
  11:    width=80 
  12:    scrolling=no 
  13:    height=15>
  14:  </IFRAME>", 
  15:                  (post.AbsoluteLink.ToString()));
  16:                rating.Attributes.Add("class", "rating");
  17:                row.Cells.Add(rating);
  18:            }

 

嗯,看起來效果好多了,至少我自己看起來順眼多了 :D

下一篇預告一下,下一篇會推出我自己寫的 PostViewCounter Extension,主要就是拿來作每篇文章的點閱率。BlogEngine 沒內建,找來的現成的又不是很合用,索性就自己寫了一個,請期待續篇 :D



6/25/2008 2:48:00 AM

Bot Checker 回來了!

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET | 我的作品

哈哈,終於加回來了 :D

 

為什麼原本在 CS 上很簡單就加上去的 Bot Checker, 在 BE 上弄到現在才好? 原因只有一個,就是 BE 在張貼回應時用了不少 AJAX 的機制,變的要插一段 CODE 進去要追半天 @_@...

很諷刺的是,AJAX 其實是 Community Server 用的比較兇,到處都要來一下... 反而 BlogEngine.NET 就中規中舉多了,很多地方就都乖乖的用 PostBack.. 唯讀回應的地方很突兀,感覺好像是特別要現一下回應的那個 TEXT EDITOR 還有 BBCODE 預覽的樣子... 那邊的 CODE 弄的實在是有點亂...

也是之前幾次都沒認真追啦... 追到一半覺的煩就去逛網站了,哈哈... 今天狠下心把它解決掉了。只不過卡著 AJAX,又不想跟它奮鬥,所以有些地方就偷懶混過去了。什麼意思? 意思就是攤開 HTML 原始碼,這個 Bot Checker 也是防不了 Bot 啦,不過我就賭我的站還有我的 Bot Checker 沒大到有人願意寫 Bot 來攻擊我.. 哈哈.. 來看看效果:

image

 

當然,張貼出去的回應也會帶著 Bot Checker 的問題。只不過礙於 AJAX,一堆東西被迫要移到 CLIENT SIDE 來處理,這邊就偷懶,題目產生完就先填到 [評論] 欄了,各位在填回應時,不爽附上 "芭樂雞萬歲" 的話,還是可以把它刪掉...

 

image

 

 

--
題外話,在追 BE 的程式碼的過程中,發現 BE 也有 CAPTCHA ? 不過還真的找不到怎麼把它打開... 有興趣用 BlogEngine.NET 又想用正統的 CAPTCHA 驗證的人可以試看看



6/21/2008 4:10:22 AM

[BlogEngine.NET] 改造工程 - CS2007 資料匯入

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET | CS | 技術隨筆

自從搬到 BlogEngine.NET 之後,每天晚上都沒閒著,一點一點的慢慢把整個網站改成我要的樣子... 資料庫 / 檔案是我最在意的,當然要優先顧好,光這部份就花了一個多禮拜...

最早找到了 CS2007 --> BlogML --> BlogEngine,大概把我想要的 90% 資料都轉過來了,不過看到那 10% 的資訊沒過來,心裡就很不是滋味...

 

"這是我要一直保留下來的資料耶,現在沒把資料補回來,以後就永遠補不回來了..."

 

就是心裡一直這樣想,所以... 重匯吧! 不然以後後悔也是補不回來的。首先當然是想先從 BlogEngine 匯入 BlogML 的工具下手,看了一下,沒提供 Source Code ? 再看一下,一篇文章一個 Web Service Call,一則回應也是一個 Web Service Call ... 感覺起來有點沒效率,不過不管了,我內容也不多 (兩百多篇文章) ... 既然有 Web Services,先看看它的 WSDL ...

 

image
http://columns.chicken-house.net/api/blogImporter.asmx

 

沒幾個 WebMethod 嘛,不過看一看它的 Interface 就已經漏掉很多資訊沒轉進來了 (像是我要的原 CS PostID,事後作新舊網址對照表用,還有 PageViewCount... etc),要改原程式也是個大工程,不但 CLIENT 要改,WEB METHOD 也要擴充... 我又不是非得遠端用 WEB SERVICES 執行匯入不可,就當下決定另外寫一個匯入程式還比較快...。重寫的話就得研究它的寫法,正好每個 WebMethod 都只作單一的動作,裡面的實作就是現成的範例... 省了不少熟悉 API 的時間 :P

 

事後證明這作法沒錯,BlogEngine 的 LIB 蠻簡單易懂的,另外寫真的比較快... 就一切以原 .ASMX 的程式碼為藍本,寫了一個 .ASHX ,不作什麼事,就是把事先放在 ~/App_Data 下的 BLOGML 讀進來,一篇一篇 POST 處理,一個一個回應處理... 兩層迴圈就搞定原本那 90% 的匯入作業...

 

接下來就是自訂的部份了。先來列一下那些是我要補的資訊:

  1. 每篇 POST 的 COUNTER
  2. 每則回應的詳細資訊
  3. 每篇 POST 的 ID ( CS2007 內的 ID ),及匯入後的新 ID
  4. 所有必要的網址修正 (圖檔,站內文章連結..)
  5. 原程式的一些缺陷修正 (如前文提到,Modified Date錯誤的問題)

 

源頭就有東西要補了。如果一切基於 BlogML 的話,BlogML 沒定義到的資料就沒機會撈出來了。看了一下我還需要額外抓出來的資訊,像匿名的 COMMENTS 作者資訊,IP 等... 不過這些都用某種序列化的方式存在 CSDB,ㄨ!! 真麻煩... 在 CS2007 DB 內的 [cs_posts] 有這兩個欄位: PropertyNames, PropertyValues... 沒錯,全部壓成一個大字串,得自己寫程式去 PARSE ...

CS2007 的作法是這樣,在 PropertyNames 欄位放的值長的像這樣 (包含 Name, 型別, 字串起始及結束位置):

SubmittedUserName:S:0:3:TitleUrl:S:3:35:

 

而在 PropertyValues 對應的值為:

小熊子http://michadel.dyndns.org/netblog/

 

拆起來有點小麻煩,PropertyNames 是 {Name}:{Type}:{StartPos}:{EndPos}: 的組合,有多組的話就一直重複下去。以 Comments 的 SubmittedUserName 來說,它的型別是 S ( String ),它的值就要從後面的 PropertyValues 整個字串的第 0 個字元 ~ 第 3 個字元之間的值 ( 小熊子 ),而 TitleUrl 則是第 3 個字元 ~ 第 35 個字元的值 ( http://michadel.dyndns.org/netblog/ ) ...

看來用 T-SQL 拆或是用 XPath / XSLT 拆都有點辛苦,想想算了... 這段 CODE 跑不掉一定要寫... 就硬解出來吧。解出來後就可以修正 CS2007 匿名張貼的 Comment 沒正確顯示在 BlogML 內的問題。

接下來要把 SQL 這堆資料倒成 XML 檔,偷吃步一下,找工具直接倒出來就好。反正只作一次,可以不要寫 CODE 就不要寫了... SQL2005 要把 QUERY 結果存成 XML 倒是很簡單,打開 SQL2005 Management Studio, 執行這段 QUERY:

取得所有 comments 的額外 PropertyValues 的查詢指令[copy code]
   1:  select 
   2:    PostID, 
   3:    PostAuthor, 
   4:    PropertyNames, 
   5:    PropertyValues 
   6:  from cs_posts 
   7:  where ApplicationPostType = 4 
   8:  for xml auto


 

會看到這樣的畫面:

image

 

點下去就是 XML 了,手到加上頭尾的 Root Element 存檔就搞定了。接下來補段 CODE 把我要的 Property 拆出來存 XML 檔,第一步驟收工!

接下來這堆問題就以 (4) 最麻煩了。原本想的很天真,用文字編輯器把所有 XML 檔替換掉就沒事... 錯! 先從最單純的圖檔路逕來看吧..

 

[替換圖檔及下載檔案的網址]

最基本的就是圖檔網址... 在 CS2007 裡圖檔是這樣放的 (WLW會幫你補上絕對路逕,不是用相對路逕)

http://columns.chicken-house.net/blogs/chicken/xxxxxxx/xxx.jpg

而看了 BlogEngine 的作法,它用 HttpHandler 的方式來提供前端下載圖檔,格式像這樣:

http://columns.chicken-house.net/xxxxx/image.axd?picture=xxx.jpg

而 BlogEngine 的設定檔是這樣寫的:

 

BlogEngine 在 Web.config 內的 HttpHandler 區段[copy code]
   1:  <httpHandlers>
   2:    <add  verb="*" 
   3:      path="file.axd" 
   4:      type="BlogEngine.Core.Web.HttpHandlers.FileHandler, BlogEngine.Core" 
   5:      validate="false"/>
   6:    <add  verb="*" 
   7:      path="image.axd" 
   8:      type="BlogEngine.Core.Web.HttpHandlers.ImageHandler, 
   9:      BlogEngine.Core" 
  10:      validate="false"/>
  11:    .....
  12:  </httpHandlers>

 

看起來不難,理論上我只要找到原網址,把 ~/blog/chicken/ 後的路逕切出來,前段改成 image.axd?picthre= 就可以了。

大致上是這樣沒錯,不過變數還不少... 因為有好幾種網址 @_@

  1. 分家前  ( http://community.chicken-house.net/blogs/chicken/xxxxx )
  2. 分家後  ( http://columns.chicken-house.net/blogs/chicken/xxxxx )
  3. 古早時代手工打的 ( /blogs/chicken/xxxxx )
  4. 有些內建的要避開,像 ( http://columns.chicken-house.net/blogs/chicken/rss.aspx )

代換要嘛一次做完,要嘛一定要照 1 2 3 順序,否則先做 (3) 就會把 (1) (2) 都破壞掉了... 但是怎麼想程式都很不乾脆,雜七雜八的 CODE 一堆... 最後搬出  http://regexlib.com/ 這個網站,它提供大量的 Regular Expression 的資料庫供你使用,也提供了線上版本讓你測試 MATCH 的結果,很好用! 一定要推一下...

最後定案的 Regular Expression 長這樣:

(http\://(columns|community)\.chicken\-house\.net)?/blogs/chicken/([a-zA-Z0-9\-_\./%]*)

一切都很順利,簡單的就精確的抓到要代換的範圍。補一下小插曲,最後轉完才發現漏掉 (4),不過沒幾篇,就手動改掉了 :P,所以這段 REGEXP 是沒處理到 (4) 這種情況的...

 

 

[站內連結的修正]

再來就開始麻煩了... 動手前我先想了一下,每篇文章要匯進去後才會知道它的新 ID,新網址... 在還不知道新網址之前有些內容的聯結就無法替換了,怎麼樣都不可能在 1 PASS 內解決這問題... 只能用 2 PASS 來處理了。

既然這樣,第一次匯入就不修站內聯結了,只要把新舊 ID 記下來就好...

頭痛的來了,CS2007 的網址格式有好幾種 @_@ (難怪它有專門一個 config 檔來設定幾十個 URL Rewrite 的設定...):

  1. /blogs/chicken/archive/2008/06/20/1234.aspx
  2. /blogs/chicken/archive/2008/06/20/MyPostTitleHash.aspx
  3. /blogs/1234.aspx

其中 1234 是 CS2007 內部使用的 PostID .  再看看 BlogEngine 的網址格式:

  1. /post.aspx?id=xxxx-xxxx-xxxxxx-xxxxxxxxxxx
  2. /post/xxxxxxxxxxxxxxxx.aspx (某種 TITLE 的 HASH,它稱作 SLUG)

還好 BlogEngine 單純多了,只要抓到 (1) 的 ID,SLUG 就有 API 抓的到...

記得 CS 網址還有不需要 URL Rewrite 的版本 ( /blogs/xxxxx.aspx?postID=1234 這樣),不過從來沒出現在我文章內容,就不理它了...

這裡的重點不是單純的代換了,還要精確的抓出 1234 這段 PostID .. 最後定案的 Regular Expression 長這樣:

(http\://(columns|community)\.chicken\-house\.net)?/blogs/chicken/archive/\d+/\d+/\d+/(\d+)\.aspx

果然人家說 Regular Expression 是 "WRITE ONLY LANGUAGE" 真有道理,寫完連我自己都看不懂這堆符號是在幹嘛... 最後新舊 ID 都抓到就可以產生出正確的新站內連結了..

 

 

[站外連結修正]

我自己的 LINK 找到就可以修,不過寫在別人那邊的我那修的到啊... 因此就要靠上一篇講的,想辦法要去接有人點到舊的 CS LINK,然後當場做轉址的動作...

好在舊的文章不會再變多了,靠前面做出來的對照表就有足夠的資訊了... 成果看上一篇就知道了。這裡主要是靠 CSUrlMap.cs 這個我自己寫的 HttpHandler 來解決所有連到 /blogs/*.aspx 的連結...

方法差不多,我得從各種可能的網址格式,抓出舊的 CS PostID,然後查表,找出新的網址,把使用者引導到新的頁面... 貼一下主程式的部份:

CSUrlMap, 將 CS2007 的網址重新導向到 BlogEngine 網址的 HttpHandler[copy code]
   1:  public void ProcessRequest(HttpContext context)
   2:  {
   3:      if (matchRss.IsMatch(context.Request.Path) == true)
   4:      {
   5:          //  redir to RSS
   6:          context.Response.ContentType = "text/xml";
   7:          context.Response.TransmitFile("~/blogs/rss.xml");
   8:      }
   9:      else if (matchPostID.IsMatch(context.Request.Path) == true)
  10:      {
  11:          //  redir to post URL
  12:          Match result = matchPostID.Match(context.Request.Path);
  13:          if (result != null && result.Groups.Count > 0)
  14:          {
  15:              string csPostID = result.Groups[result.Groups.Count - 1].Value;
  16:              if (this.MAP.postIDs.ContainsKey(csPostID) == true)
  17:              {
  18:                  context.Items["postID"] = this.MAP.postIDs[csPostID];
  19:                  context.Items["redirDesc"] = "網站系統更新,原文章已經搬移到新的網址。";
  20:              }
  21:              else
  22:              {
  23:                  context.Items["redirDesc"] = "查無此文章代碼,文章不存在或是已被刪除。將返回網站首頁。";
  24:              }
  25:          }
  26:          else
  27:          {
  28:              context.Items["redirDesc"] = "4444";
  29:          }
  30:      }
  31:      else if (matchPostURL.IsMatch(context.Request.Path) == true)
  32:      {
  33:          context.Items["postID"] = this.MAP.postURLs[context.Request.Path];
  34:          context.Items["redirDesc"] = "網站系統更新,原文章已經搬移到新的網址。";
  35:      }
  36:      else
  37:      {
  38:          context.Items["redirDesc"] = "查無此頁。將返回網站首頁。";
  39:      }
  40:      context.Server.Transfer("~/blogs/AutoRedirFromCsUrl.aspx");
  41:  }

 

看程式說故事,大家都會吧 :D,結果就是上一篇各位看到的樣子... 已經沒力一行一行再說明下去了 :P

 

寫到這邊真是大工程 @_@,動作都不困難,但是都是雜七雜八的工作,害我搞了一個禮拜... 不過到此為止搬家要處理的資料部份全部都完成了。下一篇就輕鬆多了,把網站的版面調整成我想要的樣式... 有興趣的人請多等一等吧 :D 主題會放在跟 FunP 推推王的密切整合上... 敬請期待 :D



6/19/2008 3:26:00 AM

從 CommunityServer 2007 到 BlogEngine.NET

Microsoft.NET | 543 | BlogEngine.NET | CS

哈,不是我要搬家了,是網站搬家... 搬家是想了很久沒錯,只是決定搬到 BlogEngine.Net 倒是沒花幾天的時間,所以準備動作也是有點 LiLiLaLa .. 從 CommunityServer 這種成熟的系統搬到還很年輕的 BlogEngine.Net,其實碰到不少小麻煩,在這裡記錄一下給需要的人參考,也算功德一件!

 

要搬 BLOG,最直覺的就是用 BlogML 了,不錯,我用的 CommunityServer2007 有工具可以匯出 BlogML,而 BlogEngine.Net 也有工具可以匯入 BlogML,想想真是太好了... 就先用 DevWeb 架了 BlogEngine.Net 起來試搬看看..

 

 

image

BlogEngine.Net 匯入的方式,是用 ClickOnce,直接從它的官方網站下載的 WinForm App 配合 BlogEngine.Net 本身提供的 Web Service 來進行匯入 BlogML 的動作,除了 BLOGML 之外也支援 RSS? 不過 RSS 怎麼試都試不出來,放棄... 直接用 BlogML ...

 

image

畫面很乾淨,它也很忠實的完成了它的工作... CommunityServer2007 的 BLOGML 匯入時出了點問題,文章的修改時間不知為何,BlogEngine.Net 都一直抓到 0000/01/01 00:00:00.000 ,而BlogEngine.Net 還會幫你修正時區的問題 (台灣時區,要 -8 小時才是標準時間),結果一扣就變成負的,就送我一個 Exception ... Orz

 

匯入的第一步就得動用到 Visual Studio 2008,真不是好兆頭... 所性拿掉那行程式,就一切沒問題了,順利匯入! 架了台測試網站,研究了一兩天,實在是有點不滿意...

  1. LINK 不對
    Windows Live Writer 很好心的幫你把上傳的圖檔都用絕對網址來表示,因此所有的圖都連回原本的網站,正常顯示沒有問題。不過搬家那有這樣搬的... 改!!!
  2. 只搬了BLOG,文章沒有搬
    只怪當時年輕,無聊去用 article 的功能,造成部份是 BLOG 文章,部份是 ARTICLE。CommunityServer2007是都有忠實的匯到 BLOGML,不過 BlogEngine.Net 的匯入工具略過它了... 改!!!
  3. 站內 LINK 不對
    圖檔 LINK 還好修,字串換一換就搞定,不過站内的文章戶連才是個問題... 改!!!
  4. 站外 LINK 不對
    其實這一點是搬完家才發現的,剛搬好 PAGE VIEW 低的可憐... 看一看 LOG 都是 404 Page Not Found ... 改!!!
  5. 沒有 COUNTER
    BlogEngine.Net 說實在話,以BLOG來說功能一點都不缺,不過很怪,竟然沒有最基本的 VIEW COUNT ? 所幸 BlogEngine.Net 有定好 Extension 的架構,要寫它的擴充程式很簡單,有個高手寫了 BlogEngine.Net 的 View Count Extension... 畫面不怎麼樣,不過很實用... 裝!!!
  6. 原有的 VIEW COUNT 沒匯入
    廢話,BlogEngine.Net 本來就沒內建 VIEW COUNT,那是我自己裝了擴充程式才有... 資料要匯,當然也只能自己想辦法 :~~ 改!!!
  7. 版面問題
    標準的版面我很喜歡,就是我要的那種素素的 STYLE,很標準的 ASP.NET Master / UserControl 架構,太棒了... 我要求的也不多,就放放廣告 (看廣告收益很有意思耶,好像電動裡打怪會賺錢或是經驗值那樣... 真的賺多少其實也不重要 :D),還有幾個我自己寫的 CONTROL ... 改!!!
  8. 我在CommunityServer加的功能
    沒辦法,自作孽,改了一堆只能自己搬... 有好幾個:
    • Google Ads (可以用)
    • FunP 推推王 (沒在用了)
    • Recent Comments (已經有內建,可以扔了)
    • 皮哥芸妹年齡 (跟太座的網站分家了,我這邊就不需要了)
    • Bot Check (還沒搞定要怎麼改 :~)
    • Code Formatter 在 BLOG SERVER 要配合的部份

 

 

上面這幾點,都是搬家時或多或少會碰到的小問題,不過很想哭的是,這六個最後都是搬出 Visual Studio 2008 出來才搞定的 @_@,什麼意思? 就是自己寫 CODE 來修啦... 真不知道該誇還是該罵,也因為這樣有機會 Trace 幾次它的 Code,忍不住想再誇它一次,要建起開發環境實在太容易了 [H] 哈哈...  它的 CODE 很精實,CODE 不多,架構很好,規規舉舉的很容易懂,這種 CODE 改起來真舒服.. (為什麼公司的 CODE 都沒有像這種的...),修改的難度大概只有 CommunityServer 的 1/3 .. (CommunityServer的作者很猛,把 M$ 在 2.0 才推出的那套架構在 1.1 時代就都實做了一套 [Y])

 

現在還有空在這裡慢慢打,當然是這些問題都解決完了,先看看成果吧,細節有空再補 HOWTO 文章。那些 LINK 不對的問題,各位翻翻舊文章,如果都看的到圖點的到東西,就是沒問題了。有問題的請再留話給我,我來修看看。

 

 

image

來看看加上 Google Ads 的版面,老實說這個版跟 Google Ads 還真搭... 看起來就像同一套的.. 我只調了 CSS ,跟 MASTER PAGE..

 

image

再來是站內文章互連的 LINK。這邊讓我傷了幾秒鐘的腦筋... 就放棄原本只想寫寫批次檔代換的念頭了。轉檔前跟本不知道轉檔後的新 LINK 是長什麼樣子,轉檔後就錯失先改好 BLOGML 再匯入的機會了... 想了一下,無解,一定要動用到 2 PASS 才行... 就乖乖的改轉檔程式了。第一輪就是基本的匯入,然後在原本的 BLOGML 附記新的 LINK 及 BlogEngine.Net 的 PostID,然後 PASS 2 再把舊文章逐一翻出來 SEARCH & REPLACE ... 上圖可以看到,內文 [四核 CPU] 的 LINK (在底下) 就已經是修正過的了,各位可以去試看看...

 

再來是站外的 LINK,站內我自己的可以改,站外可沒辦法... 拿最捧我場的 Darkthread 網站為例,搜尋一下就有七八篇是連到我這邊,怎麼可以辜負大人的好意... 動搖國本也要改! 現在我的 BlogEngine.Net 已經可以接受舊系統的網址了,而且會正確的轉到同一篇文章的新網址。不過為了不讓大家 "不知不覺" 就轉過來,我特地加了一頁提示,因為書上教的,不要把錯誤隱藏起來 :D,你可以用防禦性的方式寫 CODE,但是務必加上 ASSERT 及 TRACE 提醒自己這裡要注意... (出自一本古董書: Writing Solid Code... 太古董了實在找不到 LINK ... )

 

 

image 

就挑這篇來示範吧,黑暗大哥的文章裡有個 LINK,存的是舊的 CommunityServer 格式 URL,點下去之後會跳到我的網站...

 

 

image

倒數完或是你沒耐性直接按下去的話,就會跳到這篇文章... BINGO,原本的內容出現了!

 

 

image

 

 

嗯,總算修正回來了。其實修正網址是搬家最頭痛的問題了,這個搞定其它都好說 :D  留給有興趣想從 CommunityServer 搬家到 BlogEngine.Net 的人參考...

(小熊子別再撐了... 還有那個誰也是一樣...)



6/17/2008 2:01:24 AM

換到 BlogEngine.Net 了!

Microsoft.NET | BlogEngine.NET | CS | 技術隨筆

也許有人只是覺的換版面而以... 原本是打算升級到 CommunityServer 2008 的,不過自從 Community Server 商業化之後,個人版限制越來越多,整套系統也越來越大,常常出了一些問題都沒辦法自己解決 (連 Error Message 都藏的很隱密 -_-)...

 

其實上面這些也不算缺點,不然我也不會一路從 .Text 時代就用到現在,一用就用了四年多... 上禮拜無意間聽同時講到 BlogEngine.Net,一時好奇抓下來看看,馬上就被它的簡易安裝嚇到了.. 有多簡單? 步驟如下:

  1. 下載
  2. 解開
  3. 設定 IIS 虛擬目錄 / 用 DEV WEB 執行它
  4. 完成了

我已經很努力的把它寫複雜一點了... 它安裝就是這麼簡單,因為它可以不需要 DB (用一堆 XML 檔取代),因此一行 web.config 都不用改就可以用了... 驚喜之餘,也吸引我更深入的多試了幾個功能...

 

基本功能試過一次之後,發現它比 CS 還符合我的須要,怎麼說?

  1. 很簡單
    不只是設定簡單,它的功能也很專一,就是一套BLOG而以。沒有複雜的會員機制,也沒有帳號申請,也沒有多套BLOG管理 (不過它支援多個作者),除了 BLOG 也沒有其它功能... 不多不少,正好我要的都有!
  2. Open Source
    雖然 CS 也有 Source Code 可以看,不過它的原始碼越來越難找了... 每次逛它的網站都要找半天才找的到 SDK 在那裡下載...
  3. 不需要 DB
    雖然我自己 HOSTING 我的網站,DB並不是什麼大問題。不過不需要 DB 對我也是個大利多。一方面網站備份更容易,另一方面除錯及改程式也更容易... 更好用的是,未來我可以把整個網站目錄燒到 DVD 上 (不過兩百多篇文章,不到100MB,燒什麼 DVD...),只要再搭配 .NET 附的 DevWebServer,做個 AutoRun ... 想到可以幹嘛了嗎? 我的 BLOG 馬上就變成一份可以放在 CD,需要時就可以就地用 BROWSER 來看內容了!
  4. CODE 精簡,程式碼架構佳
    這點又免不了跟 CS 拿來比較一下... CS 的作者也是高手,CODE寫的很漂亮,不過跟 BE 最大的差別是,CS實在太肥了。肥到什麼功能都要繞個兩三圈... 要修改雖然不難,但是都要花點時間...
  5. 免費! 免費!
    心理作用,其實賣錢的 CS 送的免費版本,功能比完整的 BlogEngine.NET 還多... 不過商業化之後難免會有些功能得付費才能使用... Orz
  6. 小巧,易用,速度快
    CS 即使是在 LOCAL 執行,速度都沒辦法讓人覺的 "飛快",但是換了 BlogEngine.Net 就有這種感覺... 我目前的文章只有兩百多篇,不靠 DB 的速度都還很快。我試過灌了1000篇文章,速度依然很快... 我想這樣就夠了,我要寫到破千篇,不知道還要多久?

光是這幾個優點,就讓我決定試用只有一天的 BlogEngine.NET 換掉用了四年的 CS ... 剩下的問題只有 "怎麼轉" ? 各位看到現在都成功搬過來,那一定是搞定了... 哈哈... 沒錯,周末花了兩個晚上研究 + 寫匯入程式,今天就重新開張了 :D

轉檔的過程,改天再寫另一篇,有興趣的朋友請耐心等待續集...






精選文章

RUN! PC 文章及範例下載
2010/07. 結合檔案及資料庫的交易處理
2010/05. TxF讓檔案系統也能達到交易控制
2010/04. 生產者 vs 消費者 - 執行緒的供需問題
2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式

如何學好 "寫程式" 系列
#1. 該如何學好 "寫程式" ??
#2. 為什麼 programmer 該學資料結構 ??
#3. 進階應用 - 資料結構 + 問題分析
#4. 你的程式夠 "可靠" 嗎?

#5. 善用 TRACE / ASSERT

安德魯是誰?

Andrew Wu | Create Your Badge

我喜歡鑽研物件導向、軟體工程及作業系統等相關技術。我會在這裡發表我的研究心得,也當作我自己的學習筆記。


Recent comments

Comment RSS