2010/3/18

[Code] 建立Template式的 User Controls

在ASP.NET網站上使用UserControl是常見的一種使用方法,不過Clark以往比較少在User Control上使用到Template的方法。前陣子遇到一個需求:要將所有的Grid的下方加上不同的廣告;由於這些Grid統一都是使用了一個GridControl 的UserControl,而廣告加的位置正好就在Grid和分頁器的中間。一遇到這種情況時,在還不知道Template的作法時,直覺式的用法可能就是在UserControl中加一個MultiView的ServerControl,再利用外部帶參數進入UserControl來決定要顯示什麼廣告。這樣的方式的確可以達到功能,但是每次一改廣告,就要去改動這個GridControl,似乎不是長久之計,也不是良好的設計方法。

但是一想到Panel or Repeater這種Server Control明明可以使用<xxxTemplte/>的方法來從元件外部指定內部的Control 或HTML,那一定有種方法可以讓UserControl也達到這種功能囉!?沒錯!只要宣告 ITemplate 型別的屬性就可以讓UserControl多了指定內部Control的功能,再加上實作部份程式碼就能夠將外部指定的元素Render出來。

MyBox.ascx:

<%@ Control Language="C#" ClassName="MyBox" %>
<script runat="server">
   1:  
   2:     public String Title { get; set; }
   3:  
   4:     private ITemplate _contentTemplate = null;
   5:         /// <summary>
   6:     /// 宣告ContentTemplate,供外部指定(e.g.<ContentTemplate>xxxx</ContentTemplate>)
   7:     /// </summary>
   8:     [TemplateContainer(typeof(ControlContainer))]
   9:     [PersistenceMode(PersistenceMode.InnerProperty)]
  10:     [TemplateInstance(TemplateInstance.Single)]
  11:     /* TemplateInstance.Single 表示Template只實例化一次,
  12:      * 此時才可以用ID直接指定Template中的Control, 否則 Compile 會失敗 */
  13:     public ITemplate ContentTemplate
  14:     {
  15:         get
  16:         {
  17:             return _contentTemplate;
  18:         }
  19:         set
  20:         {
  21:             _contentTemplate = value;
  22:         }
  23:     }
  24:  
  25:     void Page_Init()
  26:     {
  27:         ///一旦外部有指定<ContentTemplate>內容則將內容實例化到Container中
  28:         ///再加入PlaceHolder中
  29:         if (_contentTemplate != null)
  30:         {
  31:             ControlContainer container = new ControlContainer();
  32:             _contentTemplate.InstantiateIn(container);
  33:             placeHolder1.Controls.Add(container);
  34:         }
  35:     }
  36:  
  37:     public class ControlContainer : Control, INamingContainer
  38:     {
  39:         public ControlContainer()
  40:         {
  41:         }
  42:     }
  43:  
</script>
<fieldset>
    <legend><%
   1: =Title 
%></legend>
    <asp:PlaceHolder ID="placeHolder1" runat="server" />
</fieldset>

Demo.aspx

<%@ Page Language="C#" %>
<%@ Register Src="~/MyBox.ascx" TagName="MyBox" TagPrefix="My" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<script runat="server">
   1:  
   2:     protected void OnbtnSearch_Click(object sender, EventArgs e)
   3:     {
   4:         txtSearch.Text = DateTime.Now.ToString();
   5:     }
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>demo templated User Control</title>
    <style type="text/css">
        fieldset
        {
            border: 2px;
            border-style:solid;
            border-color: Black;
            padding: 2px;
            width: 250px;
        }
    </style>    
</head>
<body>
    <form id="form1" runat="server">
    <div class="leftBlock">
        <My:MyBox ID="mybox1" Title="SearchBox" runat="server">
            <ContentTemplate> 
                <!-- 測試Template中的Server Control是否可用 -->
                <asp:TextBox ID="txtSearch" runat="server" />
                <asp:Button ID="btnSearch" runat="server" Text="送出" OnClick="OnbtnSearch_Click" />
            </ContentTemplate>
        </My:MyBox>
    </div>
    </form>
</body>
</html>

執行結果:

template

當然,Template的用法不僅僅這麼簡單,當然也可以做到類似Repleater這樣的Server Control的功能。不過在這樣的實作方式中,已經可以解決Clark的問題了,如此一來只要在原來的GridControl上加上該功能,廣告就可以一率從外部指定,這樣一來元件就會更加具有彈性!

更多說明請參考MSDN 上的範例:How to: Create Templated ASP.NET User Controls

※此範例和MSDN上的範例在VS2008上均有無法在Design Mode中檢視UserControl的問題 (Error Creating Control – xxxx Type 'System.Web.UI.UserControl' does not have a public property named 'ContentTemplate' ),這個問題目前Clark還未找到解法,但是不影響執行的結果,若有網友有解決方式也請不吝賜教,造福大家一下~

2010/3/8

[Memo] 當Memcached 安裝成為 Windows Service 時如何指定參數

前陣子由於負責的網站Loading越來越重,加上前端Web Server Load Balance 有N台主機,在原本只有.NET Cache當作Cache的情況下,每個網頁仍需要Query N次資料才能夠全部進Cache。這個情況下只好求助Memcached這樣的Cache Server機製,好讓多台Web Server能夠共用Cache資料。達到『一人吃N人補』的境界。

Memcached 應該算是目前還滿流行的Cache Server,除了功能強大之外使用上也算簡單。安裝部份則只要下二行參數

memcached -d install
memcached -d start 

就能夠將memcached安裝並啟動成為Windows Service。不過有一個比較垢病的地方是無法指定服務啟動時的參數,導致memcached啟動時只能使用預設的最大64MB的記憶體。

就目前為止查到的解決這個問題的方法只有一種,進Regstry修改。

修改的路徑為:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\memcached 下的 ImagePath中的值,將原來『"N:xxxx\xxx\xxx\memcached.exe" -d runservice』後方下參數(e.g. –m1024),像這樣
『"N:xxxx\xxx\xxx\memcached.exe" -d runservice -m1024』。

memcached-1 修改完成後,重新啟動即可。

Memcached相關文章:

2010/3/6

[Tips] Runtime JavaScript and Css 壓縮方式

上回有提到過壓縮JavaScript來加快網頁載入速度的方式,而實際導入專案中使用的方式有許多種;一般來說我們在開發期間會以未壓縮的js&css檔案進行開發工作,以方便修改及Debug工作,等到了測試期間才會改用壓縮格式的檔案來測試。所以通常會採用的作法是在Debug和Release version時Include不同的檔案來達成,並透過類似MSBuild的方式在Build Release版本時將.js的檔案做壓縮,產出類似xxx-min.js這樣的壓縮檔案。

產出不同版本檔案的方式其實很直覺,但是Clark覺得這樣還不夠彈性,由於YUI Compressor for .Net版本也和YUI Compressor一樣支援css檔案的壓縮,自然就可以考慮如果是在輸出js及css檔案時即時做壓縮。不過前提是即時壓縮需要搭配一些機製來減少Server的負擔,例如Client Side 和 Server Side 的Cache。不然光是應付每個User Request的js檔案就讓你的Server吃不消了!

Clark是以Handler的方式實作即時壓縮的功能,只要頁面在include js時指定"Scripts/js.ashx?path=jquery-1.3.2.js",handler就會將該js檔讀入並使用YUI Compressor for .Net做壓縮,並吐回壓縮過的檔案到Client端。為了針對Debug and Release version而吐出不同的js,使用#if DEBUG來區分版本。

以下是程式碼:

js.ashx.cs:

/// <summary>
/// js.ashx.cs
/// </summary>
public class js : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        String filePath = context.Request["path"];
        String fullPath = Path.Combine(Path.GetDirectoryName(context.Request.PhysicalPath), filePath);
        String ext = Path.GetExtension(fullPath);
        if (String.IsNullOrEmpty(ext)  ext.ToLower() != ".js") return;
        String orgJS = "";
        //TODO: 將原始資料存入Cache,不必每次做載入
        using (FileStream fs = new FileStream(fullPath, FileMode.Open))
        {
            StreamReader reader = new StreamReader(fs);
            orgJS = reader.ReadToEnd();
            reader.Close();
        }
        //TODO: 將壓縮過的資料存入Cache,不必每次壓縮
#if DEBUG
        String outputJS = orgJS;
#else
        String outputJS = JavaScriptCompressor.Compress(orgJS);            
#endif
        //TODO: 判斷Browser是否支援GZip, 支援則將資料做GZip壓縮等..
        context.Response.ContentType = "text/javascript";
        context.Response.ContentEncoding = Encoding.UTF8;
        context.Response.ExpiresAbsolute = DateTime.Today.AddYears(1);
        context.Response.Write(outputJS);
    }
 
    public bool IsReusable
    {
        get
        {
            return true;
        }
    }
}

css.ashx.cs:

/// <summary>
/// css.ashx.cs
/// </summary>
public class css : IHttpHandler
{
 
    public void ProcessRequest(HttpContext context)
    {
        String filePath = context.Request["path"];
        String fullPath = Path.Combine(Path.GetDirectoryName(context.Request.PhysicalPath), filePath);
        String ext = Path.GetExtension(fullPath);
        if (String.IsNullOrEmpty(ext)  ext.ToLower() != ".css") return;
        String orgCss = "";
        //TODO: 將原始資料存入Cache,不必每次做載入
        using (FileStream fs = new FileStream(fullPath, FileMode.Open))
        {
            StreamReader reader = new StreamReader(fs);
            orgCss = reader.ReadToEnd();
            reader.Close();
        }
        //TODO: 將壓縮過的資料存入Cache,不必每次壓縮
#if DEBUG
        String outputCss = orgCss;
#else
        String outputCss = CssCompressor.Compress(orgCss);
#endif
        //TODO: 判斷Browser是否支援GZip, 支援則將資料做GZip壓縮等..
        context.Response.ContentType = "text/css";
        context.Response.ContentEncoding = Encoding.UTF8;
        context.Response.Write(outputCss);
    }
 
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebSite._Default"%>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Css & JavaScript Compressor Test</title>
    <!-- 借用ZenGarden的css sample. 所有要include的css檔都丟給 css.ashx處理 -->
    <link type="text/css" rel="stylesheet" href="Styles/css.ashx?path=zengarden-sample.css" />
    <!-- 所有include的js檔都丟給js.ashx處理 -->
    <script type="text/javascript" src="Scripts/js.ashx?path=jquery-1.3.2.js"></script>
   1:  
   2:     <script type="text/javascript">
   3:         $(function () {
   4:             //test for jquery
   5:             $('#pageHeader h1').fadeOut().fadeIn();
   6:         });
   7:     
</script>
</head>
<body>
    <form id="form1" runat="server">
    <div id="pageHeader">
        <h1>
            Css And Javascript Test
        </h1>
    </div>
    </form>
</body>
</html>

這樣一來,只要在include css & js時都照此方式來處理,那在Release Version時,Client就會自動改用壓縮過的js和css檔,而不需要另外做設定,也不會產出多個檔案減少檔案管理上的負擔。

※以上的範例僅為Demo壓縮的概念,若要架構在Production Site上還有一些功能需要去實作,以免造成Server Loading過重,以及沒有Client端Cache導致User Experience不佳等問題。

2010/3/3

[Memo] 『無法辨認的逸出序列』的另一種可能性

Compile不知道幾千次以上的Web Project突然蹦出一則錯誤 Unrecognized escape sequence (中文訊息:無法辨認的逸出序列)。打開出現錯誤的Souce Code,發現都是在中文字的後方才會出現這樣的問題;照MSDN上的解釋為"在字串中反斜線 (\) 的後方出現未預期的字元",不過光從Editor上看來該行根本沒有出現"\"的字元。

遇到這個問題,加上錯誤出現在中文字串後面,所以直覺認為是Encoding問題。在另存檔案看了一下檔案的Encoding方式為big5。由於之前沒有仔細在看是否所有檔案都是以utf-8編碼,所以猜想難道是什麼動作改了檔案的編碼方式?!先不管是否這樣,在將檔案改以utf-8格式存檔以後,該行的錯誤已經沒出現了,但是其他行陸續出現這個問題,顯示錯誤的檔案不止一個,這樣改下去不是辦法。

在看了一下Subversion中的Log之後,確認近期並沒有動到該檔案,本地的檔案時間也一樣停留在上次修改的時間,所以證實並非檔案編碼遭到修改而造成。

回頭想想這幾天修改了什麼設定,想到昨天為了測試某個Open Source Project而將根目錄改成該Project的目錄。但是心想遇到問題的Project是以虛擬目錄的方式存在,應該不會被影響才對!為了證實問題,將目錄改回原來的C:\inetpub\wwwroot下,重新Compile一次…鏘鏘!錯誤訊息不見了,Compile完成!

既然知道了問題,本著求知的精神將Web.config打開來看,發現有一段設定相當可疑..

<globalization
        requestEncoding="utf-8"
        responseEncoding="utf-8"
        fileEncoding="utf-8"
/>

fileEncoding 指定為utf-8,但是剛剛出現錯誤的Project中不少檔案是以big5編碼的,所以問題應該是虛擬目錄繼承了根目錄的設定,使用utf-8當作檔案的編碼格式。回頭將Project中的Web.config加了一行。

<!-- 如果是線上環境,千萬別直接照著修改,請先確認自已的encoding方式 -->
<globalization fileEncoding="" />

不指定fileEncoding的值,讓Compiler直接以檔案的BOM記錄的編碼方式來當作檔案編碼,重新Compile一次,問題解決!

結論:

  1. 虛擬目錄中如果未指定Encoding方式,會直接繼承根目錄"指定"的Encoding。
  2. 若未指定fileEncoding的值,Compiler會以檔案的BOM值來作為檔案Encoding格式。
  3. 光從錯誤訊息不一定能直接看出問題,但是可以懷疑造成問題的可能性!

.NET Reflector Pro 讓你遇到問題不再"不知所謂"

在Coding的不歸路上,隨著專案越來越大,歷史越來越久遠,用到的Library也越多,這個時候每當到問題時,常常發生的一個情況是在Debug到某個Library的method時由於無法再追下去(沒有Source Code或者就算有也不一定能確定版本是否正確)。這個時候常常需要用到.NET Reflector 加上"充份的相像力"來查看Library的method裡頭哪裡有可能會出問題。而越是分散式的寫法,在看Code時就更需要來回切換在不同的Class裡頭。

而這種刻苦的情節在Clark發現了.NET Reflector Pro提供了Debug Assembly這樣的好物之後將不再會發生了!幾乎所有的Library都可以被Decompile然後攤在陽光下,讓你 一步一步的追出問題的根源,或者瞭解它的實作方式,來提供做為問題分析的依據。

不過這套工作是需要付費的,請花195美元來享用它的功能。但大家可以先下載並執行最新版本的.NET Reflector 來將.NET Reflector Pro自動安裝到Visual Studio 2008 or Visual Sutdio 2010上做15天的試用。

Debug Code or Assembly 的方式官網已經說明得很清楚了,所以就不再多做說明,有圖有真相。

 ReflectorPro

2010/2/23

[Tips] HttpUtility.ParseQueryString 使用注意事項

之前在黑暗執行緒上看到這篇文章,知道了有PrseQueryString這個工具,由於一般在網頁上如果遇到參數的需求,不外乎以Request[xx]方式就可以讀出需要的參數,所以這樣的需求應該比較少會遇到。不過偶然情況下在最近的專案中需要用到一個將 "var1=1&var2=2&..” 這樣將 url 傳遞過來的參數做拆解的功能。由於該key-value傳是存在 url 的某個參數下(如"demo.aspx?type=a&keyList=…"的keyList中),所以直接使用Request當然就無法讀到需要的值了。

遇到這個情況,如果自已寫一個拆解的method當然不是問題,不過直覺想起HttpUtility.ParseQueryString應該可以解決這個需求,所以直接使用了這個元件來解決問題。但是隔沒多久就有User反應功能有問題;在debug之後,發現一旦遇到傳遞的key-value中含有plus(『+』)字元時,就會變成空白。回頭研究了一下確認,證實ParseQueryString會將未UrlEncode過的plus字元轉換成空白。

針對此一情況,一種的解法是在使用ParseQueryString前事先將plus轉換成"%2b”,如此一來ParseQueryString即可正確解決變成空白的問題。

//keylist = HttpUtility.UrlEncode("var1=1&var2=2&total=1+2");
//http://localhost/ParseTest.aspx?type=parse&keylist=var1%3d1%26var2%3d2%26total%3d1%2b2
String query = Request["keylist"];
Response.Write(HttpUtility.ParseQueryString(query)["total"]);
//output: 1 2
Response.Write(HttpUtility.ParseQueryString(query.Replace("+","%2b"))["total"]);
//output: 1+2

不過由於這種修補方式不夠正式,所以雖然可以解決遇到"+"時出現錯誤的問題,但仍不建議這麼使用。由於需求的功能較單純,為確保讀出的資料都可以如實的取回來,最後還是改用自行解析的方式來處理。

public static NameValueCollection ParseQueryString(String query)
{
    NameValueCollection nvList = new NameValueCollection();
    String[] fieldList = query.Split('&');
    foreach (String field in fieldList)
    {
        int eqIndex = field.IndexOf('=');
        //=存在且不在第1個和最後一個字元
        if (eqIndex > 0 && eqIndex != (field.Length-1))
        {
            String name = field.Substring(0, eqIndex);
            String value = field.Substring(eqIndex + 1);
            nvList.Add(name, value);
        }
    }
    return nvList;
}

結論:
HttpUtility.ParseQueryString對於一般"編碼"過的QueryString的折解沒有問題,但如果遇到QueryString中的類QueryString的情況,由於類QueryString中的各個value無法先行編碼,所以一旦被HttpUtility.ParseQueryString解讀時就可能會出現"+"字元被解成空的問題,這個時候就無法再偷懶了,請自行解析吧!

2010/2/19

[Memo] 以命令列工具 appcmd 管理 IIS7

之前的一篇文章 命令列管理IIS 提到如何使用Command Line來管理IIS,更精確來講應該是『命令列管理IIS6』。由於該指令只存在於IIS6,所以不管是IIS5 or IIS7 都無法透過該指令來進行管理。有鑑於此,特別另外寫一篇來記錄IIS7的命令列管理方式以求一個完整性。

由於IIS7已改為統一使用 appcmd.exe 來管理IIS ,所以不再需要像IIS6一樣分成幾種Script來進行管理動作,省去不少麻煩。另外一個特殊的功能是將查輸出結果以XML格式回傳;這個功能搭配上 pipe 的功能,就可以上一個 appcmd 的輸出結果設定當成第二個 appcmd 的輸入來使用,增加指令使用上的變化性和彈性。舉個例子,如果要將目前正在停止中的”多個”網站重新啟動可以執行以下指令:

REM 於目錄 %windir%\system32\inetsrv 下執行
appcmd list site /state:Stopped /xml | appcmd start site /in

其中2個指令以 『|(pipe)』連接,前半部使用 "/xml" 指定將目前正在停止中的網站以XML格式輸出,後半部加上 "/in" 表示將前半部XML輸出資料當成輸入,並將該輸入中所列的網站啟動。
反之,當然也可以將所有正在執行中的網站停止

appcmd list site /state:Started /xml | appcmd start site /in
有了這樣彈性的使用方式,只要再熟悉使用的參數,就可以將部份設定工作批次來進行,節省不少時間。
以下列出幾種常用的指令當作參考:
REM 列出網站(SITE)支援的命令
appcmd site -?
 
REM 列出所有網站 
appcmd list site
 
REM 列出網站的List命令的參數及說明(其他功能的說明方式依此類推)
appcmd list site -?
 
REM 建立網站到8088 port下,並指定對應到d:\temp目錄下
appcmd add site /name:"My Test Site" /id:99 /bindings:http://*:8088 /physicalPath:"d:\temp\"
 
REM 列出所出電腦上所有虛擬目錄 
appcmd list vdir
 
REM 列出指定網站下的所有虛擬目錄
appcmd list vdir "預設網站/"
 
REM 建立虛擬目錄
appcmd add vdir /app.name:"My Test Site/"  /path:/MyVdir /physicalPath:"d:\temp\MyVdir"
 
REM 刪除虛擬目錄
appcmd delete vdir "My Test Site/MyVdir"
 
REM 刪除網站
appcmd delete site /site.name:"My Test Site"

由於指令用法甚多,所以僅列出較常用的幾種當作範例,以便了解參數的給法的方式為何;更多詳細的內容以及進階的需求就需要再參考MSDN上的說明了。


MSDN參考資源:http://technet.microsoft.com/zh-tw/library/cc772200(WS.10).aspx

2010/2/12

[Tips] 壓縮JavaScript檔案以加快網頁載入速度

隨著jQuery的使用率越來越普遍,網頁上許多UI上的功能大家都會透過jQuery plug-in來解決。在這個情況下,頁似乎都免不了會在網頁上include不少 .js & .css 檔案。而在考慮 performance issue 的情況下,網頁上引入的檔案應該是越小、越少越好,這樣不但能夠加快User載入頁面的速度,也能夠減少網站的流量及connection數量,尤其對流量大的網站來說,差距應該就更加明顯。

大型網站如Google, Yahoo一定都會做的一件事情,就是盡可能的減少js & css檔案的size,以提供User最快的服務速度以及使用者體驗。雖然大概沒有多少人有機會經營這樣大型的網站,不過即使對一般中小型網站來說,頻寬仍然是重要的一項資源,而對User來講,速度慢的網站始終讓人無法忍受。

對於js & css這樣的靜態檔案壓縮,還可以分出2種階段,分別為Gzip壓縮及檔案內容的壓縮(去除不必要的空白字元、註解或替換JavaScript變數名稱等);Gzip需要Browser能夠支援Gzip解壓縮時才能夠使用這種壓縮檔案的方式,而檔案內容的壓縮的方式則不受Browser的限制,所以是必定可行的一種壓縮方式。

而關於js檔的內容的壓縮工具,Google,Microsoft,Yahoo分別都有提供了對應的工具,以下列出這三種工具供大家參考,基本上三種工具都有一定的品質,至於哪個工具好用的話除了以瘦身的比例來比較之外,也不外乎是青菜蘿蔔各有所好,不必爭論誰比誰好了。

  • Closure Compiler

    Google提供的工具,提供Comand-line及Online方式來進行分析、重寫(rewrite)、壓縮的動作(視使用的模式而定),目前已廣泛應用在Google Maps, Google Docs等服務上。該工具使用Java開發,目前在Codeplex上已經有以一個Closure Compiler 為基礎使用.NET開發出來的非官方版本的UI Compiler .NET,不過目前還在Beta階段。
  • Microsoft Ajax Minifier
    Microsoft開發出來提供給Ajax Library使用的工具,提供Command-line式的使用介面,以及MSBuild task的方式將檔案壓縮的功能整合在Build動作上來達到流程自動化的功能。
  • YUI Compressor

    使用提供針對css&js檔案壓縮的Command-line工具,也是使用Java進行開發的OpenSource Project;目前在Codeplex上也有非官方的.NET版 YUI Compressor for .Net 可以用,而且也支援MSBuild task功能。由於它多提供了一種針對css檔案壓縮的功能,所以對Clark來說可用性相對的較高一些。

以上三種工具大家都可以選擇自已喜好的來用,不過考慮針對css檔處理的部份來說的話,Clark會選擇使用YUI Compressor,使用統一的工具在管理上也較為方便。

而當有了工具以後,該怎麼用又是另一個問題了!由於大家在開發時期如果直接include壓縮過的js檔案,容易造成控管及debug上的不便(Closure Compiler倒是可以搭配Closure Inspector + FireBug來避開這個問題),所以Clark自已也會選擇在Build時期來產出壓縮過的檔案,減少控管上的問題。