Quantcast
Channel: 黑暗執行緒
Viewing all 2311 articles
Browse latest View live

杜絕ASP.NET網站JavaScript註解外露

$
0
0

ASP.NET MVC的壓縮打包能有效縮小CSS與JS檔案體積,減少HTTP往返次數,進而提升網站效能。JavaScript經壓縮可讀性雖然已大幅下降,但"保護程式邏輯不外洩"的效果仍然有限,不必過度期望。只是壓縮對我還有另一層重大意義: "JavaScript中的註解會被一併移除!"

我很愛在程式裡寫故事註解,把程式邏輯修正的來龍去脈交待清楚,例如:
//2012-04-01 Bug Fix: VIP級使用者呼叫MehtodA前需呼叫MethodB以校正狀態
//2013-04-01 應客服部要求,錯誤次數上限調整為5
//2014-04-01 TODO: 不支援資料量大於1MB,未來如有超過上限需求,此段需修改

手上維護的系統一多,修改動機甚至更動本身很容易迷失在時間的洪流裡,等某天射茶包查出該處修改是禍源,要不是靠註解喚醒記憶,恐怕會有不小心問候到自己爹娘的風險;或者,急著把Bug修掉還原修改,卻忽略當初調整的理由,就會上演"修好A問題,以前改過的B問題又他X的冒出來"悲劇,少不了挨刮。腦海有幾段鮮明記憶: 某段修改在很久之後造成死傷慘重的大爆炸,靠著程式註解詳載使用者堅持調整的始末,第一時間列為呈堂證供,程式人員才沒被當祭品~ 註解能協助程式碼理解、防止邏輯被誤改,緊急時刻還能用來驅邪防身,是程序員的好朋友! 如果講成這樣還不能讓你寫註解,試試這個: 老闆請了個脾氣暴躁的程序員接手維護你的程式,有流言說那傢伙有重傷害前科...

總之,被註解救過小命,就更愛在程式碼加上滿滿註解,救人又救已,連JavaScript也不例外。但這類註解並不適合外流,其中可能涉及能當成八卦的敏感人事時地物,也可能透露開發者真性情,尤其JavaScript註解會原封不動送到Client端,要上到正式環境前,隱藏或移除才是上策。

ASP.NET MVC的打包壓縮功能啟用後,Client將出現<script src="/bundles/script_pack_name?v=XGaE…" type="text/javascript">及壓縮過內容,理論上使用者無從得知原始JavaScript路徑取得原始碼。但我的資安偏執容不下"看起來夠安全",畢竟原始檔案還是放在網站上,萬一有人得知原始路徑或壓縮功能被意外關閉,JavaScript的原始檔含註解就會經由/scripts/blah.js公諸於世,我決心要社絕這一層風險。

當初為專案客製的Script已集中在/scripts/mycode目錄方便管理,因此針對該目錄統一處理即可。一開始想到的做法是用WebGrease工具在每次部署時將JS檔壓成min.js,再將.js的內容也換成壓縮版,當同檔名的.js及min.js同時存在,ASP.NET MVC打包壓縮機制會直接取用min.js版打包,不致重複壓縮;而.js檔已被換成壓縮版,即使用/scripts/mycode/boo.js也看不到原始碼及註解。但這有個缺點 -- 當迫不得已必須在正式台偵錯時,看到的永遠是壓縮後的版本,無法用原始碼偵錯。

最後,我找到一個更彈性的解法: 使用HttpHandler攔截所有/scripts/mycode/*.js請求,讀取JS檔後即時壓縮再傳到Client端;而少數允許偵錯的內部機器,可事先在web.config中列舉IP,針對這些偵錯用IP,則提供未壓縮版本,魚與熊掌兼得。

在ASP.NET MVC網站加入AppScriptHandler.cs:

using Microsoft.Ajax.Utilities;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Hosting;
 
namespace MyWeb.Models
{
publicclass AppScriptsHandler : IHttpHandler
    {
publicbool IsReusable
        {
            get { returnfalse; }
        }
 
staticstring[] debugClients = null;
staticstring[] DebugClients
        {
            get
            {
if (debugClients == null)
                {
                    var list = new List<string>();
string config = 
ConfigurationManager.AppSettings["AppScriptsDebugClients"];
if (!string.IsNullOrEmpty(config))
                    {
foreach (string ip in config.Split(',', ';'))
                        {
//Replace to regular expression pattern
                            list.Add(ip.Replace("*", "[0-9]+").Replace(".", "\\."));
                        }
                    }
                    debugClients = list.ToArray();
                }
return debugClients;
            }
        }
//Check if the ip address matches any pattern in DebugClients
staticbool IsDebugClient(string ip)
        {
foreach (string ipPattern in DebugClients)
            {
if (Regex.IsMatch(ip, ipPattern))
returntrue;
            }
returnfalse;
        }
publicvoid ProcessRequest(HttpContext context)
        {
string jsFile = Path.GetFileName(context.Request.Url.AbsolutePath).ToLower();
string jsPath = Path.Combine(
                   HostingEnvironment.MapPath("~/Scripts/mycode"), jsFile);
            var resp = context.Response;
if (!jsFile.EndsWith(".js") || !File.Exists(jsPath))
            {
                resp.Write("//JavaScript not found: " + jsFile);
            }
else
            {
string js = File.ReadAllText(jsPath);
                resp.ContentType = "text/javascript";
if (jsFile.EndsWith(".min.js") || 
                    IsDebugClient(context.Request.UserHostAddress))
                {
//if already minified or from debug clients
                    resp.Write(js);
                }
else
                {
//return minified result
                    Minifier minifier = new Minifier();
                    resp.Write(minifier.MinifyJavaScript(js));
                }
            }
        }
    }
}

在web.config中,加入appSetting指定可偵錯的來源IP: (支援萬用符號"*",酷吧?)
<add key="AppScriptsDebugClients" value="::1;127.0.0.1;192.168.1.*;172.28.*.*"/>

接著,在web.config要指定將script/mycode/*的存取請求交給AppScriptsHandler處理:
<system.webserver>
    <handlers>
    <add name="scripts" path="scripts/mycode/*" verb="GET" type="MyWeb.Models.AppScriptsHandler" preCondition="integratedMode,runtimeVersionv4.0"/>
    </handlers>
</system.webserver>

就這樣,使用偵錯機器可以看到原始版,其他IP只能看到壓縮版,成功對外隱藏開發者不為人知的一面,又能兼顧線上偵錯的便利性,很棒吧!


【延伸閱讀】


【筆記】JavaScript與CSS壓縮打包工具-WG.EXE

$
0
0

ASP.NET MVC的CSS/JS打包壓縮功能,背後靠的是WebGrease開源元件,而WebGrease還提供MSBuild支援及工具程式--wg.exe方便應用。

前篇文章AppScriptsHandler用來壓縮JavaScript的Microsoft.Ajax.Utilities.Minifier,便是來自WebGrease的元件,這篇文章則談談如何將wg.exe複製到Server上執行。

文件提到wg.exe會隨Visual Studio一併安裝,使用Visual Studio命令提示字元就能直接執行(This tool is automatically installed with Visual Studio. To run the tool, we recommend that you use the Visual Studio Command Prompt.),但試過手邊的機器(有裝VS2012及VS2013),均無法直接從VS Command Prompt啟動(文件有誤?),只能自力救濟。

開啟用NuGet安裝過WebGrease套件的專案資料夾,找到以下檔案:

packages\Antlr.3.4.1.9004\lib\Antlr3.Runtime.dll
packages\Newtonsoft.Json.5.0.*\lib\40\Newtonsoft.Json.dll
packages\WebGrease.1.6.0\lib\WebGrease.dll
packages\WebGrease.1.6.0\tools\WG.exe

將這四個檔案複製在同一資料夾部署到伺服器,即可執行wg.exe壓縮或打包JavaScript及CSS。列舉常用語法如下:

wg.exe -m -in:d:\src_folder -out:d:\dst_folder
將d:\src_folder\所有.js檔壓縮成.min.js後寫入d:\dst_folder

wg.exe –m -in:d:\src_folder\boo.js -out:d:\dst_folder\boo.min.js
將d:\src_folder\boo.js壓縮後寫入d:\dst_folder\boo.min.js

wg.exe –b -in:d:\src_folder -out:d:\dst_folder\packed.min.js
將d:\src_folder\的所有.js檔壓縮打包成單一檔案寫入d:\dst_folder\packed.min.js

【補記】一開始用WebGrease 1.5版測試,發現以下Bug


當命令列視窗現有路徑被設成wg.exe所在資料夾,執行時出現不明錯誤;但只要將當下路徑改成wg.exe所在目錄以外的任何目錄,就可正常正常。本想追進原始碼射茶包,發現問題在1.6版已消失,因茶包已自盡改以不起訴結案。

【茶包射手日記】怪異的web.config HttpHandler重複錯誤

$
0
0

前幾天提到用HttpHandler即時壓縮JavaScript以兼顧註解隱藏及原始碼偵錯需求,今天部署到某台測試機時出現怪異錯誤:

HTTP Error 500.19 - Internal Server Error

Error Code
   0x800700b7

Config Error
   Cannot add duplicate collection entry of type 'add' with unique key attribute 'name' set to 'SrcScriptFilter' 

Config File
   \\ ? \ D:\www\MyWeb\web.config

Config Source:
  113:       <add name="SrcScriptFilter" path="scripts/mycode/*" verb="GET" type="MyWeb.Models.AppScriptsHandler" preCondition="integratedMode,runtimeVersionv4.0"/>
  114:     </handlers>

Requested URL
   httq://localhost:80/Myweb/home/login

Physical Path
   D:\www\MyWeb\home\login

IIS抱怨SrcScriptFilter HttpHandler在web.config重複出現,在web.config搜尋"SrcScriptFilter"關鍵字,確認沒有第二筆,更何況該web.config複製自另一台運作正常主機不應有錯。加上測試機幾週前就裝好MyWeb經過驗證,直到web.config加入AppScriptsHandler才壞掉。

感覺詭異的情境,想了想,該不會是"遞迴"吧?

開啟IIS管理員,印證了我的假設:

httq://localhost/根網站(Root Website)指向D:\www\MyWeb,底下又設了一個httq://localhost/MyWeb虛擬目錄(簡稱MyWeb子網站)也指向D:\www\MyWeb…

啊哈! 根網站與子網站指向同一資料夾,換句話說,MyWeb\web.config既是根網站的web.config,也是MyWeb子網站的web.config;由於父子網站間的web.config有繼承關係,結果就是AppScriptsHandler在根網站先加一次,到了/MyWeb子網站再加一次,就是web.config明明只有一筆,IIS卻一直嚷著重複的原因。這個目錄錯設問題存在已久,但appSettings、customErrors等設定只是被同樣的值覆寫未被察覺,直到多了Handler設定才爆開。

修正目錄設定後,問題消失,結案!

Yet Another Enter To Tab jQuery Plugin

$
0
0

雖然按 Tab 切換輸入焦點已是Windows操作的普遍慣例,但每回在寫作業平台網站時,規格中總少不了"要能直接按 Enter 切換到下一個輸入欄位"的要求。網路上已經有很多在網頁上用 Enter 取代 Tab 移動焦點的 jQuery Plugin,但很可惜沒有一個100%滿足我的需求,所以,這世上又再多了一個用 Enter 取代 Tab 的 jQuery 套件 (Yet Another Enter To Tab) 囉!

它主要提供以下功能:

  • 允許使用者以"Enter"取代"Tab"鍵移動焦點
  • 支援 input, select 及 button
  • 依照 tabindex 決定焦點順序
  • 自動略過 tabindex==-1, 唯讀(readonly), 停用(disabled), 隱藏(invisible) 狀態的輸入欄位
  • 支援群組化,焦點只會在同群組的欄位間循環
  • 欄位可設成按 Enter 時略過,按 Tab 可取得焦點

基本用法

$(selector).enableEnterToTab();

在容器元素呼叫 enableEnterTab() 即可為其中的輸入欄位 (包含 input, select, button. Enter 鍵在textarea代表換行,故 textarea 會納入焦點循環,但不支援Enter2Tab ) 加上 Enter2Tab 功能。取得焦點的順序完全依照 tabindex與元素在 DOM 的順序無關。如果你想在 Enter 切換焦點時略過某個欄位,請加上特別 CSS 名稱 "e2t-ignore" 。

<input type="text" tabindex="2" />  <input type="text" tabindex="3" class="e2t-ignore" /><input type="text" tabindex="4" />

在 input tabindex=2 按 Enter,焦點會移至 input tabindex=4;在 input tabindex=2 按 Tab 則會將焦點移到 input tabindex=3。

Screenshot 
圖1 Field 5 被指定了class="e2t-ignore",故在按 Enter 切換焦點時會被略過,但按 Tab 切換焦點時則可取得焦點。
注意: 瀏覽器UI上的輸入控制項也屬於 Tab 切換焦點的循環範圍,在上例中,在 Field 4 按下 Tab 後,焦點會移至瀏覽器的網址列。

焦點移動規則

每次按下 Enter ,ya-enter2tab 會先在容器內找出所有可以取得焦點的候選人。其中,具有正數 tabindex 屬性的 input, select,button, textarea會是候選人,ya-enter2tab 會將其依 tabindex 排序,與目前焦點所在欄位的 tabindex 相比,找出下一個取得焦點的對象。

Screenshot
圖2在 Form 1 裡,每一個 Field N 欄位代表其 tabindex 也被設成 N ,ya-etner2tab 會 tabindex 大小決定焦點移動順序,與其在網頁的擺放位置無關。

<input type="text" tabindex="2" />  <input type="text" tabindex="3" readonly /><input type="text" tabindex="4" disabled /><input type="text" tabindex="5" style="display:none" /><input type="text" tabindex="6" />

上例中,在 input tabindex=2 按 Enter 或 Tab ,焦點會移至 input tabindex=6 。若使用jQuery 對 input tabindex=3 removeAttr("readonly"),再按下 Enter 或 Tab ,則焦點會移至 input tabindex=3 。

 

Screenshot
圖3 一開始,焦點循環會略過被隱藏的 Field 5 及停用中的 Field 7 ,利用下方按鈕執行JavaScript顯示 Field 5 並啟用 Field 7 後,二者即會納入焦點循環中。

焦點循環分群

你可以將欄位放入不同容器,例如: div A 及 div B,並在二者分別執行 .enableEnterToTab() 。 當在 div A 的欄位中按下 Enter 或 Tab ,焦點只會在 div A 內部的欄位切換,不會跑到 div B 的欄位。這個功能在同一網頁有不同輸入區塊時特別有用。

我用了一點技巧實現焦點循環分群: 當 div A 的任一欄位透過滑鼠點擊取得焦點時,ya-enter2tab 會尋找其他啟用 enablingEnterToTab() 的空器,找出其中的欄位,將 tabindex 現值暫存至 data-tab-index 後將 tabindex 改為 -1 ,防止其在 Tab 切換時取得焦點。同時,ya-enter2tab 也會檢查 div A 中的輸入欄位,由 data-tab-index 還原其 tabindex 值,確保焦點切換順序正確。如果想要徹底防止按 Tab 時焦點移到容器之外,請記得將可取得焦點的欄位都放入容器並執行 .enableEnterToTab() 。

Screenshot
圖4焦點循環分群

LIVE DEMO

Online Demo

原始碼

https://github.com/darkthread/jquery.ya-enter2tab

BIG5 GB2312繁簡編碼快篩

$
0
0

BIG5 與 GB2312 是繁體中文與簡體中文最常採用的 ANSI 形式編碼,當代系統多已改採 Unicode ,但在涉及傳統系統整合的情境中,仍有處理中文 ANSI 編碼的需求。有時,資料來源較雜,BIG5、GB2312 編碼都有可能,系統規劃者多半希望系統能由二進位資料 (Byte Array) 自動判別其編碼為 BIG5 或 GB2312 。就理論而言,以程式判斷 BIG5、GB2312 不可能 100% 精確,理由是二者有部分編碼區段重疊。 例如: 某字元的兩個 Byte 為 0xb1、0xf0,若以 BIG5 編碼解讀為「梗」、以 GB2312 解讀則為「别」,都屬有效字元,此時便無從斷定其編碼。 所幸在實務上,文字內容通常不會只有單一字元,當字元數一多,就有頗高的機會出現某兩個 Byte 在 BIG5 是有效字元,在 GB2312 則否的狀況,反之亦然。 只要掌握這些線索,就有機會實現 BIG5、GB2312 編碼的自動偵測功能,雖無法 100% 精準,已能滿足實務需求。

偵測原理

偵測元件以 .NET 撰寫而成,使用方法很簡單,只需呼叫 int ChEncAutoDetector.Analyze(byte[] data) 傳入二進位資料,程式會分別用 BIG5 與 GB2312 解讀,產生統計資料,計算 ASCII、符號、常用字、次常用字、無效字元的字元數目,並算出亂碼指數 (我稱之為 BadSmell,即無效字元及次常用字佔全部字數的比例,其中無效字元的權重設為次常用字的三倍),接著比較採 BIG5 解碼及採 GB2312 解碼的 BadSmell 何者為高? 當 GB2312 BadSmell 較高時傳回 1,代表該內容為 BIG5 的可能性較高;當 BIG5 BadSmell 較高時傳回-1,代表內容為 GB2312 的可能性較高;若二者的 BadSmell 相同,則意味著程式無從判斷屬何者編碼。 BadSmell 演算法的核心只是簡單的 Byte 比對邏輯,雖然元件以 .NET 開發,但不難改用其他語言實現,而 BadSmell 的計算規則( (無效字元*3 + 次常用字) / 總字元數 )也可依不同使用情境調整參數,但以依初步測試經驗,現值已有相當不錯的準確率。

線上測試

原始碼包含一個網站測試介面( ASP.NET Website Project ),可透過瀏覽器測試中文內容檢測結果,另外亦有線上版

Screenshot

已知限制

由於 BIG5 與 GB2312 的編碼特性,必定存在無法識別甚至誤判的可能性,故應用時請視狀況保留人工複核及事後校正的機制。

無法識別案例:

Screenshot

Screenshot

程式碼下載

https://github.com/darkthread/CEAD

【茶包射手日記】SignalR WebSocket Crash Mac Safari二部曲

$
0
0

上回處理過WebSocket導致Mac Safari當機,修復後狀況明顯改善,但有時仍重新載入網頁時Safari仍會當掉,而錯誤訊息模糊許多:

Process:         com.apple.WebKit.WebContent [1527]
Path:            /System/Library/PrivateFrameworks/WebKit2.framework/Versions/A/XPCServices
/com.apple.WebKit.WebContent.xpc/Contents/MacOS/com.apple.WebKit.WebContent
Identifier:      com.apple.WebKit.WebContent
Version:         9537 (9537.71)
Build Info:      WebKit2-7537071000000000~3
Code Type:       X86-64 (Native)
Parent Process:  ??? [1]
Responsible:     Safari [1109]
User ID:         501

Date/Time:       2014-04-11 17:09:51.606 +0800
OS Version:      Mac OS X 10.9 (13A603)
Report Version:  11
Anonymous UUID:  F8190022-15F6-1032-03AC-3B2053B998AE
Sleep/Wake UUID: EE71F795-BA3F-43AE-A1E6-143B4B4AFEBD

Crashed Thread:  0  Dispatch queue: com.apple.main-thread

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: EXC_I386_GPFLT

Application Specific Information:
Bundle controller class:
BrowserBundleController
 
Process Model:
Multiple Web Processes

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   com.apple.WebCore                 0x00007fff9190523a void std::__1::__push_heap_front<WebCore::TimerHeapLessThanFunction&, WebCore::TimerHeapIterator>(WebCore::TimerHeapIterator, WebCore::TimerHeapIterator, WebCore::TimerHeapLessThanFunction&, std::__1::iterator_traits<WebCore::TimerHeapIterator>::difference_type) + 186
1   com.apple.WebCore                 0x00007fff918790c7 WebCore::TimerBase::heapPopMin() + 135
2   com.apple.WebCore                 0x00007fff918783c8 WebCore::TimerBase::updateHeapIfNeeded(double) + 280
3   com.apple.WebCore                 0x00007fff91875e00 WebCore::TimerBase::setNextFireTime(double) + 192
4   com.apple.WebCore                 0x00007fff91907a1b WebCore::FrameView::layout(bool) + 107
5   com.apple.WebCore                 0x00007fff91a0b681
(...以下省略...)

EXC_I386_GPFLT是所謂一般性失敗,EXC_BAD_ACCESS則程式存取範圍外的記憶體區塊,是程式錯亂的標準症狀,二者都沒什麼營養。唯一有價值的線索是Callstack有Timer字樣,讓我懷疑跟setTimer、setInterval有關,雖然這樣就把瀏覽器搞掛不合邏輯,我仍試著停用網頁的setTimer、setInterval碰碰運氣,反覆測試均不見效果,射手開始茫然...

瞎試一陣子後,發現重大情資 -- 網站裝在四個不同測試機上,只有兩台(A,B)會導致Safari當機,另外兩台測試台(C,D)從沒把Safari搞掛過! A,B,C是Windows 2012,D是"Windows 2008"。靈機一動,該不會又跟WebSocket有關吧? 檢查後證實,測試機C雖是Windows 2012因IIS設定問題,跟D一樣沒啟用WebSocket,而A,B有成功啟用WebSocket。這點意外成為破案關鍵,Safari當機的原因,繞了一大圈,最後又回到WebSocket上。

幾經嘗試,最後我找到的解法是在window.onbeforeunload事件加入一小段程式,在離開網頁前主動關閉SignalR連線。

window.onbeforeunload = function () {
try {
if ($.connection && $.connection.hub) {
            $.connection.hub.stop();
        }
    }
catch (ex) {  }
finally {  }
}

從此之後,SignalR網頁就跟Safari過著幸福快樂的日子了... (希望如此啦)

SCSS初體驗

$
0
0

終於,CSS麻瓜也走到這一步!

專案進入網頁排版配置微調階段,陸續加進各式CSS巧門,例如: 依視窗寬度自動隱藏多語系按鈕圖檔切換、依父容器Class切換顯示效果... 東西愈加愈多的下場是style.css愈來愈肥,充斥錯綜複雜的樣式語法,更要命的是因技巧生硬搞出一堆複製貼上、寫死的尺寸數字,依照寫C#、JavaScript的習慣,搞到如此複雜又難以維護,讓人如坐針氈~

每天打開醜陋的CSS修修改改,終於搞到自己都受不了,想起之前聽過的Sass/SCSS,透過巢狀結構、變數、運算、函式等技巧降低CSS的複雜度,將樣式設定內容結構化及模組化,可大幅減輕維護的痛苦。是該練等升級,學習使用高檔CSS兵器的時候了。

網路上有不少Sass/SCSS的介紹,在此不多贅述,提供幾篇我參考的文章:

要在Visual Studio裡編輯SCSS,暫時還需要套件協助,而好消息是 -- 已進入RC階段的VS2013 Update2將內建Sass/SCSS編輯功能,近期內會正式發佈。我的Visual Studio 2013有裝Web Essential,本身支援基本的Sass/SCSS語法高亮標示(Syntax Highlight)、編譯及結果預覽功能,編輯維護SCSS不成問題,先頂著用並期待VS2013 Update2上市吧!
(如需進階編輯支援,也可考慮Web Workbench套件。參考: 在 Visual Studio 撰寫 Sass(SCSS) 讓您快樂似神仙 by Bruce)

將Web Essentials的SASS選項調整如下,設定每次存檔時編譯產生css檔,並在編輯視窗旁顯示結果預覽(新手必備):

在專案中加入*.scss檔案,編輯儲存後會自動產生對應的.css檔案。

編輯.scss時,存檔後右半邊可檢視轉換出來的.css內容。

Sass/SCSS語法並不複雜,已經熟悉CSS的開發者可以很快上手。只花了半天就把原本複雜交錯的style.css整理成style.scss,原本的複製貼上改寫成@mixin + @include,幾處寫死的尺寸數字也改為運算式,未來要調整就簡單多了,至於進一步優化,留待未來慢慢重構。(說穿了是現在等級不到、技能還沒點足 orz)

跟所有前端工程師一樣,體驗過SCSS後只會有一個感想: "回不去了!"。未來面對較複雜的網站Style,我應該都會選擇使用SCSS,撰寫起來簡潔許多,更重要的是能大量減少"Copy & Paste",日後修改容易,也較不易漏改出錯。

最後,分享這次利用SCSS重構原有Style的幾則應用。(如果大家發現有改善空間,歡迎回饋給我!)

依瀏覽器寬度自動隱藏

原寫法:

#barX {
    display: none;
}
@media only screen and (min-width: 1024px) {
    .user #barX {
        display: block;
    }
}
@media only screen and (min-width: 1000px) {
    .vip #barX {
        display: block;
    }
}
#barY {
    display: none;
}
@media only screen and (min-width: 824px) {
    .user #barY {
        display: block;
    }
}
@media only screen and (min-width: 800px) {
    .vip #barY {
        display: block;
    }
}

SCSS版:

@mixin show-when-width($prefix, $minWidth) {
    @media only screen and (min-width: $minWidth) {
        #{$prefix} & { display: block; }
    }
}
$w1: 1024px;
$w2: 1000px;
#barX {
    display: none;
    @include show-when-width(".user", $w1);
    @include show-when-width(".vip", $w2);
}
#barY {
    display: none;
    @include show-when-width(".user", $w1 - 200px);
    @include show-when-width(".vip", $w2 - 200px);
}

多語系按鈕背景圖切換

原寫法:

.tw .send-btn {
    background: url(img/tw/send-button.png);
}
.cn .send-btn {
    background: url(img/cn/send-button.png);
}
.en .send-btn {
    background: url(img/en/send-button.png);
}
.tw .cancel-btn {
    background: url(img/tw/cancel-button.png);
}
.cn .cancel-btn {
    background: url(img/cn/cancel-button.png);
}
.en .cancel-btn {
    background: url(img/en/cancel-button.png);
}
.tw .preview-btn {
    background: url(img/tw/preview-button.png);
}
.cn .preview-btn {
    background: url(img/cn/preview-button.png);
}
.en .preview-btn {
    background: url(img/en/preview-button.png);
}

SCSS版:

@mixin multilang-bgimg($img) {
    .tw & { background: url(img/tw/#{$img}); }
    .cn & { background: url(img/cn/#{$img}); }
    .en & { background: url(img/en/#{$img}); }
}
.send-btn {
    @include multilang-bgimg("send-button.png");
}
.cancel-btn {
    @include multilang-bgimg("cancel-button.png");
}
.preview-btn {
    @include multilang-bgimg("preview-button.png");
}

特定模式時隱藏

原寫法:

#btn1 {
    width: 200px;
}
.vip #btn1 {
    display: none;
}
#btn2 {
    width: 120px;
}
.vip #btn2 {
    display: none;
}

SCSS版:

@mixin hide-for-vip {
    .vip & { display: none; }
}
#btn1 {
    width: 200px;
    @include hide-for-vip;
}
#btn2 {
    width: 120px;
    @include hide-for-vip;
}

批次產生樣式

原寫法:

.vip .mod-a .boo {
    display: none;
}
.vip .mod-b .boo {
    display: none;
}
.vip .mod-f .boo {
    display: none;
}
.vip .mod-g .boo {
    display: none;
}
.vip .mod-m .boo {
    display: none;
}

SCSS版:

@each $mod in mod-a, mod-b, mod-f, mod-g, mod-m {
    .#{$mod} .boo {
        @include hide-for-vip;
    }
}

使用陣列參數執行SQL WHERE IN比對

$
0
0

傳入字串或數字陣列當作篩選參數是很常見的SQL查詢情境,例如: 使用者在UI勾選取10項類別代碼,希望從Products資料表找出這10類的所有產品,轉換成SQL語法,相當於SELECT * FROM Products WHERE CategoryId IN (1,3,8,...,215)。

遇到這類需求,好傻好天真的開發者不小心會寫成恐怖的SQL Injection自殺式查詢:

string sql = "SELECT * FROM Products WHERE CategoryId IN (" +
                   string.Joing(", ", Request["categories"].Split(',')) + ")";

【嚴正提醒】直接將使用者輸入內容組成SQL字串如同在加油站放鞭炮,為害人害己的自殺行為,按江湖上的規矩,應判唯一死刑(阿魯巴到死!)! 請所有開發人員格外留意。

串SQL字串的做法大錯特錯,出了新手村的開發者都知道,DB查詢要用Parameter才是王道,但面對WHERE IN情境,得配合IN條件的資料筆數一一生出對應的Parameter,有點難度。不過,這可難不倒老江湖,薑!薑!薑!薑~

using (SqlConnection cn = new SqlConnection(cnStr))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
string[] ids = "1,4".Split(',');
int idx = 0;
                List<string> list = new List<string>();
foreach (string id in ids) {
string pn = "@p" + idx++;
                    cmd.Parameters.Add(pn, SqlDbType.Int).Value = id;
                    list.Add(pn);
                }
                cmd.CommandText = "SELECT * FROM Products WHERE CategoryID IN ("
                    + string.Join(",", list.ToArray()) + ")";
                var dr = cmd.ExecuteReader();
while (dr.Read())
                {
                    Console.WriteLine(dr["ProductName"]);
                }
                Console.Read();
            }

依IN條件筆數在SQL字串加入變數,再逐一產生SqlParameter放進SqlCommand.Parameters,運用List<string>、String.Join()的技巧,程式碼尚稱簡潔,但隱約覺得有點笨拙。比較大的問題是--這個做法很難搬進Stored Procedure,畢竟Stored Procudure的輸入參數必須預先定義寫死,不像C#有params object[] args可用!

我想起了TVP(Table-Value-Paramter, 資料表值參數,SQL2008起支援)! 如果能將WHERE IN篩選條件用陣列參數傳給SQL,多麼優雅呀!!

先來個小測試,在SQL建立NVarChar與Int型別的Table型別,基本上就能涵蓋大部分的WHERE IN應用。接著宣告一個@cattIds變數,塞入1跟4兩筆資料,當成北風資料庫Products資料表的類別WHERE IN條件,成功!

CREATE TYPE dbo.Str64Array 
ASTABLE(item NVARCHAR(64))
CREATE TYPE dbo.IntArray 
ASTABLE(item INT)
 
DECLARE @catgIds dbo.IntArray
INSERTINTO @catgIds VALUES(1);
INSERTINTO @catgIds VALUES(4);
SELECT * FROM Products 
WHERE CategoryID IN
(SELECT Item FROM @catgIds)

下一步,把戰場拉回.NET,改用ADO.NET呼叫。@catgIds SqlParameter的型別是SqlDbType.Structured,需傳入DataTable物件當值。為便於重複利用,我寫了個小函式GetTVPValue<T>(params T[] args),透過泛型技巧跟params彈性參數個數,就能用GetTVPValue<int>(1,2,3)或GetTVPValue<string>("A","B","C")輕鬆產生所需的DataTable。

改用TVP傳送WHERE IN參數後,程式碼是不是清爽多了呢?

using System;
using System.Data;
using System.Data.SqlClient;
 
namespace ConsoleApplication1
{
class Program
    {
staticstring cnStr = 
"Data Source=(local);Integrated Security=SSPI;Initial Catalog=Northwind";
 
static DataTable GetTVPValue<T>(params T[] args)
        {
            DataTable t = new DataTable();
            t.Columns.Add("Item", typeof(T));
foreach (T item in args)
            {
                t.Rows.Add(item);
            }
return t;
        }
 
staticvoid Main(string[] args)
        {
using (SqlConnection cn = new SqlConnection(cnStr))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = 
@"SELECT * FROM Products 
WHERE CategoryID IN 
(SELECT Item FROM @catgIds)";
                var p = cmd.Parameters.Add("@catgIds", SqlDbType.Structured);
                p.TypeName = "IntArray";
                p.Value = GetTVPValue<int>(1, 4);
                var dr = cmd.ExecuteReader();
while (dr.Read())
                {
                    Console.WriteLine(dr["ProductName"]);
                }
                Console.Read();
            }
        }
    }
}

同樣的概念,搬到Stored Procedure自然也是一氣喝成:

CREATEPROCEDURE SelectProductsByCategoryId (
    @catgIds dbo.IntArray READONLY
)
AS
SELECT * FROM Products 
WHERE CategoryID IN
    (SELECT Item FROM @catgIds)

 

using (SqlConnection cn = new SqlConnection(cnStr))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = "SelectProductsByCategoryId";
                cmd.CommandType = CommandType.StoredProcedure;
                var p = cmd.Parameters.Add("@catgIds", SqlDbType.Structured);
                p.TypeName = "IntArray";
                p.Value = GetTVPValue<int>(1, 4);
                var dr = cmd.ExecuteReader();
while (dr.Read())
                {
                    Console.WriteLine(dr["ProductName"]);
                }
                Console.Read();
            }

搞定收工!


Hello, TypeScript!

$
0
0

歷經一年多的漫長等待,TypeScript RTM囉!

昨天在北美TechEd 2014,微軟發佈了Visual Stuiod 2013 Update 2 RTM,除了上週提過的SCSS,還有另一則令人興奮的消息 -- TypeScript 1.0 RTM也正式納入Visual Studio支援項目,從此我的軍火庫再添利器一枚,伴我在網站叢林衝鋒陷陣。

TypeScript是我的偶像--Anders Hejlsberg的最新力作(想感受神人灌頂的朋友可以看這裡)。近三十年Coding生涯,有超過一半時間與Turbo Pascal、Delphi、C#為伍,這些語言幾乎都出自Anders之手,我稱得上是資深信徒。而隨著前端程式愈寫愈多,JavaScript日益複雜肥大,老覺得手邊專案的JavaScript遲早會變成哥吉拉,有追需解決方案的危機感。而TypeScript剛好能補足JavaScript欠缺強型別與介面規範、結構鬆散難掌握,以及缺少編譯檢核保護等弱點,讓我格外有興趣。如今TypeScript問市,預期會在我未來的前端開發中扮演重要角色。

先前已聽說TypeScript RTM並納入VS2013 Update2 RC,一收到VS2013 Update 2 RTM的消息,二話不說立刻下載安裝,接著網站新增項目選單就有TypeScript File可選:

新增TypeScript檔案後,VS2013會提示要不要用NuGet下載現有JavaScript程式庫的TypeScript Typing File(即定義檔,Definition File,附檔名為*.d.ts)?

在NuGet用"tag:typescript"關鍵字查詢,已有不少知名JavaScript程式庫的定義檔,例如: jQuery、AngularJS、Knockout、Google Maps… 等,而元件廠商如Telerik也開始為其元件如Kendo UI提供TypeScript定義檔。將現有JavaScript的定義檔加入專案後,即使還沒開始寫自己的TypeScript,也能感受TypeScript帶來的好處。

另外,在現有JS檔按右鍵,選單的Search for TypeScript Typings...也可帶出NuGet Manager查詢"tag:typescript 該JS名稱"關鍵字,方便快速尋找對應的定義檔。

以jQuery為例,透過NuGet安裝TypeScript,專案會多出Scripts/typings/jquery/jquery.d.ts[如下圖(1)所指],在自訂的TyepScript檔案(hello.ts)加入/// <reference path="typings/jquery/jquery.dts" />參考宣告[如下圖之(2)],此時輸入jQuery函式,會有完整的Intellisense提示[如下圖之(3)],而其內容與傳統JavaScript Intellisense有很大不同: 支援同一方法參數個數、型別不同的多載(Overloading),而參數的型別會有如JQueryAnimationOptions之特定型別。

最後,若程式呼叫參數不符合介面規範,例如: .on()只傳單一字串參數未提供事件處理函式,TypeScript將編譯失敗,並顯示錯誤訊息及錯誤所在位置[如下圖之(4)],享受如同編譯式語言的謹嚴防呆。

前面提到的JQueryAnimationOptions在定義檔中已定義屬性,應用時只需宣告變數的型別為JQueryAnimationOptions,Visual Studio會自動列出並限制可用的屬性名稱,屬性名稱不符會造成編譯錯誤。對我來說,這是TypeScript最迷人之處,日後為了打錯字氣到想剁手指的機會應該會少一點吧? orz

*.ts檔案在存檔或編譯專案時會自動產生*.js及*.js.map(.map可用於IE偵錯JavaScript時對應出TypeScript程式碼所在位置),但產生的JS檔及MAP檔不會被包含在專案中。可以想像TypeScript對應的JavaScript如同bin/*.dll,是編譯後的產品,而非原始碼的一部分。

經過以上的簡單示範,大家應該已約略體驗到TypeScript的優點,如果你跟我一樣有重度JavaScript開發需求,值得考慮在專案引進TypeScript,防止JavaScript變成追擊的怪獸。

【延伸閱讀】

變更電腦名稱後找不到TFS Workspace

$
0
0

配合公司網管政策,工作機改了電腦名稱。記憶中,更改電腦名稱有件麻煩事是得手動修正SQL Server設定,這回則發現一枚新地雷。

TFS的Workspace設定資料包含電腦名稱(以區隔同一個使用者在多台機器建立的Workspace),更改電腦名稱後,Visual Studio發生找不到原有Workspace,出現以下訊息:

所幸錯誤訊息非常優秀(傑出到我都想提名它角逐奧斯卡最佳錯誤訊息獎),除了告知找不到Workspace,再順便提示可能原因--電腦更名,最後,送佛送上天,連該用什麼工具下什麼指令都一併交代清楚。

要呼叫tf.exe,最簡便的方法是開啟Visual Studio Command Prompt

執行tf.exe,提供舊電腦名稱以及TFS Collection的URL:

C:\Program Files (x86)\Microsoft Visual Studio 12.0>tf workspaces /updateComputerName:舊電腦名稱 /s:"httq://tfs:8080/tfs/Collection名稱"

搞定收工!

短小精悍的.NET ORM神器 -- Dapper

$
0
0

應該有很多人像我一樣,對LINQ的依賴已經到達"LINQ or Die!"(不LINQ,吾寧死)的地步,到了需要存取DB的場合,打死也不想再走ADO.NET + DataTable、DataRow的回頭路。不過,在專案引用EntityFramework或其他ORM解決方案(NHibernation、SubSonic...),固然嚴謹紮實,卻也多出額外工作--要依照Schema在專案定義Entity物件、資料庫變更時要記得同步更新Entity定義,遇到多TABLE JOIN查詢得另外宣告自訂類別承接查詢結果(我還為此寫過潛盾機)。對於要求嚴謹精準的中大型系統,這類準備工作屬無法避免的代價,但在一些力求快速輕巧的開發情境(例如: 轉檔工具、範例程式、單純但大量的報表需求...),所有對資料庫的存取(Table、View、Stored Procedure)需預先定義,還必須隨時保持與資料庫一致,引用EF之類的架構便顯笨重。

前陣子提到用TVP傳WHERE IN參數的做法,在FB專頁得到網友Lane Kuo的回饋(在此致謝),得知好物一枚 -- Dapper(英文原義是短小精悍,用過即知Dapper不負其名),一個精簡小巧的.NET ORM工具,不需在專案裡新增DB Table、View或Stored Procuedure定義,只要取得IDbConnection(SqlConnection、OracleConnection、MySqlConnection...都適用),就能立即享受接近EF、LINQ to SQL等ORM架構的便利,最重要的是能用LINQ把玩資料,這才是上流社會的程式寫法呀!

不囉嗦,打開NuGet就能找到它:

試用之後,感動到直起雞皮疙瘩,這就是我一直在尋找的,好吃又不黏牙的DB LINQ解決方案~

以下整理Dapper的特色:

第一點絕對要大推! 之前在公司推廣LINQ/EF,最常被挑戰的罩門 -- "過去不管再複雜的SELECT FROM WHERE,丟給ADO.NET就能拿到DataTable;弄了LINQ/EF之後,不定義Entity或自訂類別就收不到結果。幹! 你知道我的系統裡有多少個花式SELECT嗎?"

我一直覺得這是由傳統ADO.NET開發邁向LINQ/EF世界的最大阻礙,也動過腦筋想讓ExecuteStoreQuery<T>的T接受dynamic,而Dapper實現了!

using (var cn = new SqlConnection(cnStr))
            {
//1) 不需要定義POCO物件,直接SELECT結果轉成.NET物件集合!(酷)
//   注意: 結果為IEnumerable<dynamic>,會喪失強型別優勢
//2) 可宣告及傳入具名參數
                var list = cn.Query(
"SELECT * FROM Products WHERE CategoryID=@catg", new { catg = 2 });
foreach (var item in list)
                {
                    Console.WriteLine("{0}.{1}({2})",
                        item.ProductID, item.ProductName, item.QuantityPerUnit);
                }
            }

另外,Dapper在SQL語法裡可使用具名參數(如@catg),不像ExecuteStoreQuery只能用{0}、{1},可讀性較佳。

執行結果:

3.Aniseed Syrup(12 - 550 ml bottles)
4.Chef Anton's Cajun Seasoning(48 - 6 oz jars)
5.Chef Anton's Gumbo Mix(36 boxes)
6.Grandma's Boysenberry Spread(12 - 8 oz jars)
8.Northwoods Cranberry Sauce(12 - 12 oz jars)
15.Genen Shouyu(24 - 250 ml bottles)
44.Gula Malacca(20 - 2 kg bags)
61.Sirop d'erable(24 - 500 ml bottles)
63.Vegie-spread(15 - 625 g jars)
65.Louisiana Fiery Hot Pepper Sauce(32 - 8 oz bottles)
66.Louisiana Hot Spiced Okra(24 - 8 oz jars)
77.Original Frankfurter grune Sose(12 boxes)

使用dynamic物件固然方便,但會喪失強型別在編譯時期的防錯優勢,因此Dapper當然也支援將查詢結果應對到自訂類別。此外,以下範例一併示範Dapper能直接將參數陣列展開成WHERE col IN (@arg1, @arg2, @arg3)的特異功能,相當方便。

publicclass SimpProduct
        {
publicint ProductID { get; set; }
publicstring ProductName { get; set; }
        }
 
privatestaticvoid Test()
        {
using (var cn = new SqlConnection(cnStr))
            {
//1) 將SELECT結果轉成指定的型別(屬性與欄位名稱要一致)
//2) 直接傳數字陣列作為WHERE IN比對參數
//   =>自動轉成WHERE col in (@arg1,@arg2,@arg3)
                var list = cn.Query<SimpProduct>(
"SELECT * FROM Products WHERE CategoryID IN @catgs", 
new { catgs = newint[] { 1, 4 } });
foreach (var item in list)
                {
                    Console.WriteLine("{0}.{1}",
                        item.ProductID, item.ProductName);
                }
            }
        }

執行結果:

1.Chai
2.Chang
11.Queso Cabrales
12.Queso Manchego La Pastora
24.Guarana Fantastica
31.Gorgonzola Telino
32.Mascarpone Fabioli
33.Geitost
34.Sasquatch Ale
35.Steeleye Stout
38.Cote de Blaye
39.Chartreuse verte
43.Ipoh Coffee
59.Raclette Courdavault
60.Camembert Pierrot
67.Laughing Lumberjack Lager
69.Gudbrandsdalsost
70.Outback Lager
71.Flotemysost
72.Mozzarella di Giovanni
75.Rhonbrau Klosterbier
76.Lakkalikoori

除了查詢,Dapper提供.Execute()執行SQL資料更新,最特別的是它可以一次傳進多組參數,用不同參數重複執行同一SQL操作,批次作業時格外有用。

using (var cn = new SqlConnection(cnStr))
            {
//1) 可執行SQL資料更新指令,支援參數
//2) 以陣列方式提供多組參數,可重複執行同一SQL指令
                cn.Execute(@"INSERT INTO Region VALUES (@id, @desc)",
new[] {
new { id = 5, desc = "Taiwan" },
new { id = 6, desc = "Mars" }
                    });
            }

Dapper還可以在命令中一次包含多組SELECT,透過QueryMultiple()後再以Read()或Read<T>分別取出查詢結果。

publicclass SimpCust
        {
publicstring ContactName { get; set; }
publicstring ContactTitle { get; set; }
        }
privatestaticvoid Test4()
        {
using (var cn = new SqlConnection(cnStr))
            {
//一次執行多組查詢,分別取回結果
                var multi = cn.QueryMultiple(@"
SELECT * FROM Customers WHERE CustomerId = @id
SELECT * FROM Orders WHERE CustomerId = @id
", new { id = "ALFKI" });
                var cust = multi.Read<SimpCust>().First();
                Console.WriteLine("{0} / {1}", cust.ContactName, cust.ContactTitle);
                var ords = multi.Read(); //取回IEnumerable<dynamic>
                Console.WriteLine("Orders Count = {0}", ords.Count());
            }
        }

執行結果:

Maria Anders / Sales Representative
Orders Count = 6

至於StoredProcedure,一樣可以透過Dapper Query()查詢及使用Execute()執行,直接取回SELECT結果或使用Output參數都難不倒它。

using (var cn = new SqlConnection(cnStr))
            {
//呼叫StoredProcedure查詢資料
                var res = 
                    cn.Query("dbo.CustOrderHist", new { CustomerID = "ALFKI" }, 
                             commandType: CommandType.StoredProcedure);
foreach (var item in res)
                {
                    Console.WriteLine("{0} = {1}", item.ProductName, item.Total);
                }
//取回ReturnValue及Output參數
/*
CREATE PROCEDURE AddOne
    @n INT, @r INT OUPUT
AS 
BEGIN 
SET @r = @n + 1
RETURN 1024
END
*/
                var p = new DynamicParameters();
                p.Add("@n", 1);
                p.Add("@r", dbType: DbType.Int32, 
                    direction: ParameterDirection.Output);
                p.Add("@rtn", dbType: DbType.Int32, 
                    direction: ParameterDirection.ReturnValue);
                cn.Execute("dbo.AddOne", p, 
                    commandType: CommandType.StoredProcedure);
                Console.WriteLine("@r = {0}, return = {1}",
                    p.Get<int>("@r"), p.Get<int>("@rtn"));
            }
        }

執行結果:

Aniseed Syrup = 6
Chartreuse verte = 21
Escargots de Bourgogne = 40
Flotemysost = 20
Grandma's Boysenberry Spread = 16
Lakkalikoori = 15
Original Frankfurter grune Sose = 2
Raclette Courdavault = 15
Rossle Sauerkraut = 17
Spegesild = 2
Vegie-spread = 20
@r = 2, return = 1024

除了呼叫應用的便利性,Dapper很強調效能,在一些實測中明顯勝過EF及其他ORM架構。

【結論】

Dapper的輕巧犀利令人驚豔,讚嘆之餘頗有相見恨晚之感,不過現在知道也不算遲。未來中大型專案我想仍會維持預先定義Entity、Model、ViewModel,力求嚴謹分明的原則,但在一些需要巷戰搶灘近身肉博的場合,Dapper將會是我的好伙伴!

【茶包射手日記】Enum型別錯置奇案

$
0
0

在同事的專案採集到一枚奇特茶包。程式看似無誤,欄位也宣告成NVARCHAR,但塞入的Unicode難字硬是變亂碼,以下程式片段可重現問題:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.OracleClient;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Text;
 
namespace ConsoleApplication1
{
class Program
    {
staticstring cnStr =
"Data Source=MyORACLE;User Id=user;Password=pwd;";
staticvoid Main(string[] args)
        {
using (OracleConnection cn = new OracleConnection(cnStr))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = "INSERT INTO JEFFUNICODE VALUES (:t)";
                var p = cmd.Parameters.Add("t", SqlDbType.NVarChar);
string s = "犇";
                p.Value = s;
                cmd.ExecuteNonQuery();
                cmd.CommandText = "SELECT * FROM JEFFUNICODE";
                cmd.Parameters.Clear();
                var dr = cmd.ExecuteReader();
                dr.Read();
                var t = dr["T"].ToString();
                Func<string, string> charCode = str => 
                    BitConverter.ToString(Encoding.UTF8.GetBytes(str));
                Debug.WriteLine(string.Format("{0}[{1}]->{2}[{3}]",
                    s, charCode(s), t, charCode(t)));
            }
        }
    }
}

執行結果為: 犇[E7-8A-87]->[EE-9B-95]

把眼睛睜大20%重新看一次程式,才發現裡面有個地方錯得離譜,明明是OracleCommand.Parameters.Add(),怎麼用SqlDbType.NVarChar? 要命的是還可以編譯可以執行?

修正為 var p = cmd.Parameters.Add("t", OracleType.NVarChar); ,一切就正常了。
犇[E7-8A-87]->犇[E7-8A-87]

整起事件離奇之處在於 -- 依System.Data.OracleClient.OracleParameterCollection的定義:

public OracleParameter Add(string parameterName, OracleType dataType);

為什麼傳入SqlDbType不會產生編譯錯誤? 強型別的C#接受了錯誤的型別,還可以編譯、執行,這一點都不科學啊! 難道,拎北這十幾年的.NET白學了? 眼睛再張大到50%(啊啊啊,眼角都撐到滲血了)重看一次定義才發現蹊蹺, Add()在兩個參數時還有一個Overloading:

public OracleParameter Add(string parameterName, object value);

原來,當第二個參數不是OracleType,SqlDbType.NVarChar被視為object,套用的是(string, object) Overloading,於是SqlDbType被誤當成OracleParameter.Value。以下程式可以證實:

var p = cmd.Parameters.Add("t", SqlDbType.NVarChar);
Console.WriteLine(p.DbType + "/" + p.Value.GetType());

結果為: Int32/System.Data.SqlDbType ,真相大白!

準備收工時才發現Visual Studio早給了提示: Parameters.Add()下方有條綠蚯蚓,提到.Add(string, object)已過時(deprecated)不建議使用,請改用AddWithValue()... 結果我還查到眼角流血才破案 XD

題外話,設計Overloading時要小心處理object型別,因為所有參數型別都可視為object,較容易套用錯誤發生非預期結果。過去開發時,看到有些Overloading在參數數量及順序做了特殊安排,起初覺得不自然,後來才意會到跟減少混淆誤用有關,是一種防呆設計,API的好壞就都在這些細節中囉~

VS2013 Update 2更新後無法開啟EDMX

$
0
0

迫不及待更新到VS2013 Update 2,開始享受內建的SCSS支援,也展開了TypeScript冒險之旅。

VS2013更新後,開啟工作專案冒出以下錯誤: (但編譯、執行完全不受影響)

Cannot load 'X:\TFS\MyProject\DEV\src\DAL\Boo.edmx': Specified cast is not valid.
無法載入 'X:\TFS\MyProject\DEV\src\DAL\Boo.edmx': 指定的轉換無效。

在VS2013試圖開啟Boo.edmx檔案,出現同樣錯誤,推測是新版EF Model編輯器與原.edmx不相容所致。很快在Microsoft Connect找到報案記錄,確認問題出在EF 5的Metadata函數(Function)參數資料缺少Precision及Scale屬性造成Model設計工具轉換錯誤(嚴格來說是新版設計工具未考量舊版格式的Bug)。如果.edmx沒用到有數字參數的Function,則不會出現錯誤。

在Bug修正前,有個暫時解決方案。如下圖,使用文字編輯工具開啟edmx檔,找到SSDL區的Function定義,尋找所有Type="decimal"或Type="numeric"的Parameter,手動加上Precision="8" Scale="4"兩個Attribute(Precision/Scale的值不重要,有給即可),存檔後,Visual Studio 2013就能正常開啟原有edmx檔囉~

2014-05-21更新: 此Bug在EF6.1 Beta已加入修正。(感謝Chris Torng補充)

批次修改Windows記憶密碼

$
0
0

工作機平日使用本機帳號登入,存取伺服器或網站時才使用網域(AD)帳號,為了避免每次都重新輸入,我會在第一次登入時使用Windows內建的密碼記憶功能把網域帳號密碼存起來。公司的伺服器及網站眾多,於是乎就出現一張長長的密碼記憶清單...

問題來了! 基於資安管控,網域帳號的密碼需要定期更換,但換過密碼後要是忘了更新上述的記憶密碼,下回想連上某台伺服器,Windows便會拿著舊密碼試著登入... 登楞! 很快地,連錯三次,帳號上鎖 orz 必須連絡網管才能解鎖。

因此,每回改完網域帳號密碼,得趕快更新記憶密碼。要像下圖逐一點開每台伺服器/網站項目,按下【Edit】修改密碼、儲存,同樣動作要重覆十多次,而且每三個月需要重頭演練一次。雖然對正常人來說,操作十來次根本不算什麼,但,動作重複是程式設計功力不足的象徵,對程式魔人是一種羞辱。不行! 重複操作正在毁滅我的人生(謎之聲: 有這麼嚴重? 施主,你病得不輕呀!),如果沒法讓它自動化,我就退出江湖。

認真研究後,找到Windows有個內建命令列工具,cmdkey,可用來管理記憶密碼。

既然有工具,理論上就有對應的Windows API,但已有現成cmdkey.exe可用,透過Process呼叫外部程式的把戲也難不倒我,就不花功夫去研究API了,寫支C#程式,先輸入網域帳號(Domain\Account)及新密碼,呼叫cmdkey /list列出所有儲存密碼,從中比對User名稱找出屬於該帳號的項目,再逐一呼叫cmdkey /add:server_name /user:Domain\Account /pass:newPassword變更成新密碼,就大功告成囉!

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
 
namespace UpdateStoredPwd
{
class Program
    {
 
staticvoid Main(string[] args)
        {
            Regex reTarget = new Regex("Target: (?<t>.+):target=(?<s>.+)");
            Regex reUser = new Regex("User: (?<u>.+)");
            Console.Write("User: ");
string account = Console.ReadLine();
            Console.Write("Password: ");
            Console.ForegroundColor = ConsoleColor.Black;
string pwd = Console.ReadLine();
            Console.ForegroundColor = ConsoleColor.White;
 
bool catchAccount = false;
string currTarget = null, currType = null;
foreach (string line in Execute("cmdkey", "/list")
                                    .Replace("\r", string.Empty).Split('\n'))
            {
if (!catchAccount)
                {
                    var m = reTarget.Match(line);
if (m.Success)
                    {
                        catchAccount = true;
                        currTarget = m.Groups["s"].Value;
                        currType = m.Groups["t"].Value;
                    }
                }
else
                {
                    var m = reUser.Match(line);
if (m.Success)
                    {
                        catchAccount = false;
string user = m.Groups["u"].Value;
if (string.Compare(user, account, true) == 0)
                        {
string a = string.Format(
"/{3}:{0} /user:{1} /pass:{2}", currTarget, account, 
                                pwd, currType == "Domain" ? "add" : "generic");
                            Console.WriteLine("cmdkey " + 
                                a.Replace("pass:" + pwd, "pass:*****"));
string res = Program.Execute("cmdkey", a);
                            Console.WriteLine(res);
                        }
                    }
                }
 
            }
            Console.Read();
        }
 
staticstring Execute(string prog, string args)
        {
            Process p = new Process();
            p.StartInfo = new ProcessStartInfo(prog, args);
//必須要設定以下兩個屬性才可將輸出結果導向
    p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardOutput = true;
//不顯示任何視窗
              p.StartInfo.CreateNoWindow = true;
            p.Start();
            StringBuilder sb = new StringBuilder();
while (!p.HasExited)
            {
string line = p.StandardOutput.ReadLine();
                sb.AppendLine(line);
            } 
return sb.ToString();
        }
    }
}

【後記】

猜想已有軟體能做類似的批次密碼修改,但初步搜尋沒找到,就決定把尋找時間省下來自寫兼練功(謎: 明明就是手癢所以不認真找),但若有人知道有相關軟體可用,歡迎分享。另外,也找到PowerShell呼叫API更改記憶密碼的範例,但寫慣C#硬要用PowerShell有種舒馬克跑去開飛機的彆腳感,最後還是抄起C#就打,呵!

SQLPlus執行UTF-8編碼指令檔

$
0
0

作業環境有個需求: 有一些PL/SQL DDL指令需先匯成sql檔,再透過程式呼叫SQLPlus.exe執行。

SQLPlus.exe可讀入SQL Script檔案直接執行,例如: sqlplus –s user/pwd @script.sql,而透過程式啟動外部EXE也不是問題,一切看似小菜一碟...

BUT! 人生最厲(ㄐㄧ)害(ㄨㄞ)的就是這個BUT!

問題出在SQLPlus在命令提示視窗(Command Prompt)執行,只支援ANSI編碼,而script.sql可能包含Unicode文字,故得用UTF-8編碼。沒想到這小小的編碼要求,最後得五關斬六將方能達成。

先來個簡單測試,用Notepad++新增chinese.sql,存成UTF-8編碼。將Command Prompt設成CodePage 65001,type chinese.sql可以看到"SELECT '中文測試' FROM DUAL;",驗證是UTF-8無誤。呼叫sqlplus執行,出現unknown command beginning錯誤,而SELECT指令前方有一個無法識別字元,推測是BOM作祟。

利用Notepad++將檔案轉存為"UTF-8 without BOM"。(此時下方的編碼顯示會由UTF-8變成"ANSI as UTF-8")

再執行一次,看來執行成功,sqlplus該呈現的中文全變成"?"是怎麼一回事? 在網路找到文章,提到設NLS_LANG及使用SPOOL將結果輸出到檔案的技巧,改寫成spool.sql,終於成功繞過sqplus受限ANSI編碼的限制,在result.txt取得正確執行結果:

如果.sql中包含Unicode難字,則要使用N'…'以及ORA_NCHAR_LITERAL_REPLACE環境變數,一樣能克敵致勝:

最後,除了靠SPOOL,SQLPlus真的沒法讀取UTF-8直接顯示中文嗎? 先前參考的文章提過一招密技,使用PowerShell ISE... 娘子啊,跟牛魔王出來看中文:

【參考資料】


2014海山馬拉松~

$
0
0

第16,海山馬拉松。

跑了15場馬拉松,未曾嚐過落馬滋味,今天開了洋葷!既然算不上一「馬」,就用「丐」字充數吧!

去年跑海山馬時足底筋膜炎纏身,加上天熱難當,跑出逼近六小時關門的PW(Personal Worst)。今年的比賽時間提前到五月底巧遇陰天,酷熱不似去年,而賽前訓練狀況不差,預期不會像上回煎熬,説不定還能跑出不錯成績。

 

六點半準時起跑,雖然太陽偶爾露臉,大部分仍是多雲陰天,加上河濱平坦無坡,未刻意加速,心率不高,Pace一路維持在540,跟平時輕鬆跑的速度相近,感覺良好!

順利跑完7公里多,膲見前方景色不錯,減速靠邊打算拿出手機拍張照,一不小心踩到路緣斜面,左腳盤翻了一下扭到腳踝,心中罵了聲"暗",立即檢視傷勢,以從小到大扭傷的經驗相比,不算嚴重,但要拖著把35K撐完幾無可能,硬幹只怕要把腳搞殘。

留得青山在,不怕沒柴燒~ 考慮了一分鐘決定棄賽,上面的照片成了棄賽紀念。

打算走回水站求援,一路上則留意大會巡邏車有沒有路過。走到水站時大會巡邏車剛好經過(噗,一公尺都沒少走),稟明腳扭棄賽,便以"落馬客"身分坐專車用"兩分速"奔回終點~ (感謝照片裡的大哥相救)

回到會場,跟醫護站要了包冰塊冰敷一陣子,領回寄物走了好遠才坐上捷運打道回府,為第一次的落馬劃上完美句點~ XD

事後檢討,天氣不算熱,配速屬輕鬆等級,不到腳軟地步;已跑了一陣子,難把藉口推給暖身不足;而踩歪的路緣斜面四處可見,算不上什麼天險。究竟,是什麼原因讓黑大踩空呢? 只能說Shit Happens吧! 老天爺自有他的安排 :P

KO範例30 - 可排序的多重選擇器

$
0
0

專案裡的介面需求: 網站需開放使用者由候選清單挑選多個項目放入組成清單,組成清單的項目要能調整順序。

實作範例如下,就想像從三國武將中挑選精英組團打副本吧!(裡面有個黑大是什麼東西?) 並可調整出場順序。

搬出Knockout,運用下拉選單的options、selectedOptions多選繫結加上JavaScript陣列基本操作,就能讓選取項目在兩個<select>間搬移;至於選取項目在陣列上下移動,以前我只會傻傻用for跑迴圈搬動,學會splice()後,就能用兩行指令優雅地完成。

就這樣,不到90行,搞定收工~ 線上展示

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>KO範例30 - 可排序的多重選擇器</title>
<style>
      select { width: 100px; height: 120px;}
</style>
</head>
<body>
<table>
<tr>
<td>
<selectmultipledata-bind="options: candidates, selectedOptions: selCandidates"></select>
</td>
<td>
<inputtype='button'value='&gt;'data-bind="click: pick"/>
<br/>
<inputtype='button'value='&lt;'data-bind="click: unpick"/>
</td>
<td>
<selectmultipledata-bind="options: members, selectedOptions: selMembers"></select>
</td>
<td>
<inputtype='button'value='▲'data-bind="click: moveUp"/>
<br/>
<inputtype='button'value='▼'data-bind="click: moveDown"/>
</td>
</tr>
</table>
 
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
<script>
function myViewModel() {
var self = this;
          self.candidates = ko.observableArray(
            ["關羽", "張飛", "孔明", "呂布", "典韋", "許褚", "黑大"]);
          self.selCandidates = ko.observableArray();
          self.members = ko.observableArray();
          self.selMembers = ko.observableArray();
          self.pick = function() {
            $.each(self.selCandidates(), function(i, m) {
              self.candidates.remove(m);
              self.members.push(m);
            });
            self.selCandidates([]);           
          };
          self.unpick = function() {
            $.each(self.selMembers(), function(i, m) {
              self.members.remove(m);
              self.candidates.push(m);
            });
            self.selMembers([]);
          };
function get1stSelMem() {
var ary = self.selMembers();
if (ary.length) {
var mem = ary[0];
              ary = self.members();
return { array: ary, index: $.inArray(mem, ary), value: mem };
            }
returnnull;
          }
          self.moveUp = function() {
var res = get1stSelMem();
if (res && res.index) {
              res.array.splice(res.index, 1);
              res.array.splice(res.index - 1, 0, res.value);
              self.members(res.array);
            }
          };
          self.moveDown = function() {
var res = get1stSelMem();
if (res && res.index < res.array.length - 1) {
              res.array.splice(res.index, 1);
              res.array.splice(res.index + 1, 0, res.value);
              self.members(res.array);
            }
          };
        }
var vm = new myViewModel();
        ko.applyBindings(vm);
</script>
</body>
</html>

[KO系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post

從Knockout到AngularJS

$
0
0

照片出處: http://www.geograph.org.uk/photo/360000作者: Barbara Voules

兩年前初見Knockout.js後便一腳踏入MVVM世界無法回頭。學習簡單很快上手,用Knockout做出錯誤少又容易擴充維護的AJAX網頁。在此之前,為了讓欄位連動,總要寫一堆<input>、<select> onchage、onclick事件,事後常需要在一堆事件程式碼裡追查更動來源,更糟的是稍一調整就常因觸發順序改變導致錯誤,修改維護是件苦差事;改用KO後,專心把ViewModel邏輯寫好,餘下的欄位連動便能一次到位,加上邏輯都集中寫在ViewModel內,維護起來輕鬆許多。

不過,最近參與SPA專案(Single Page Application,指從頭到尾沒有任何PostBack,停在同一網頁裡完成全部工作的網頁,最經典的例子是Gmail),逐漸感受單靠Knockout(或者該說MVVM)的不足。SPA需在同一網頁切換不同操作介面,當介面複雜度提高,網頁HTML、JavaScript開始龐大交錯,管理及維護難度急速上升。面對這類情境,引用JavaScript端MVC設計模式讓架構分明,簡化維護難度,是較好的選擇,而Knockout只專注於MVVM,仍需要其他MVC架構輔助才容易建構成SPA。

經過粗淺評估,目前市場上較常提及的JavaScript MVC架構包含: AngularEmberBackboneDurandal(杜蘭德爾,中古世紀聖騎士羅蘭的配劍名字)。由於我的後端一定是用ASP.NET MVC開發,故整合資源、參考資料的多寡將是考量關鍵,以此標準,就是Angular JS或Durandal二選一了。

Angular JS擁有最大的開發社群,是幾個架構中最夯的,氣勢驚人。而選擇Durandal的優勢在於Knockout! MVVM也算是MVC裡的一環,Angular有自己一套處理MVVM的做法,而Durandal則直接採用Knockout處理MVVM,微軟傳教士John Papa也曾針對Durandal有一系列的介紹,對我而言,已累積的KO知識在應用Durandal時將可發揮,感覺上Angular與Durandal各有優勢。看似兩難的抉擇,其實至今局勢已大幅改變~

在今(2014)年初,Open Source的Durandal啟動了一個集資計劃(Kickstarter),雖然創始人Rob聲明說即使集資不成仍會持續發展,只是開發速度趨緩,但已令人憂心Durandal的發展動能及前景。到了4月中,Angular的部落格上有篇很特別的文章,作者是Rob Eisenberg -- 是的,就是剛說的Rob,Durandal的創始者(Rob還打趣的說,大家不要驚慌,他不是靠Heartbleeding漏洞駭進Angular Blog偷發文),文中透露驚人的消息。Rob從2月起已加入Angular Core 2.0 Team,計劃將原本Durandal下一代(NextGen)的構想融入Angular 2.0,同時提供Durandal 2.x遷移到Angular的途徑,Durandal 2.x仍會繼續維護(但可預期將不再有新版本),此舉如同宣告Durandal的發展已劃上句點。

發展至此,微軟不再(也無法)漠視Angular的成長並開始擁抱Angular(令我憶起當年AJAX Control與jQuery的瑜亮故事),John Papa近期有愈來愈多談Angular整合的文章,前陣子結束的北美TechEd還有場談ASP.NET+Angular建立RIA的議程。由此不難得感受,使用Angular建構ASP.NET SPA已是當今的主流!

最後,我很倚重的Kendo UI元件為Angular再下一成。前陣子發表Angular Kendo UI v1.0,方便Angular整合Kendo UI,雖然KO也有社群發展的knockout-kendo可用,但Angular Kendo UI由官方推出,二者層級有別,也算Angular穩坐前端MVC一哥地位的佐證。

分析至此,態勢明顯,如果要走ASP.NET SPA,Anuglar才是主流選擇!! 廢話不多說,立刻向Angular出發~

但,Knockout該何去何從?

首先得強調一點,Knockout根本不是Angular的對手!! 倒不是因為Angular太強大,KO還沒上擂台就被叫去領便當;而是二者定位不同,KO只專注於MVVM,而Angular包含整套MVC。MVVM所聚焦的Data Binding只是MVC中實做View的一環,故二者不該直接相比,就像沒人會拿Office跟iPhone相比一樣道理,要比也是Windows Phone對上iPhone,定位才相近。Angular是完整的MVC架構,真要比較,對手應該是Durandal、Ember或Backbone,而Knockout隸屬Durandal陣營,所以應是Durandal vs Angular,哦哦...

這段時間,其實我已投入不少時間熟悉了解AngularJS,Angular在語法簡潔性(自訂Directive及Filter的概念很酷)、架構彈性(可動態載入切換View、Dependency Injection的點子很棒)、可維護性(甚至已考慮自動測試需求)方面考慮嚴謹周延,常讓我發出讚嘆(回頭看到自己的土砲架構,不禁發出感嘆)。但在Binding運作上,Angular不像KO靠宣告observable()、observableArray()建立依賴關係,而是直接觀察Scope內JavaScript物件的改變自行建立相依。雖然Angular宣告ViewModel時不必寫成observable()、observableArray()很省事,但面臨較複雜的欄位互動邏輯,初學者常難以判斷某個動作是否會被Angular感測。除此之外,Angular涉及MVC,整體架構範圍比MVVM龐大,而官方技術文件較Knockout深澀。依我個人看法,Angular的學習曲線較Knockout陡峭許多。在一些簡單的ASP.NET AJAX應用場合,如果只是需要一套MVVM機制,Knockout較好學習上手,而observable/observableArray宣告雖然囉嗦,但應用時容易區分是否觸發UI連動,還是很有優勢。

至於我的選擇? 由於專案會走向SPA,Angular是強大、完整、嚴謹、彈性的主流MVC架構工具,並已廣為ASP.NET社群接受,就算學習曲線陡峭如同單攻奇萊主峰,也得咬牙吞下去,著眼熟悉活用後帶來好處。近期將會陸續分享我的Angular學習筆記,一連串KO之後,NG要上場囉~

對SQL XML節點進行JOIN查詢

$
0
0

今天遇到的隨堂測驗,考題如下:

SQL Server有一資料表,其中Records欄為XML型別,其中包含多筆記錄,目標要將記錄展開成多筆查詢結果:

.xmljoin-tab td { padding: 3px; }
001Jeffrey2014-01-019999
001Jeffrey2014-04-0132767
002Darkthread2014-06-1065535

要實現以上需求,需動用兩項SQL功能:

  • SQL XML nodes()
    將XML型別轉成關聯式資料
  • CROSS APPLY
    以結果列的特定欄位當成參數傳給函式得到資料表後與原結果列進行JOIN

二者結合成以下語法

SELECT
    A.PlayerId, A.PlayerName,
    T.C.value('@Date', 'VARCHAR(10)') RecDate,
    T.C.value('.', 'INT') Score
FROM XMLJOIN A
CROSS APPLY A.Records.nodes('//Rec') T(C)

資料表與XML節點的JOIN查詢就完成囉~

NG筆記1-AngularJS學習資源與術語

$
0
0

學習AngularJS一段時間,初步心得是"很強大但不好駕御",跟學習Knockout的經驗相比,差異程度大概像這樣:


照片來源: Jace 美國空軍

感受懸殊,關鍵在於應用心態不同。Knockout著眼於MVVM,以Binding為核心;但Binding卻只是Angular的一環,之所以選擇Angular,並非意圖用它取代KO處理MVVM(依我個人看法,KO把MVVM做得很好),而是SPA專案裡的JavaScript異常複雜龐大,Angular內建模組化、Controller、共用服務、View切換、依賴注入、自訂宣告(Directive)、自動測試等特性,能有效區隔程式邏輯實現SoC,大幅降低JavaScript複雜度。然而豐富的工具與強大的擴充性也意味著一大堆可調整的參數、可互相替代的做法、無限多種的排序組合... 學得愈多愈讓人迷惘,嚴重時還會誘發工程師的冒牌者症候群大爆發: "暗! 我這樣寫到底對不對呀?" 若只聚焦在MVVM,Angular並不難上手,當把視野放大到如何用Angular寫出"容於擴充維護的優質SPA",資深老鳥也很難不發抖吧? _(:3 」∠)_ 

以下整理我找到的一些學習資源: (如果大家還知道其他推薦資源,歡迎回饋給我)

  • 前端工程的極致精品: AngularJS 開發框架介紹
    保哥的介紹,不錯的入門起點,另外還有AngularJS系列文
  • 官網互動式入門教學(英文)
    教學影片 + 投影片 + 實做測驗,很炫!
  • YouTube上的教學影片(英文)
  • API官方文件 (英文)
    對API有疑義,這裡是最權威的文件來源,並區隔不同版本。唯文字與範例走"言簡意賅"風,偶爾需要一點慧根才能參透。(如果能像KO一樣Friendly就好了)
  • 男丁格爾大大的AngularJS入門教學
    有三十多篇,Binding部分介紹得蠻完整,但部落格分類目錄採倒著排序且需在目錄與文章間切換,為了方便閱讀,我用Angular寫了一個閱讀器 XD
  • AngularJS 開發實戰:解析 angular-seed 專案架構與內容 by 保哥
    談功能模組化與專案檔案配置,屬進階議題,但案子變大時一定得面對
  • Plunker
    提供類似JSBinJSFiddle的線上JavaScript程式展示平台,特色是有工作區及社群互動概念,一個工作區稱為一個Plunk,可以包含多個HTML、JS、CSS檔案,支援版控,還像Github一樣可以Fork、Star,Angular官方及社群的範例很多都放在Plunker。(PS: Plunker本身就是用AngularJS打造滴 :P)

後面我計劃將先前以KO實做過的範例全部用NG(Angular簡稱為NG)演練一次,確認自己有能力改用NG滿足常見的MVVM需求。但在開始之前,先定義一些我所理解的術語,以便未來在文章中出現時不會有太大落差: (如有謬誤也請指正)

  1. Controller
    就是MVC中的C,負責將ViewModel與View結合產生UI。HTML容器元素(body、div)用ng-controller標明負責該元素的Controller,一個網頁裡可以有多個Controller負責不同元素的Binding,父子元素的兩個Controller,其ViewModel預設會出現繼承關係。
  2. Scope
    資料繫結的對象,在Controller中以$scope物件形式出現,當我們寫<span ng-bind="someProp">時,通常就是指$scope.someProp(使用ng-repeat迴圈時則為集合中的逐一物件),Scope可以視為NG中ViewModel角色。跟KO的ViewModel一樣,Scope可以放屬性,也可以加入方法。
  3. Directive
    上面提到的ng-bind、ng-repeat就是所謂的Directive。Directive穿插於HTML標籤之間,串連HTML元素及ViewModel。在NG裡,也靠Directive將DOM事件(Click、Blur、Change...)關聯到ViewModel,例如: <input type="button" ng-click="buttonClick()">。
    另外,自訂Directive是我心中NG強悍的關鍵之一,例如,我們能自訂一個<check-list data-source="objectArray" prop-text="textProp" prop-value="valueProp">,將objectArray轉成Checkbox清單,活生生就像ASP.NET WebControl在HTML端復活了!
  4. Filter
    可應用於Directive,對繫結的資料進行後置處理,例如: {{ someDate | date:'yyyy-MM-dd' }}中date Filter將日期物件轉成指定格式字串、{{ dataArray | filter:{ id: 'A' } | json } }}中filter Filter對陣列進行篩選,只留下id屬性有"A"的物件,之後還能再串接另一個json Filter,將過濾後的陣列以JSON字串輸出。
    NG允許開發者自訂Filter,讓各式資料轉換、處理邏輯能很容易被加入Binding過程。
  5. Service
    跨Controller共用的邏輯一般會收納成Serivce。NG的Service有三種模式: Factory(所有Controller共享一個Service Instance,即Singleton模式)、Service(為每個Controller建立一個Instance專用)、Provider(自訂Service建構方式)
  6. Module
    Module是NG用來收納整理Controller、Directive、Filter、Serivce的管理單位,可以想像成.NET的Assembly。用Module將相關邏輯集中,存成不同JS檔,有利於維護及引用。
  7. Dependency Injection
    NG用了很多DI概念,於是乎你會常看到不知道從哪冒出來的$scope、$http、$sce等變數。在執行期間,NG依預先註冊的DI設定將$scope、$http指向對應的服務物件。這麼做有兩個好處: Controller不綁死服務來源,必要時可偷偷換掉或調整,呼叫端完全不用改(甚至不會察覺),第二項好處是做自動測試時,可換上測試用的Mock版本,以快速模擬特定情境,單獨測試指定的Controller或Service。

介紹完NG的幾個基本術語,後面就要捲起袖子試試NG的威力囉! 敬請期待。

Viewing all 2311 articles
Browse latest View live