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

在VS2012/VS2013環境編譯Web Deployment Project

$
0
0

VS2012起不再支援Web Deployment Project(WDP),是許多還在維護Web Site Project(WSP)朋友的痛吧?以我的工作環境為例,線上系統仍有超過半數是WSP,雖然知道Web Application Project(WAP)/ASP.NET MVC是未來主流勢不可擋,但運作得好好的系統,若有人膽敢嚷嚷新版開發工具不支援部署專案編譯,所以網站得翻掉重測重上線,就算不被主管巴頭,也會被同事拉到牆角餵磚頭。想必不少朋友的解決方案跟我一樣-平日開心使用VS2012/VS2013享受上流開發體驗,要編譯WSP wdproj時,就找台有VS2010的機器或VM專門負責。(怕有人不知道:開源社群、非企業及小型開發團隊,可以使用免費的Visual Studio Community 2013哦!參考

最近對MSBuild有多一點了解,發現VS2012/2013不支援WDP的問題並不如想像複雜。基本上wdproj本身就是個MSBuild設定檔,只要補足WDP專屬的Task,在只裝VS2013的機器也能成功編譯WDP!

方法很簡單:找一台裝好VS2010及Web Deployment Project支援的機器,找到 C:\Program Files (x86)\MSBuild\Microsoft\WebDeployment\ 資料夾,裡面有個v10.0子資料夾(VS2008 WDP則為v8.0),將整個WebDeployment資料夾複製到VS2013主機的相同路徑,搞定。

要由命令提示視窗執行MSBuild需事先設好PATH等必要環境參數,而最簡便的方法是直接使用Developer Command Prompt for VS2013(VS2013開發者命令提示字元),預先已備妥所有環境變數,直接下指令就能順利執行。

 

如下圖所示,在VS2013開發者命令提示字元中切換路徑到wdproj所在目錄,輸入msbuild project_name.wdproj(如果該資料夾只有一個wdproj,還可省略檔名,直接下msbuild即可),就能產生與VS2010時代相同的編譯結果囉~


【茶包射手日記】奇妙的TypeScript編譯Bug

$
0
0

同事報案:Visual Studio 2013不知何時起無法編譯TypeScript,存檔或編譯都不會產生JavaScript檔。嘗試重啟VS2013、Windows依然無法解決,灰心喪志之餘,已經萌生重灌VS2013的念頭。

茶包射手出動。SOP第一步為判斷災難範圍,盡可能將事故現場縮到最小,用最少條件或步驟重現問題,封鎖線的範圍愈小,搜索調查的困難度愈低。

另開一個簡單Web專案,加入Hello World等級的TypeScript,測試編譯功能正常。故排除VS2013 TypeScript完全損壞的可能性,改將問題聚焦到導致TypeScript編譯故障的專案,要找出害VS2013 TypeScript中毒身亡的鬼東西。

依先前的理解,當專案包含TypeScript,Web Essentials會在背後啟動node.js執行編譯作業,負責SCSS、TypeScript、LESS等檔案的編譯工作。當TypeScript編譯有錯,Visual Studio輸出視窗可以看到Something went wrong reaching: httq://127.0.0.1:nnnn/?service=TSLint&source=…之類的訊息,便與背後node.js執行的TypeScript編譯服務有關。我觀察到VS2013只要一開啟問題專案,工作管理員的node.exe會無故消失,服務掛點,之後TypeScript編譯功能停擺也就合情合理。

註:對這段運作細節有興趣的朋友,在C:\Users\your_user_name\AppData\Local\Microsoft\VisualStudio\12.0\Extensions資料夾找到WebEssentials2013.dll,其子資料夾Resources\nodejs\tools裡的檔案可以為你解答所有疑惑,May the force (and source) be with you!

偵辦方向明確,同事回頭檢視最近做的修改,很快找到埋在程式碼裡的炸彈!

有個TypeScript使用了JavaScript "use strict" 嚴格模式,但寫法有誤,將"use strict"放在module內第一列(正確位置應在整個檔案或function內的第一列)還忘了加上分號。就是這個看似不嚴重的錯誤,讓TypeScript編譯服務崩潰~我猜這個罕見的情境組合踩中TsLint node.js程式碼未料想到的邏輯路徑形成Bug,加上缺少容錯防呆,造成程序中止。

以下是精簡示範,將"use strict"後方的分號移除並存檔,node.exe瞬間消失,下方輸出視窗則出現錯誤訊息。

移除無效"use strict"寫法,問題排除!順手把這個大概不會有其他人踩到的Bug回報到Web Essential Github,結案。

【笨問題】在Visual Studio 2013顯示SCSS詳細錯誤訊息

$
0
0

WebEssentials套件加持之下,Visual Studio 2013可以直接編修SCSS,每次存檔自動編譯出css、min.css及.map,非常方便。但初心者如我,寫錯語法在所難免,一旦造成SCSS無法編譯,Output視窗只會看到somethine went wrong、compilation failed: The service failed to response to this request等含糊訊息,右方預覽視窗則停留在前次成功編譯的結果,連怎麼死的都不知道,令人氣餒。

面對這種情境,我之前用的笨笨解法,是按Ctrl-Z取消所有修改,還原到上次成功編譯的狀態,再一點一點把修改加回去,直到加入某段指令出錯抓出凶手。笨歸笨,但挺管用,每次總能化險為夷。

但老人家耐性有限,這種愚公移山的把戲玩多了令人抓狂,有誘發心血管疾病的風險。心想,應該有更簡便的解決方案吧!經過一番研究,找到一招-將Web Essentials / SASS / Use Ruby Runtime 改為 True(預設為False):

VS2013就會顯示詳細的SCSS編譯錯誤訊息囉!而且Error List還會列出SCSS錯誤項目,點兩下會自動跳到錯誤所在位置,享受與C#、TypeScript同等級的上流開體驗。

故事結束了嗎?不,裡面有個大哉問!WebEssentials預設將Use Ruby Runtime設定False肯定有原因,通常也意味啟用得付點代價。經過一番爬文,找到解答:

WebEssentials自2.5.2版起,預設改用node-sass(使用Node.js整合libsass,一套用C/C++寫的Sass編譯器),主要基於C寫的libsass在效能上狂電用Ruby寫的官方版Sass編譯器,若每次SCSS存檔都需要即時編譯,改用libsass能節省可觀時間,換成libsass的理由充分。但libsass對Sass語法的支援度不如Ruby版編譯器完整,故WE仍保留「User Ruby Runtime」選項解決相容問題。搞懂這點,我也才恍然大悟:之前升級Visual Studio 2013 Update 4時,為何修改該選項能解決無法編譯問題。而我主機上的選項切回False,應該是後來WE更新解決無編譯問題,後來再遇到其他問題時又切換的。

由以上觀察,可以知libsass除了Sass規格支援問題外,與VS2013整合也不如Ruby版密切,出錯時無法顯示詳細錯誤訊息。但基於libsass效能上的絕對優勢,我想最平時應該還是保持Use Ruby Runtime選項為False,特別需要偵錯時再切為True,才是上策。另一個解法是可以使用Koala等Sass編譯程式輔助,顯示詳細訊息,也是可考慮的做法。

偵測JavaScript物件屬性異動時機

$
0
0

由Knockout跨到Angular半年,對於NG的Dirty Check機制卻始終沒好感,老覺得它髒,為了偷懶不宣告Observable跟少寫一些訂閱連動,卻無法預期程式觸發次數與時機,讓我很沒安全感。如果可以選擇,我寧可乖乖多寫一些Code,100%掌控程式運作,避免陷入程式 一旦複雜就可能失控的擔憂。(註:我想Angular RD也認同這點,在2.0將另推Observable。參考:… One approach is to replace the dirty checking that AngularJS currently does with Object.observe, which is a proposal to add native support for model change listeners and data binding. AngularJS 2.0 will totally use this to significantly speed up the whole data-binding and update cycle. …)

不過,既然用了NG 1.x,再怎麼不喜歡Dirty Check也得跟它和平共處。只是在實務上,我常遇到一項困擾:$watch()機制可以掌握資料被更改的時點,卻無從得知修改來源。尤其當程式碼龐大,互動複雜(尤其涉及AJAX、setTimeout等運作時),追不出變動來源讓偵錯除錯的困難度上升不少。

用一個簡單範例展示:

<!DOCTYPEhtml>
<htmlng-app="app">
<head>
 
<metacharset="utf-8">
<title>JavaScropt property change tracing</title>
</head>
<bodyng-controller="ctrl as m">
<span>{{m.prop}}</span>
<inputtype="button"value="Change"ng-click="m.change()"/>
<scriptsrc="//code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.js"></script>
<script>
function ViewModel($scope) {
var self = this;
    self.prop = "darkthread";
    self.change = function() {
      self.prop = "Jeffrey";
    };
    $scope.$watch(function() {
return self.prop;
    }, function(newValue, oldValue) {
      console.log("nv=" + newValue + ", ov=" + oldValue);
      debugger;
    });
  }
  angular.module("app", []).controller("ctrl", ViewModel);
</script>
</body>
</html>

在以上程式中,我們用$scope.$watch()捕追prop異動的時點,用debugger指令觸發偵錯中斷,但在Callstack(呼叫堆疊)中,只見angular.js的$apply(), $digest(),看不出prop是被self.chnage()中的self.prop = "Jeffrey"所更動。

JavaScript語言的彈性眾所皆知,自然有神妙的方法解決這類困境。參考網路上高人的文章,透過一些JavaScript技巧將屬性改成透過getter及setter存取,就有機會在屬性被設定時加入自訂邏輯。但原文直接設在Object.prototype.watch的做法容易跟jQuery、Angular等程式庫打架,故我改寫成共用函式版本:

<!DOCTYPEhtml>
<htmlng-app="app">
<head>
 
<metacharset="utf-8">
<title>JavaScropt property change tracing</title>
</head>
<bodyng-controller="ctrl as m">
<span>{{m.prop}}</span>
<inputtype="button"value="Change"ng-click="m.change()"/>
<scriptsrc="//code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.js"></script>
<script>
function watchPropSet(instance, prop, handler) {
var val = instance[prop],
        getter = function () {
return val;
        },
        setter = function (newval) {
return val = handler.call(instance, newval, val);
        };
if (delete instance[prop]) { // can't watch constants
if (Object.defineProperty) { // ECMAScript 5
            Object.defineProperty(instance, prop, {
                get: getter,
                set: setter
            });
        }
elseif (Object.prototype.__defineGetter__ &&
            Object.prototype.__defineSetter__) //legacy
        {
            Object.prototype.__defineGetter__.call(instance, prop, getter);
            Object.prototype.__defineSetter__.call(instance, prop, setter);
        }
    }
}
function unwatchPropSet(instance, prop) {
var val = instance[prop];
    delete instance[prop]; // remove accessors
    instance[prop] = val;
}
 
function ViewModel($scope) {
var self = this;
    self.prop = "darkthread";
    self.change = function() {
      self.prop = "Jeffrey";
    };
    watchPropSet(self, "prop", function(newValue, oldValue) {
      console.log("nv=" + newValue + ", ov=" + oldValue);
      debugger;
    });
  }
  angular.module("app", []).controller("ctrl", ViewModel);
</script>
</body>
</html>

依循$scope.$watch()的概念,我們為prop加上setter函式,在其中可取得新值及舊值,並埋入debugger中斷,這樣就能輕鬆追出變更屬性的凶手囉~

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

零利率分期短期數選項之謎

$
0
0

前陣子上購物網站振興經濟,到了刷卡步驟,一萬多元的商品,購物網站提供了0利率分期,而且有3期、6期、9期、12期跟24期等多種選擇,勾起我的好奇心。如果不同分期數都是零利率,總金額也相同,消費者基於理性應選擇對自己最有利的方案-分24期兩年繳完以獲得最大利益,為什麼還要提供3、6、9、12等其他選擇呢?

黑格爾說:凡存在必合理!

世上所有抉擇的選項,必定各有利弊,有所得就有所失,萬物運行才得以平衝。若存在毫無缺點的選項,必然會排擠消滅其他選項,其他選項一旦消失,便不再需要選擇,所謂「毫無缺點的選項」不該被稱為「選項」。基於這項矛盾,世上沒有「毫無缺點的選項」!換句話說,即使有24期可選,3、6、9、12分期在某些狀況下仍具有優勢,才得以並列為選項。但,分期數較少的優勢到底是什麼?心裡容不下這個疑問,決定追尋解答。

首先,為了確認選擇較多分期零利率沒有隱藏手續費等額外成本,我打了客服電話。聲音甜美的客服小姐說:所有零利率分期不管分幾期,都不會收手續費。抱著姑且一試求解的心情,我趁機插花提問「既然都是零利率,可以分24期為什麼會有人選3、6、9、12期呢?」,客服小姐聲音依舊甜美,但顯然她也沒仔細研究過:「不一定耶,就有客人不喜歡分太多期」。

我得到三點結論:1)分期數變多並不會衍生額外費用 2)確實有人選擇較少分期數 3)客服小姐缺乏追根究底的精神(謎:像你這麼無聊的人並不多,好嗎?)。

用我粗淺的投資學知識,分期數增加產生的利益來源來自以下幾點:

  • 資金具有時間價值,未來的100元,經過折現計算,價值小於現在的100元。用最簡單的方法想,如果兩年後才要付100元,在銀行存98元,靠年定存利率1.5%,滾兩年就會超過100元,若忽略存提款等操作成本,我們可以說,兩年後的100元,相當於今天的98元。故若金額相同,愈晚付款,等同付出的金額愈少。
  • 依一般經驗,貨幣貶值的機率高於升值(不是都說錢只會愈來愈薄咩?),當物價上漲,同樣貨幣金額的價值相對變少,延遲付款也能因此獲利。
  • 分期數增加,每期支付金額變少,經濟壓力較輕,讓消費者能負擔原本買不起的東西。(好邪惡的特性)

由以上推論,對消費者而言,愈晚付款愈有利,在不需要額外支付利息或手續費成本的前題,分期分得愈多,每期繳付金額愈少,愈有延後支付效果!那麼,當有3、6、9、12期可選,消費者不選12期,要選3/6/9期甚至一次付清的理由到底是什麼?

把疑問丟上FB,引發朋友的討論並找到一些不想分12期的理由:

  • 不喜歡欠債的感覺
  • 怕自己克制不住,挪用分期付款預算亂買東西
  • 萬一商品壽命比分期期間還短,心理將遭受打擊
    (東西已經吃掉、喝掉、用掉或是壞掉,還是得繼續付錢,感覺很糟… 呵)
  • 信用卡費有忘繳的違約或利息風險
  • 分期付款待繳部分將視為個人負債,影響銀行徵信的評比結果

若排除主觀感受,個人缺陷(胡亂敗家、忘繳卡費)等因素,真正客觀具有影響的只有「分期未付會計入負債」一項,帶給我一些提示,爬文找到心目中的正解:

分期付款待繳部分會佔用信用額度,導致可用餘額減少。例如:買24萬的東西分24期,下個月付掉1萬元,但信用額度會少23萬元,逐月遞減直分期付清為止。

由此可知,分期期數愈多,信用額度被佔用時間愈長,將造成信用卡額度低於應有水準,影響運用彈性,對資金充足但額度吃緊的人便會是重大缺點。

又解開一件久懸心中的謎團,開心~
(3、6、9分期:黑大,謝謝你!證明我們存在是有意義的…)

使用dynamic簡化Json.NET JObject操作

$
0
0

不知是RSS ATOM錯亂還是怎麼的,feedly RSS閱讀器冒出一篇Rick Strahl 2012的老文章 Using JSON.NET for dynamic JSON parsing ,讓我大吃一驚,發現自己一直用JProperty的笨拙方法處理動態JSON物件,其實結合dynamic就能大幅簡化。莫名其妙讀到兩年多前的文章,是老天爺的安排吧?擔心不順從天意會遭天譴,特筆記分享之。XD

以下範例示範使用強型別物件、JObject+JProperty、JObject+dymanic三種不同做法,處理JSON反序列化及序列化。

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace JsonNetLab
{
class Program
    {
staticvoid Main(string[] args)
        {
            lab1();
            lab2();
            lab3();
            lab4();
            lab5();
            lab6();
            Console.Read();
        }
staticstring jsonSrc = 
@"{ 
""d"": ""2015-01-01T00:00:00Z"", 
""n"": 32767, 
""s"": ""darkthread"",
""a"": [ 1,2,3,4,5 ]
}";
 
 
publicclass foo
        {
public DateTime d { get; set; }
publicint n { get; set; }
publicstring s { get; set; }
publicint[] a { get; set; }
        }
 
//方法1 定義強型別物件接收資料
staticvoid lab1()
        {
            foo f = JsonConvert.DeserializeObject<foo>(jsonSrc);
            Console.WriteLine("d:{0},n:{1},s:{2},a:{3}", f.d, f.n, f.s, 
string.Join("/", f.a));
 
        }
//方法2 使用JObject、JProperty、JArray、JToken物件
staticvoid lab2()
        {
            JObject jo = JObject.Parse(jsonSrc);
            DateTime d = jo.Property("d").Value.Value<DateTime>();
int n = jo["n"].Value<int>();
string s = jo["s"].Value<string>();
int[] ary = jo["a"].Value<JArray>()
                               .Select(o => o.Value<int>()).ToArray();
            Console.WriteLine("d:{0},n:{1},s:{2},a:{3}", d, n, s, 
string.Join("/", ary));
        }
//方法3 使用dynamic
staticvoid lab3()
        {
            JObject jo = JObject.Parse(jsonSrc);
            dynamic dyna = jo as dynamic;
            var ary = ((JArray)jo["a"]).Cast<dynamic>().ToArray();
            Console.WriteLine("d:{0},n:{1},s:{2},a:{3}", dyna.d, dyna.n, dyna.s,
string.Join("/", ary));
        }
 
//方法1 使用強型別
staticvoid lab4()
        {
            foo f = new foo()
            {
                d = new DateTime(2015, 1, 1),
                n = 32767,
                s = "darkthread",
                a = newint[] { 1, 2, 3, 4, 5 }
            };
            Console.WriteLine("JSON:{0}", JsonConvert.SerializeObject(f));
        }
 
//方法2 使用JObject、JPorperty、JArray
staticvoid lab5()
        {
            JObject jo = new JObject();
            jo.Add(new JProperty("d", new DateTime(2015, 1, 1)));
            jo.Add(new JProperty("n", 32767));
            jo.Add(new JProperty("s", "darkthread"));
            jo.Add(new JProperty("a", new JArray(1, 2, 3, 4, 5)));
            Console.WriteLine("JSON:{0}", jo.ToString(Formatting.None));
        }
//方法6 使用dynamic
staticvoid lab6()
        {
            dynamic dyna = new JObject();
            dyna.d = new DateTime(2015, 1, 1);
            dyna.n = 32767;
            dyna.s = "darkthread";
            dyna.a = new JArray(1, 2, 3, 4, 5);
            Console.WriteLine("JSON:{0}", dyna.ToString(Formatting.None));
        }
 
    }
}

相較之下,使用dynamic動態型別,效果與使用JProperty存取相同,但程式碼簡潔許多,未來在.NET 4+環境就決定用它囉~

NG筆記22-避免ngRepeat重新產生DOM元素

$
0
0

ngRepeat最大的功能是將陣列項目依模版轉換產生DOM元素,以清單方式呈現資料。而我們都知道,動態DOM元素操作往往是效能瓶頸所在,想像以下情境:以AJAX方式由伺服器端取回100筆資料的陣列,交由ngRepeat轉化為100列<tr>;隨後資料更新,再次由伺服器取回陣列,同樣為100筆,但其中有5筆順序調換、10筆舊資料被刪除、另外10筆資料是新増的。此時ngRepeat會如何處理?丟棄前次產生的100個<tr>再重來一次?還是能重覆利用已產生的DOM元素提升效率?但新舊100筆有新增有刪除有順序調動,ngRepeat要如何解決新舊資料對應問題?(恭喜可直接回答以上問題的朋友,請關閉瀏覽器略過)

過年前在專案上遇到此一疑惑有些迷惘,代表自己學藝不精,技術問題不好拖過年,決定寫幾個範例做實驗為自己解惑。

首先,要先找出「DOM元素被重覆利用而非重新產生」的偵測方法,我最愛用的做法是透過jQuery.css("color", "…")修改文字顏色,這種事後加工在ngRepeat重新產生DOM元素不會被保存,文字顏色一旦還原便表示DOM元素已被重建。

設計測試網頁如下:

<!DOCTYPEhtml>
<htmlng-app="app">
<head>
 
<metacharset="utf-8">
<title>ngRepeat DOM重覆使用測試 1</title>
<style>
    .list {
       width: 300px;
    }
    .list td {
      border: 1px solid gray;
    }
    input { display: block; margin-top: 6px}
</style>
</head>
<bodyng-controller="ctrl as vm">
<tableclass="list">
<trng-repeat="player in vm.players">
<tdng-bind="player.id"></td>
<tdng-bind="player.name"></td>
<tdng-bind="player.score"></td>
</tr>
</table>
<inputtype="button"value="第二筆染色(by jQuery)"ng-click="vm.color2ndRow()"/>
<inputtype="button"value="變更第二筆分數"ng-click="vm.changeScore()"/>
<inputtype="button"value="第二筆移至最後"ng-click="vm.move2ndRow()"/>
<inputtype="button"value="依分數排序"ng-click="vm.sortArray()"/>
<inputtype="button"value="重新產生陣列"ng-click="vm.updatePlayers()"/>
<scriptsrc="//code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.js"></script>
<script>
function Player(id, name, score) {
this.id = id;
this.name = name;
this.score = score;
    }
function getPlayers() {
return [
new Player("A01", "Spider-Man", 63),
new Player("A02", "Iron Man", 127),
new Player("A03", "Jeffrey", 255),
new Player("A04", "Darkthread", 32767)
      ];
    }
function myViewModel($scope) {
var self = this;
      self.players = getPlayers();
      self.color2ndRow = function() {
        $(".list tr:eq(1)").css("color", "red");
      };
      self.changeScore = function() {
        self.players[1].score = 0; //修改第二筆分數
      };
      self.move2ndRow = function() {
var removed = self.players.splice(1, 1)[0];
        self.players.push(removed); //移動第二筆至最後
      };
      self.sortArray = function() {
//依分數排序
        self.players.sort(function(a, b) { return a.score > b.score; })
      };
      self.updatePlayers = function() {
        self.players = getPlayers();
      };
    }
    angular.module("app", [])
    .controller("ctrl", myViewModel);
</script>
</body>
</html>

一個陣列交給ngRepeat展成<table>,另外再放幾顆觸發測試,操作示範如下: Live Demo

一開始使用jQuery.css()將第二筆資料改為紅色,之後修改score、調動順序、陣列重新排序,該筆資料維持紅色,但是當重新指定給self.players內容完全相同的陣列,A02 Iron Man變回黑色。

看似奇妙的運作,原理在本草綱目ngRepeat官方說明已有記載,ngRepeat為每個player物件偷偷加上唯一的$$hashKey屬性值,以此識別物件與DOM元素的對應。當同一物件屬性被修改、在陣列的順序被調動,其$$hashKey不受影響,故ngRepeat會修改、調動既有DOM元素,而不是重新建立。在self.updatePlayers()裡,雖然player物件陣列內容完全相同,但其中的palyer物件與第一次產生的player物件為不同的執行個體(Instance),無法用$$hashKey對應到原有DOM元素,故ngRepeat選擇重新產生。

在<tr>加上第四欄<td ng-bind="player.$$hashKey"></td> 觀察$$hashKey的變化,可以發現到self.updatePlayers()之後$$hashKey全被換掉,解釋了為何重新指派相同內容陣列卻無法重覆使用DOM元素:Live Demo

面對此一行為,直覺解法是避免用self.players=… 重新指派陣列,改為「比對新舊資料項目,從原陣列中移除該移除的、新増要新増的、改掉待修改的項目」,藉以保留$$hashKey,儘可能沿用既有DOM元素。比對工程說難不難,但要自己寫少不了得費點工。所幸,ngRepeat自angular 1.2起新增了track by 子句,只需寫成"player in vm.players track by player.id",ngRepeat便會以player.id為鍵值進行前述比對,不用$$hashKey,改由id判斷是否資料為同一筆,決定是否沿用原來的DOM元素。

如以下範例,改寫成<tr ng-repeat="player in vm.players track by player.id">,即便重新指派self.players,只要player.id相同,將沿用現成的<tr>不重新建立,因此在按下「重新產生陣列」時,A02仍維持紅色。Live Demo

要注意的事,track by 指定的屬性必須具有唯一性,若同一陣列中出現兩筆相同值,將引發錯誤:Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: player in vm.players track by player.id, Duplicate key: A02, Duplicate value: {"id":"A02","name":"Darkthread","score":32767} Live Demo

了解此一特性後,遇到ngRepeat每筆資料DOM元素結構複雜或產生耗時(例如:內含複雜的Directive)的場合,記得可善用"track by …"提高DOM元素的再利用率,改善效能。

寫完馬年最後一篇文章,順祝大家羊年大吉,新春如意!

[NG系列]

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

NG範例23-使用Directive建立自訂網頁元素

$
0
0

可以用Directive製作自訂網頁元素是當初Angular吸引我的亮點之一(註:Knockout從3.2起也開始支援), 專案裡總不乏為特定規格量身打造的特製UI元素,像是分類、代碼或關鍵字多重查詢的商品輸入欄位,被重複應用在多個網頁輸入介面。用複製貼上是最下策(萬一邏輯要改,準備改到吐吧!),在ASP.NET WebForm裡可用UserControl或WebControl將這類要重覆利用的UI元素包成控制項,用<my:SuperProductPicker>快速安插到各網頁。在HTML5時代,自訂HTML元素也能實現類似的簡潔便利,配合NG/KO等Framework,將UI模版及邏輯包成Directive或Component, 便可比照<input>、<textarea>,用<my-super-product-picker prop1="…">在網頁擺放我們的自訂元素,這種組裝積木的做法,不正是寫應用程式的最高境界?

先前已寫過自訂Directive範例,但該範例在一般HTML元素加註anim-hide=,只透過jQuery加上動畫效果。在我後來的專案經驗,自訂Directive更常被拿來包裝自訂網頁元素,決定藉這篇文章整理自訂網頁元素的典型做法,自己筆記也分享供大家參考。

Directive的宣告格式有很多種,例如:可以只傳link函式,也可以傳一個包含link函式屬性物件再加上其他設定。scope方面也可選擇共享呼叫端的scope,或自建獨立的scope(Isolated Scope)。自訂元素若包含較複雜邏輯或會重覆出現,使用獨立scope是較好的選擇,我自己的應用中,Directive採獨立scope佔大多數。另外,<my-element>實際執行時需轉換成<div>、<input>… 等標準HTML元素,用jQuery操作DOM效率不佳且無法套用ng-bind、ng-show等語法,直接提供HTML範本給NG解析才是上策。要提供範本可透過template指定字串,或寫成獨立HTML檔案再以templateUrl載入。若範本簡單只有三五行,建議用template,否則寫成獨立HTML檔案較好,才不會"<div>line1…" + "line2…"+… 串字串串到抓狂。我做了一個範例,列舉我所知的常用的自訂元素Directive技巧。(如需要詳細的Directive說明可參考官方文件

懶得重想情境,直接拿Template範例來改,將原本的:

<span ng-repeat="member in model.members">
      <span ng-include src="'/contactTmpl.html'" ng-show="member" class='cont-block'>      </span>

改成用自訂元素<role-card>呈現,並開放電話號碼可修改:

<span class='cont-block' ng-repeat="member in model.members">
      <role-card name="member.name" phone-no="member.phone" />
</span>

另外,leader項目還多示範如何指定CSS及從Directive呼叫View Model端函式:

<role-card name="model.leader.name" phone-no="model.leader.phone" name-css="black" phone-css="red" on-change="model.log(newValue)" />

執行結果如下:


Directive宣告部分長這樣:

    .directive("roleCard", function() {
return {
        restrict: "AE",
        scope: {
          name: "=", 
          phone: "=phoneNo", 
          nameCss: "@", 
          onChange: "&"
        },
        link: function(scope, element, attrs) {
          scope.phoneCss = attrs.phoneCss;
          scope.$watch("phone", function(v) {
if (angular.isFunction(scope.onChange)) {
              scope.onChange({newValue: v});
            }
          });
        },
        template: 
"<dl>" +
"<dt>Name</dt><dd class=\"{{nameCss}}\">{{ name }}</dd>" +
"<dt>Phone</dt><dd>" + 
"<input type=\"text\" ng-model=\"phone\" class=\"{{phoneCss}}\"/>" +
"</dd>" +
"</dl>"
      }
    });

restrict: "AE",表示這個Directive支援<div role-card>("A"ttribute)及<role-card>("E"lement)兩種標示方法,採用後者就等於自訂元素。

template用來指定範本字串,在其中可放膽使用ng-model、{{propName}},跟一般NG寫完全相同,但如你所見,串接HTML字串時單引號、雙引號、換行字元都要特別處理,行數一多頗為煩人,將HTML搬去獨立檔案再改用templateUrl請NG動態載入是較好的策略。

link函式用來放置對DOM元素操作、事件處理及資料連動等程式邏輯,是Directive的程式核心。

獨立scope比較多眉角,故留到最後才講。使用自訂元素時,一般會由外部傳入參數,提供資料或控制元素外觀,而唯一的溝通管道只有HTML Attribute,例如:<my-element prop1="somePropToBind" pop2="elementStyle" pop3="someCallback()">,而這些外界傳入內容需放入獨立scope,才能在template或templateUrl的HTML中被正確存取,因此需在獨立scope宣告屬性對應這些Attribute。程式碼中出現三個特殊符號:"="、"@"、"&",各有不同意義,用以下實例解釋:

  • name: "="
    在獨立scope宣告name屬性並雙向繫結到<contact-card name="...">所指的ViewModel屬性,該屬性異動會反應到scope.name,修改scope.name也會反應回去。
  • phone: "=phoneNo"
    若scope使用的變數名稱與HTML Attriube命名不同,需另外註明。phone: "=phoneNo"指繫結對象由<contact-card phone-no="propName">指定。需注意:NG內部一律採Camel大小寫命名規則,故HTML Attribute得寫phone-no才會轉成phoneNo,寫成<contact-card phoneNo="…">永遠對不上。
  • nameCss: "@"
    @符號在scope中代表nameCss屬性將單向繫結到name-css Attribute,其傳入型別永遠是字串。Attribute值改變會反應到scope.nameCss,但修改scope.nameCss不會反應回外層View Model。 指定靜態值直接寫<contact-card name-css="black",若要跟View Model連動,則可以寫成name-css="{{model.nameCss}}"。
    =及@應用上有個小陷阱,若宣告成nameCss: "=",傳入靜態字串參數需寫成name-css="'black'"(要加單引號寫成JavaScript字串);而nameCss: "@"寫name-css="black"就可以。在傳字串常數時要認清是單向繫結"@"或雙向繫結"=",以免混淆。
  • onChange: "&"
    若要從Directive觸發事件或呼叫外層View Model函式,可使用&符號。以onChange: "&"為例,<contact on-change="vm.SomeFunction()">代表在Directive呼叫scope.onChange()會執行vm.SomeFunction()。
    如果呼叫函式帶有參數,記得要一併寫在on-change Atrribute,例如:<contact on-change="vm.SomeFunction(text)">,但Directive端呼叫時的做法較特殊,不能直接寫scope.onChange("The Parameter"),得將參數轉成物件寫成:scope.onChange({ text: "The Parameter" }) 才會順利。

以上就差不多就是寫Directive會用過的技巧,只要搞懂應不難上手。

大家一起來寫自訂元素,擺脫醜陋的複製貼上,讓網頁擠身上流社會吧~ :P

附上完整種式碼及Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>NG Directive 範例</title>
<style>
    .cont-block 
    {
        font-family: Segoe UI; font-size: 10pt;
        border: 1px solid gray; padding: 5px; width: 130px;
        border-radius: 4px; box-shadow: 5px 5px 10px #444;
    }
    .memb-list .cont-block { float: left; margin-right: 15px; }
    dt { font-weight: bold; color: purple; }
    dd { color: brown; }
    dd input { width: 80px; }
    br { clear: both; }
    dd.black { color: black; }
    input.red { color: red; }
 
</style>
</head>
<bodyng-controller="defaultCtrl as model">
<h3>Leader</h3>
<divclass='memb-list'>
<spanclass='cont-block'>
<role-cardname="model.leader.name"phone-no="model.leader.phone"
name-css="black"phone-css="red"on-change="model.log(newValue)"/>
</span>
</div>
<br/>
<h3>Members</h3>
<divclass='memb-list'>
<spanclass='cont-block'ng-repeat="member in model.members">
<role-cardname="member.name"phone-no="member.phone"/>
</span>
</div>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.js"></script>
<script>
function Contact(name, phone) {
this.name = name;
this.phone = phone;
    }
 
function myViewModel($scope) {
var self = this;
      self.leader = new Contact("美國隊長", "0800092000");
      self.members = [];
      self.members.push(new Contact("鋼鐵人", "28825252"));
      self.members.push(new Contact("索爾", "23939889"));
      self.members.push(new Contact("浩克", "0800956956"));        
      self.log = function(msg) {
        console.log(msg);
      }
    }
    angular.module("sampleApp", [])
    .controller("defaultCtrl", myViewModel)
    .directive("roleCard", function() {
return {
        restrict: "AE",
        scope: {
          name: "=", 
          phone: "=phoneNo", 
          nameCss: "@", 
          onChange: "&"
        },
        link: function(scope, element, attrs) {
          scope.phoneCss = attrs.phoneCss;
          scope.$watch("phone", function(v) {
if (angular.isFunction(scope.onChange)) {
              scope.onChange({newValue: v});
            }
          });
        },
        template: 
"<dl>" +
"<dt>Name</dt><dd class=\"{{nameCss}}\">{{ name }}</dd>" +
"<dt>Phone</dt><dd>" + 
"<input type=\"text\" ng-model=\"phone\" class=\"{{phoneCss}}\"/>" +
"</dd>" +
"</dl>"
      }
    });
</script>
</body>
</html>

[NG系列]

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

2015三芝櫻木花道馬拉松

$
0
0

第22馬,三芝櫻木花道馬拉松。

時節正好,名符其實的「櫻花馬」,整場用八個字總結:霧裡看花,宛如仙境,令人印象深刻的一次獨特跑馬體驗。

氣象預報228三芝地區降雨機率10%,氣溫19-22,預期為涼爽陰天。車行到淡水,開始下起毛毛雨,溫度也只剩15度。平心而論,這不是賞花的好天氣,但低溫微雨很適合跑馬。賽前把這場定位為「慢跑裝郊遊」,決定揹著水袋(事前聽說補給普普,果真不怎麼樣),特別帶上RX100 M3相機準備拍花,懷著輕鬆心情踏青。

有了上回的愉快經驗,這次再次跟忠孝哥組成LDS(喇笛賽)二人組。說實在的,邊跑邊聊,比一個人埋頭獨跑有趣太多了。

八點零二分(是的,大會的時鐘慢了,現場上千支以衛星訊號校時的GPS手錶可做為呈堂證供)不準時鳴槍出發。

最開始是一段河濱賞花步道,路面為岩石拼貼,每次著地都得小心翼翼,謹慎調整落腳角度避免扭到腳,跑來頗為累人。(照片為回程補拍)

跑了近一公里,轉成石板路,好跑一些。

再跑了幾百公尺接上產業道路,踏上賞花旅程。

被兩排筆直高聳樹木簇擁的感覺很棒。

開始爬坡,海拔攀升到200公尺後踏入另一個世界。我搞不清楚這白茫茫一片該算是雲還是霧,依其濃密程度,姑且視之為雲吧!之後的路程便一路都在雲裡,如夢似幻,很不真實。

跑著跑著,前方出現一座牌樓,莫非我們到了南天門,要進天宮了?

這場景,像不像天界的宮殿?

其實這是北星真武寶殿,全程最高點,海拔超過六百公尺。回家後爬文才知其原貌,但我更愛雲霧夢幻版。

前往真武寶殿的上坡極陡,上坡用走的只感吃力不覺特別,下坡時雙腳被地心引力牽動,逼著不得不跑起來,為維持平衡身體不斷後傾,有種「躺著跑」的感覺。

櫻花是本場最最最大的亮點!刻意帶相機隨行,一路始終雲霧繚繞,大大限制了發揮空間,但四處櫻花怒放,仍十足展現出另一種美。

 

 

 

 

 

 

 

相較於絢麗的櫻花及夢幻雲霧,大會補給有點掉潻。一般馬拉松賽事多到乏人問津的香蕉,以一根切成六段的方式節儉登場,巧克力則藏身糖果堆當卧底。跑了1/3,一度只剩水跟運動飲料,20K左右跑到饑腸轆轆,頓時心生「在現實人生裡,再美的櫻花都無法取代爆米花」之俗世感嘆,而自己揹了水袋卻忘了塞些補給品是大失策。所幸,下個補給站餅乾跟巧克力再次現身化解危機,但最後1/3,補給站幾乎都只剩水,連運動飲料都絕跡,直到最後有兩站緊急補了土司,拯救了不少人,避免跑友落得啃樹根或人吃人的命運(大誤)。水站志工大概也很無奈吧?巧婦難為無米之炊,大概是被跑友罵怕,我聽到一位志工直說:「東西真的太少了,大家要跟大會抗議」,這是實在話,補給匱乏多半是大會規劃失當,志工們很辛苦也很無辜,不該把氣出在水站工作人員身上。

摸到先前在網路看過的「北七 7」里程牌,令我心情一陣激動~

這張則是經典「北七,由此去」 XD (我知道很幼稚,但我好愛這種惡搞趣味,無法自拔 orz)

跑到北七線底,在懷恩聖地(民營墓園)折返…

32K里程牌則在三芝櫻花生命園區(樹葬園區)旁,加上全程不散的雲霧,敢情今天一直跑在人間與仙界的交界上。XD

最後5:27:50完賽(晶片時間),報完號碼十秒就領到成績單,但完賽證書居然沒印名次倒也罕見(果然也被跑友罵了),賽後餐點是小7國民便當跟瓶裝水,網路上有人說有貢丸湯,但我沒看到,或許半馬或健行組才有緣享用,浴巾很大條,完賽獎牌也不錯。

     

雖然賽前兩天才通知改路線改會場讓人傻眼、補給也不甚理想,但必須說,美麗的櫻花及夢幻般的雲霧救了這場比賽,明年若有機會,還會想再參加。

Windows,你為什麼不睡覺?

$
0
0

硬碟掛點事件後,我重新規劃工作機配置,作業系統改灌Windows 8,並養成下班就休眠,節約能源兼延年益壽,甚至還研究了WOL(網路喚醒)自己寫WOL發送程式解決VPN連線的特殊需求。

運作一段時間後發現一個問題:機器常常會在半夜自己醒來!電腦鬧鬼事件很早前就調查過,泰半是自動更新排程引起,但深入調查工作機被喚醒的時間並不固定在凌晨三點,01:14, 05:31, 03:50, 04:22都有,理不出規則性,且發生頻率高到幾乎每天都有,確定已停用滑鼠及鍵盤喚醒功能,研判除了自動更新,還有其他排程也會喚醒電腦(猜想說不定與機器加入網域有關)。清查找不到時間吻合的排程作業,案情陷入膠著。不過,這不該是嚴重問題,因為我們還有另一項法寶:Windows閒置一段時間後,應該要自己跑去睡覺。

 

換句話說,就算機器半夜被未知排程喚醒,事情做完沒人理他,一小時後就該乖乖去睡回籠覺。工作機不知為何一被喚醒就再睡不著,於是常常到公司發現電腦已開機數小時,「上班時間才開機」的理想破滅。

找不到喚醒電腦的凶手,找出害電腦失眠的原因也成。電源管理萬用工具程式,powercfg.exe,有個功能-查出阻止電腦休眠的裝置或程式,指令為powercfg /requests。注意:記得要以系統管理員身分開啟「命令提示字元」(cmd.exe)再執行powercfg相關指令。

例如:我們都知道Windows Media Player播放影片時,電腦不會跳出螢幕保護程式或進入睡眠狀態,不然看電影得不停搖滑鼠防止電腦睡著很蠢。實測先開啟Windows Media Player播放影片,再執行powercfg /requests,可看到以下結果:

查詢結果顯示Windows Media Player.exe及音效卡驅動程式處於使用狀態,會防止電腦啟動螢幕保護程式或進入睡眠模式(換句話說,電腦可藉此判斷系統忙碌)。關閉媒體播放程式,再執行一次powercfg /requests,這次清單空空如也,此時若不動滑鼠或鍵盤一段時間,便會依電源計劃關閉螢幕或進入睡眠模式。

透過powercfg的應用程式與驅動程式電源要求查詢,我們能很快找到電腦無法進入睡眠模式的原因,確保電源計劃能如期執行。

回到我的工作機案例,睡不著的原因是擷圖工具SnagIt造成的,爬文找到有人提及為舊版問題。將SngIt升級到12.3版,無法入睡問題排除,又重回節能環保的正軌囉~

【茶包射手日記】在IE Modal Dialog無法安裝ActiveX控制項

$
0
0

公司e-Learning網站使用了PowerCam元件播放教學錄影,不知從IE9還是IE10起,開啟課程內容網頁,內嵌PowerCam ActiveX控制項的網頁無法自動下載安裝元件,會出現以下訊息:

One or more ActiveX controls could not be displayed because:
1) Your current security settings prohibit running ActiveX controls on this page, or
2) You have blocked a pubisher of one of the controls.
As a result, the page might not display correctly.

無法顯示一個或多個 Active X 控制項,因為:
1) 您目前的安全性設定禁止在這個畫面執行 ActiveX 控制項,或者
2) 您已經封鎖其中一個控制項的發行者。
結果是畫面可能無法正常顯示。

直覺與IE安全設定,試過幾種設定組合,將網站加入受信任網站都無法解決問題。偵察過程發現PowerCam元件所在網頁採用showModalDialog開啟,打開F12開發者工具抓出網頁Url,改以IE直接開啟,忽然冒出安裝提示令人驚喜。下載安裝控制項後,教學影片順利播放,之後開啟該課程網頁就正常了。

換言之,關鍵在於Modal Dialog! 使用showModalDialog開啟內嵌ActiveX控制項網頁,瀏覽器拒絕執行ActiveX控制項;未變更任何IE安全設定,改成直接開啟該網頁,ActiveX控制項就能下載、安裝並正確執行;一旦安裝完成,用showModalDialog開啟也能正確執行ActiveX控制項。代表「Modal Dialog阻擋ActiveX控制項安裝,但不限制其執行」?

寫了兩個簡單網頁重現問題,在Test.html內嵌PowerCam播放器ActiveX控制項:

<html>
<body>
TEST:
<OBJECTID=player
CLASSID='CLSID:884C5B76-B154-45b5-A1ED-3746D0CCA352'
CODEBASE='http://download.powercam.com.tw/fsplayer6.cab#version=6,0,0,619'>
</OBJECT>
</body>
</html>

在另一個Host.html網頁以showModalDialog開啟Test.html:

function openModalDialog() { 
  window.showModalDialog("TEST.html", "diag", "width:300px;height:300px"); 
} 

點擊按鈕開啟Modal Dialog後,確實可重現拒絕安裝ActiveX控制項的情境!

可惜的是,我找不到官方文件提到這項Modal Dialog對ActiveX控制項的額外限制,無從得知可否透過安全設定避免,改用IE直接開啟網頁安裝控制項這招看來可行,就當成Workaround吧!

Strings.StrConv半形全形轉換注意事項三則

$
0
0

Microsoft.VisualBasic.Strings.StrConv靜態方法是在.NET轉換半形全形最簡便的做法(即使語言是C#也沒差,在專案加入Microsoft.VisualBasic參照即可),最近實際用在專案,又發現了一些眉角,整理筆記備忘:

  1. 在開發機測試OK,丟到測試機執行冒出VbStrConv.Wide and VbStrConv.Narrow are not applicable to the locale specified.錯誤。研究後發現問題出在測試機作業系統語系為英文,轉換中文字串會因語系不符出錯。解決方法為傳入第三個參數指定LCID(未傳入的話會依作業系統語系決定),例如:
    Strings.StrConv("黑暗執行緒ABC123", VbStrConv.Narrow, 1028)
    繁體中文的LCID是1028、簡體中文則為2052,詳細列表可參考維基百科
  2. 因為要指定語系LCID,故務必確認輸入字串的語系,否則部分字元會變成問號,例如:
    Strings.StrConv("黑暗执行绪ABC123(简体)", VbStrConv.Narrow, 1028)
    簡體字串誤設1028(台灣繁體中文)會變成"黑暗?行?ABC123(?体)",需改為2052才會正確傳回"黑暗执行绪ABC123(简体)" 。
  3. 要指定語系?聰明如你,可能已經想到「如果有Big5難字怎麼辦?」 例如:"黑暗執行緒ABC123"。
    不幸地,Strings.StrConv全形半形轉換得靠ANSI編碼(例如:BIG5)將字元轉成位元組進行判斷,不支援Unicode。以上例子為繁體中文需指定1028,傳回結果「犇」會變成問號"黑暗執行緒?ABC123"。
    放棄Strings.StrConv,自己寫全形半形製轉換函式是一種解法。懶惰如我,則選擇用雞嗚狗盗之術抄小路,參考以前寫的潛盾機,先將難字轉成NCR(&#29319),其餘字元保留,做完全形半形轉換再將NCR還原回Unicode難字,薑!薑!薑!薑!搞定囉~ (細節請參考程式範例) 
    註:此處未考慮原本字串夾帶NCR的情境,且未細究效能,如要應用請自行改寫、評估。

展示程式如下供參:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
 
namespace ConsoleApplication1
{
class Program
  {
staticvoid Main(string[] args)
    {
//測試1
      Debug.WriteLine(Microsoft.VisualBasic.Strings.StrConv(
"黑暗執行緒ABC123", Microsoft.VisualBasic.VbStrConv.Narrow)
      );
//結果:黑暗執行緒ABC123
 
//測試2
//切成英語語系(原本為zh-TW)
      Thread.CurrentThread.CurrentCulture = 
new System.Globalization.CultureInfo("en-US");
try
      {
        Debug.WriteLine(Microsoft.VisualBasic.Strings.StrConv(
"黑暗執行緒ABC123", Microsoft.VisualBasic.VbStrConv.Narrow));
      }
catch (Exception ex)
      {
        Debug.WriteLine("錯誤:" + ex.Message);
//結果: 
// 錯誤:VbStrConv.Wide and VbStrConv.Narrow are not applicable to the locale specified.
      }
//測試3 加上LCID參數
      Debug.WriteLine(Microsoft.VisualBasic.Strings.StrConv(
"黑暗執行緒ABC123", Microsoft.VisualBasic.VbStrConv.Narrow, 1028));
//結果:黑暗執行緒ABC123
 
//測試4 簡體中文套用繁體中文LCID會產生問號
      Debug.WriteLine(Microsoft.VisualBasic.Strings.StrConv(
"黑暗执行绪ABC123(简体)", Microsoft.VisualBasic.VbStrConv.Narrow, 1028));
//結果:黑暗?行?ABC123(?体)
 
//測試5 套用簡體中文LCID,結果正確
      Debug.WriteLine(Microsoft.VisualBasic.Strings.StrConv(
"黑暗执行绪ABC123(简体)", Microsoft.VisualBasic.VbStrConv.Narrow, 2052));
//結果:黑暗执行绪ABC123(简体)
 
//測試6 夾雜Big5難字
      Debug.WriteLine(Microsoft.VisualBasic.Strings.StrConv(
"黑暗執行緒犇ABC123", Microsoft.VisualBasic.VbStrConv.Narrow, 1028));
//結果:黑暗執行緒?ABC123
//測試7 黑暗小雜技:先將難字換成NCR,轉完半形再換回來
      var ncrString = toNCR("黑暗執行緒犇ABC123");
      Debug.WriteLine(ncrString); //黑暗執行緒&#29319;ABC123
      var convString = Microsoft.VisualBasic.Strings.StrConv(
        ncrString, Microsoft.VisualBasic.VbStrConv.Narrow, 1028);
      Debug.WriteLine(convString); //黑暗執行緒&#29319;ABC123
      var resultString = fromNCR(convString);
      Debug.WriteLine(resultString); //黑暗執行緒犇ABC123
 
 
      Console.Read();
    }
 
//REF: http://blog.darkthread.net/blogs/darkthreadtw/archive/2007/04/21/733.aspx
staticstring toNCR(string input)
    {  
      StringBuilder sb = new StringBuilder();
      Encoding big5 = Encoding.GetEncoding("big5");
foreach (char c in input)
      {
//強迫轉碼成Big5,看會不會變成問號
string cInBig5 = big5.GetString(big5.GetBytes(newchar[] {c}));
//原來不是問號,轉碼後變問號,判定為難字
if (c!='?'&& cInBig5=="?")
          sb.AppendFormat("&#{0};", Convert.ToInt32(c));
else
          sb.Append(c);
      }
return sb.ToString();
    }
 
staticstring fromNCR(string input)
    {
return Regex.Replace(input, "&#(?<ncr>\\d+?);", (m) =>
      {
return Convert.ToChar(int.Parse(m.Groups["ncr"].Value)).ToString();
      });
    }
  }
}

丞相,起風了!從ASP.NET 5的變革談起

$
0
0


圖片來源

羊年開工第一天,Stephen Walther在部落格發表一篇新文章:ASP.NET 5及MVC6的十大變革,雖然大部分文章所提的,我先前就陸續看過或心裡有數,但全部聚在一起還是挺震憾。如果你現在有涉及ASP.NET專案開發,不管用的是WebForm、MVC、ASHX、WebService還是WCF,這次ASP.NET 5的改變象徵ASP.NET及.NET路線上的調整,此一發展方向遲早會影響大家的專案規劃、學習方向、公司策略,甚至職涯發展,即使有些變化(甚至該說是危機)還要好幾年才會更降臨,但提早掌握情勢押對寶做好準備總是好事。這篇文章以一個寫了十幾年ASP.NET的老鳥觀點,整理我覺得影響較劇,應該提早因應的重點。若用更積極的態度,大家可依據自身條件,甚至該想想是不是已到「現在不改變,將來就會後悔」的關鍵時刻?

再會了,WebForm

我相信WebForm仍是當前ASP.NET專案的主力,市佔率遠勝ASP.NET MVC,但殘酷的事實擺在眼前,MVC、HTML5才是明日之星!這幾年WebForm開發者應已隱約感受每次新版發表對WebForm的著墨愈來愈少(但多少還是有),直到這次,ASP.NET 5完全看不到對WebForm的改革創新,即便VS2015(甚至再下一版的Visaul Studio)仍可編譯維護WebForm專案,但可以預見的劇情發展是:愈來愈多的新功能只支援MVC、Web API,WebForm不再改版進步,接著,WebControl元件廠商停止推出新版強化,漸漸中止技術支援,市場愈來愈難找到熟悉WebForm的人才(就算找得到也不會是新鮮的肝,你懂的… XD),寫WebForm專案的同學會像今天還在寫ASP的同學,需要強大心理素質面對冷言冷語:「現在都流行科技新貴,你怎麼還在做菜頭粿?」

以上預言要全部實現還有點遙遠(在本公司存活超過十年仍頭好壯壯沒人敢動的ASP表示:安啦,還早得很!) 但WebForm這條路,註定會愈走愈寂寞,愈走愈冷,WebService、SVC也會是相同處境。可預期在HTML5已成主流的今天,依賴PostBack運作的WebForm,難以整合最新的前端技術,做不出「目前最流行的網頁效果」,愈來愈難贏得使用者與開發者青睞,終究得退出江湖。

如果你現在工作主力仍在WebForm,行有餘力花點時間學習了解ASP.NET MVC/Web API這些新東西,保證是不會後悔的投資。

別了,VB.NET

當年從ASP VBScript/VB6 COM轉到.NET,曾一度為該選VB.NET還是C#猶豫,如今,可正式確認我押對寶了。(即使沒押對,之後幾年也會投靠C#吧!光看網路Sample Code比例就該知道)

這些年來,寫VB.NET的人愈來愈少。早些年,電腦書還得C#/VB.NET範例程式碼並陳,甚至為VB.NET及C#各出一本。而今新出版的電腦書裡已不見VB.NET範例,在網路上找到的文章VB.NET也近乎絕跡。

ASP.NET 5已不再提供VB.NET版專案範本(至少RTM前是如此,未來會不會透過Package加入支援不得而知。參考),如果你對VB.NET跟ASP.NET 5二者的堅持都強到「Over My Dead Body」,還是可能純手工用VB.NET打造ASP.NET 5網站,並容我致上最高敬意。

我想VB.NET會跟WebForm一樣,進入使用者愈來愈少,市場變小投入研發資源跟著變少的循環,進而走入歷史,手上仍有VB.NET專案的朋友可以考慮Telerik Code Converter之類工具,擇日將VB.NET轉成C#,長痛不如短痛,跟上主流日子較輕鬆。

AngularJS扶正

原本曾為ASP.NET 4專案先發陣容的Knockout,在這場KO vs NG較勁裡已確定敗下陣來(猶如當年ASP.NET AJAX vs JQuery的翻版,微軟終究得擁抱開發社群的主流選擇),Angular取代成為ASP.NET 5專案的先發一軍,Visual Studio 2015還提供建立AngularJS Module、Controller、Directive、Factory的項目模版,並整合了Grunt方便批次壓縮、打包Angular Script,在Visual Studio使用Angular將會享受更多便利。

另外,用Visual Studio寫Angluar程式,放著強大的TypeScript不用是暴殄天物。最近幾天有則大消息:TypeScript將正式成為Angular 2.0的開發語言(NG2.0改版幅度之大,足讓NG開發人員倒抽一口冷氣,且不支援NG1.x直接升級,基本上只能砍掉重練!但這又是另一篇故事了),想搭乘Angular 2.0列車,Visual Studio + TypeScript像張商務車票,可讓你舒適抵達目的地,好語言,不學嗎?

現在再回首,當初寫SPA專案決定由KO轉向NG是對的,未來可望繼續享受Visual Studio的強大支援。(謎之聲:尚書大人還真是機靈,在下佩服…)

擺脫Windows的束縛

過去ASP.NET跟IIS/Windows一直被緊緊綁在一起,這點在.NET開放原始碼後已完全改觀。而ASP.NET 5起,專案可選擇針對不同平台編譯,ASP.NET網站可輕易搬到Windows以外的平台(Mac、Linux甚至名片大小的樹莓派迷你電腦)執行。

基於成本考量,傳統架設大型網站主機群(Web Farm)多選擇Linux作業系統,過去ASP.NET面對這類情境就只能直接領便當。ASP.NET 5起就不同了,同樣的ASP.NET程式放在Windows、Mac或Linux都可照常執行,正式進入跨平台時代。

對於原有ASP.NET開發者,此一改變背後的挑戰除了學習ASP.NET 5新架構外,也要開始熟悉Windows以外的作業系統,學習怎麼用SSH/Telnet連上Linux下指令、查問題,如何用SCP/FTP從Windows上傳檔案到Linux主機,又有更多新東西要學囉!

結語

學習及適應需要投入力氣時間,沒有人喜歡改變,沒有人想砍掉重練,但時代趨勢誰都無法阻擋,要存活只能不斷調整因應,融入潮流。就算你厭煩一天到晚砍掉重練,氣餒到想改賣香雞排,也得想想顧客的喜好會一直改變,口味一成不變加上競爭激烈,只怕也撐不了幾年(說不定還沒WebForm撐得久 XD)。拒絕面對環境改變,不想學習新技能,難保不變成困在最後一塊浮冰上的北極熊… 共勉之。

【茶包射手日記】TypeScript JS檔打包壓縮時發生錯誤

$
0
0

前陣子專案的TypeScript更新成1.4,今天上線時發現網站出現JavaScript錯誤,由錯誤訊息推測為打包壓縮程序有誤:第一個foo.js的最後一列 //# sourceMappingURL=foo.js.map 跟第二個bar.js的第一列;var __extends = this.__extends || function (d, b) { 被合併成一列,由於開頭有//符號,整列被註解掉,bar.js的第一列被吃掉以致程式碼不全出錯:

/* Minification failed. Returning unminified contents.
(12,1-2): run-time warning JS1002: Syntax error: }
 */
var models;
(function (models) {
function foo() {
    }
    models.foo = foo;
})(models || (models = {}));
//# sourceMappingURL=foo.js.map;var __extends = this.__extends || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var models;
(function (models) {
varbase = (function () {
functionbase() {
        }
returnbase;
    })();
var bar = (function (_super) {
        __extends(bar, _super);
function bar() {
            _super.apply(this, arguments);
        }
return bar;
    })(base);
})(models || (models = {}));
//# sourceMappingURL=bar.js.map;

同樣的程式碼及打包壓縮程序在升級TypeScript 1.4前運作正常,讓人合理懷疑是1.4版調整輸出JavaScript格式導致。找到尚未升級TypeScript 1.4的VS2013,比對TypeScript的編譯結果,果真找到差異。下圖左方檔案來自1.3版,右方則是1.4版,可以看到1.3版在//# sourceMappingURL=foo.js.map後原本還有一列空白(共9列),1.4時被移除了(只有8列)。

1.4版的//# sourceMappingURL=foo.js.map後方少了換行符號及空白列,ScriptBundle又直接串接foo.js與bar.js,因而釀成慘劇。但是,TypeScript改版導致打包壓縮出錯,理應哀鴻遍野才對,但網路未見相關討論,莫非與ScriptBundle版本有關?

檢查有問題的ASP.NET 4專案,System.Web.Optimization.dll為1.0.0版。另外新建一個ASP.NET 4.5,放入相同TypeScript並用ScriptBundle打包,結果正常:

檢查ASP.NET 4.5的System.Web.Optimization.dll為1.1.0,由此推論問題與System.Web.Optimization版本有關。而原本的MVC4專案,在使用NuGet將System.Web.Optimization由1.0.0升級到1.1.0後,JavaScript錯誤消失無蹤,證實確為ScriptBundle版本問題!

結論:專案升級TypeScript 1.4後,System.Web.Optimization請升級到1.1.0+,以免打包壓縮出錯。

【茶包射手日記】詭異錯誤:MVC正常,WebAPI故障

$
0
0

接獲報案,某ASP.NET MVC網站出現奇妙現象:MVC Controller/View功能正常,存取WebAPI ApiController則傳回HTTP狀態500(內部伺服器錯誤),但除了狀態碼沒有其他錯誤訊息。

原以為該Web API Controller程式有錯,進一步測試其他Controller,發現非Web API的Controller都正常,URL只要符合"/api/controller/id",就會傳回沒有錯誤訊息的HTTP 500,甚至胡亂輸入不存在的/api/ooo/xxx也會傳回HTTP 500(正常狀況應傳回No type was found that matches the controller named 'ooo'),檢查事件檢視器沒有任何ASP.NET/IIS相關錯誤。至此,案情陷入膠著…

冷靜檢視線索,由「只要URL符合/api/controller/id格式,不管Controller存在與否,一律傳回HTTP 500」的行為推斷,問題可能發生在路由解析階段,但少了錯誤訊息就像矇眼射箭,找不到頭緒,故首要之急得快點找出詳細錯誤訊息。

爬文才知,Web API出錯時不是依web.config customErrors決定是否回傳錯誤訊息,而是另一套客製化錯誤設定-GlobalConfiguration.Configuration.IncludeErrorDetailPolicy。如同customErros,一樣有LocalOnly、Always、Off三種模式,推測該設定被設為LocalOnly,故由遠端瀏覽只有HTTP500,沒有錯誤細節。Terminal Service登入問題主機,瀏覽localhost/api/ooo/xxx,總算揭開神祕面紗:

HTTP/1.1 500 Internal Server Error
Date: Tue, 10 Mar 2015 10:08:33 GMT
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8

<Exception>
<ExceptionType>System.IO.FileNotFoundException</ExceptionType>
<Message>
Could not load file or assembly 'Autofac, Version=3.0.0.0, Culture=neutral, PublicKeyToken=17863af14b0044da' or one of its dependencies. The system cannot find the file specified.
</Message>
<StackTrace>
Server stack trace:
   at System.Reflection.RuntimeAssembly.GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes)
   at System.Reflection.RuntimeAssembly.GetExportedTypes()
   at System.Web.Http.Dispatcher.DefaultHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.WebHost.WebHostHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.Dispatcher.HttpControllerTypeCache.InitializeCache()
   at System.Lazy`1.CreateValue()
Exception rethrown at [0]:
   at System.Reflection.RuntimeAssembly.GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes)
   at System.Reflection.RuntimeAssembly.GetExportedTypes()
   at System.Web.Http.Dispatcher.DefaultHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.WebHost.WebHostHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.Dispatcher.HttpControllerTypeCache.InitializeCache()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at System.Lazy`1.get_Value()
   at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.InitializeControllerInfoCache()
   at System.Lazy`1.CreateValue()
Exception rethrown at [1]:
   at System.Reflection.RuntimeAssembly.GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes)
   at System.Reflection.RuntimeAssembly.GetExportedTypes()
   at System.Web.Http.Dispatcher.DefaultHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.WebHost.WebHostHttpControllerTypeResolver.GetControllerTypes(IAssembliesResolver assembliesResolver)
   at System.Web.Http.Dispatcher.HttpControllerTypeCache.InitializeCache()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at System.Lazy`1.get_Value()
   at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.InitializeControllerInfoCache()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at System.Lazy`1.get_Value()
   at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.SelectController(HttpRequestMessage request)
   at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsyncInternal(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
</StackTrace>
</Exception>

訊息明確,Autofac版本不對!專案已升級到3.0版,問題主機的Autofac.dll漏更新,還是2.2版,只需更新版本就可解決。

問題排除了,但我有疑問:為什麼問題只發生在Web API Controller? 決定追根究底。

由訊息WebHostHttpControllerTypeResolver跟Autofac關鍵字,直覺已被設定用Autofac解析Controller型別。只是在程式碼中挖了很久,怎麼都找不出指定由Autofac解析Controller的設定(標準做法是指定config.DependencyResolver,參考),一度以為是某種沒看過的巫術。迷惘了一陣子,回頭重看錯誤訊息Callstack,這才發現,或許Autofac不是用來解析Controller型別,先前完全查錯方向。

試著將專案移到我的Windows 8.1執行打算重現問題仔細研究,奇妙的事發生了!在我的機器上即使Autofac.dll被刪除也不會出錯!同事的Windows 7則能重現Autofac版本不符就爆的結果,再把檔案移到Windows 2003 VM測試對照,也能重現問題。這下子又多一項待解疑惑:Autofac版本不符錯誤只發生在Windows 2003/Windows 7,在Windows 8不會出現。

更!為了解開一個謎團,生出第二個謎團,這茶包會細胞分裂來著~(捏碎滑鼠)

為縮小問題範圍,祭出「消去法」,將專案非必要檔案一一移除,只求能重現/api/ooo/xxx這種亂打URL可以傳回No type was found that matches the controller named 'ooo'的行為即可。意外發現,在移除/bin下的Foo.dll、Bar.dll後,即使沒有Autofac.dll,/api/ooo/xxx的回應也是正常。而這兩顆DLL的關聯是:Foo.dll參照Bar.dll,Bar.dll再參照Autofac.dll。進一步測試,若只刪除Bar.dll,我會得到Could not load file or assembly 'Bar, Version=1.0…'錯誤。由此推測,Autofac版本不符錯誤來自Foo.dll的間接引用,與解析Controller名稱的DI(Dependency Injection)機制無關。

回頭重看Callstack,我注意到一個方法:System.Web.Http.Dispatcher.HttpControllerTypeCache.InitializeCache()。由此推測:每次存取/api/*時,MVC會掃瞄bin資料夾所有DLL,建立型別Cache。在處理Foo.dll時,先參照Bar.dll、Bar.dll又參照Autofac.dll,發生版本不符導致Cache無法建立,造成/api/*解析失敗,所有/api/*存取一律傳回HTTP 500。

/api/*路由失效有了合理解釋,但,為什麼一般MVC功能正常,只有執行Web API出錯?

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
 
routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

比較Web API與一般MVC路由註冊方式,發現Web API使用MapHttpRoute,MVC則是MapRoute,猜想兩種註冊方式背後運作的方式不同,是導致MVC功能OK,Web API失敗的原因。

最後一個謎:為什麼同樣的程式碼在Windows 7/2003會出錯,在Windows 8不會?

為此反組譯查出Callstack中出現的GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes),其實是個Unmanaged方法,實際函式在clr.dll(DllImport("QCall")是個奇妙註記,沒有所謂QCall.dll,而是指向放在.NET Runtime clr.dll的外部函式。參考):

[DllImport("QCall", CharSet=CharSet.Unicode, ExactSpelling=false)]
[SecurityCritical]
[SuppressUnmanagedCodeSecurity]
private static extern void GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes);

而Windows 7與Windows 8未共用clr.dll,因實做方式不同,各有自己專屬的版本(二者版號也不同:4.0.30319.18408 vs 4.0.30319.33440。參考),應是執行結果不同的原因。

所有的謎團都有了合理解釋,總算甘心合上茶包檔案夾,Case Closed。


網站除錯小技巧-用TELNET模擬HTTP請求

$
0
0

考慮以下情境:你使用Terminal Service登入遠端主機,基於資安限制,不能安裝軟體,無法傳送檔案到該主機,也該無法連線網際網路,唯一可用的瀏覽器只有IE6(喵的,IE6根本不算瀏覽器吧?),而你必須測試以HTTP POST存取某個API是否正常?

這像不像一宗密室殺人案件?看似所有可行做法都已被封殺,正等待金田一或柯南說出一個有點扯又有點巧妙的殺人計劃… 但,這是我親身遭遇的難題!(補聲暗)

仔細想想,情況並不如想像糟,雖然不能裝工具、沒有Chrome/POSTMan可用,還是可以寫VBScript/JavaScript操作XmlHttpRequest執行測試。只是平日都由jQuery或Angular代勞,對XHR語法不熟,還要查範例寫程式有點麻煩,直接用工具還是方便。靈機一動想到好方法-用Telnet模擬HTTP POST Request!

用Telnet測試問題主機的80埠,是我診斷網站問題常用的技巧之一。例如:發現用瀏覽器開啟網頁無回應,我會退一步試試Telnet web_server_ip 80,輸入GET /page_path按Enter,等同發出HTTP GET Request存取httq://web_server_ip/page_path,如果伺服器有回應內容,便代表問題出在瀏覽器,可能被設了無效的Proxy之類的。

以下是簡單示範,用Telnet讀取style.css:(注意:Telnet 80 Port連線成功後,鍵盤輸入的字元不會顯示出來,在以下操作中,輸入內容為"GET /MVC4/content/style.css",最後按Enter)

不過,這招測試.css、.js、.html等靜態資源OK,存取.aspx或MVC等動態程式卻常會失效,原因是網站伺服器對HTTP Request有更多要求,還需要額外Request Header。例如:若試著輸入GET /MVC4/Home/Index存取HomeController的Index() Action,會得到HTTP 400錯誤:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Bad Request</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Bad Request - Invalid Verb</h2>
<hr><p>HTTP Error 400. The request verb is invalid.</p>
</BODY></HTML>

原因是伺服器僅接受HTTP 1.1要求,我們在GET命令後方加上"HTTP/1.1"改成GET /MVC4/Home/Index HTTP/1.1。測試結果不同,但仍會出錯:

HTTP/1.1 400 Bad Request
Content-Type: text/html; charset=us-ascii
Server: Microsoft-HTTPAPI/2.0
Date: Tue, 10 Mar 2015 22:44:18 GMT
Connection: close
Content-Length: 334

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Bad Request</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Bad Request - Invalid Hostname</h2>
<hr><p>HTTP Error 400. The request hostname is invalid.</p>
</BODY></HTML>

錯誤訊息指出少了Hostname Header,我們充實Request內容再送一次,總算成功了:(註:HomeController.Inext()以return Content("Test OK")傳回單一字串)

依經驗,一個HTTP 1.1 GET Request起碼包含以下內容:

GET /MVC4/Home/Index HTTP/1.1
Host: localhost
Connection: close

網站伺服器需要"Host: URL使用的主機名稱"判斷該將Request交給哪一個Web站台處理(同一IP同樣80 Port,依Host名稱可連至不同Web站台);而HTTP 1.1預設使用Keep-Alive,指定Connection: close要求送完回應後主動斷線,可省去自己按Ctrl-C中止Telnet程式的步驟。

同理,只要組合適當的Request內容,要模擬POST送表單也不是難事。憑空拼湊Request Header太累,較簡便的做法是在其他機器用瀏覽器送一次,再經由F12工具找出送出內容。以Chrome為例,在Network找到Request封包,Request Headers有個「view source」按鈕:

展開後可以看到完整的Request Headers內容,有些非必要性如Cookie或User-Agent的可以省略,稍加調整就是要的測試內容:

如此,我們就能用Telnet模擬各式HTTP Request囉~

2015國道馬

$
0
0

第23馬,國道馬。

賽道單調,補給不值得期待(沒什麼東西,但一定不會沒東西),這場唯一的目標只有:PB、PB、PB。又寬又平的車道,不用一直轉彎繞圈,儼然運動場400米跑道的豪華升級版,速限110不用擔心跑太快吃罰單(誤)。如果這樣還跑不出好成績,只能怪自己。

去年底至今,體重逐步下探,操場五千米的成績也明顯進步,不需要使出吃奶力氣也可25分跑完,自我感覺十分良好,一度以為SUB4近了,但前兩週的櫻木花道馬,全程輕鬆跑,但30K後還是很清楚自己進入「還跑得動,猛踩油門只會冒很多白煙而不會變快」的沒勁狀態,心理有數,今年國道想挑戰SUB4機會渺茫。

週五還有冷氣團,降到完美的12度低溫,無奈週六回暖,到今天微陰偶有日照,氣溫17-26,天氣已不若前幾場低溫微雨理想,不敢奢想SUB4,只求能破去年台北馬PB(4:17)就偷笑。


五點多天色還沒亮

六點整起跑,排隊等廁所,晚到集合點只能站在後段,全馬半馬一起出發,場面浩大,但出發拱門好小隊伍拖得老長,開跑後大半天還沒什麼動靜,六分鐘後才通過起跑線。


呈現橘子色,被雲切成一段一段的太陽,我聯想到AT&T商標

既然要拼PB,不敢多逗留,只意思意思拍個幾張,前20K配速多還能維持520到540,狀況不差,路協的補給沒什麼驚喜,但準備份量與動線規劃沒什麼好挑剔,渴了餓了都一定有得吃喝,蘇打餅跟威化餅很好吃。


太陽出來了,啊啊啊啊~

隨著時間過去,天色漸亮氣溫回升,半馬之後,疲乏感也來了,不到酸痛疲累的程度,但雙腿明顯不如先前有力,漸漸守不住540配速(SUB4要求的平均配速),不時慢過六分速。


錯放的30K里程牌,明明在32K處

上回不知天高地厚,2小時48分跑30K就感動到眼眶泛淚,模糊間還以為自己就快抵達SUB4大門。這回差不多2:51跑完30K,但心境截然不同,已有自知之明,沒有太多激動。不慌不忙地探勘回周,才發現雖然SUB4城堡已在眼前沒錯,但圍著好大一條護城河,裡面全是鱷魚,以我現在的裝備跟等級,連到城堡門口按電鈴的資格都沒有。跑完32K時間來到3小時05分,很確定自己殘存的體力已不可能在55分內跑完10K,確定SUB4無望,最後10K半跑半走,勉強小破去年台北馬PB,以4:15:38完賽。


在終點遇見魯夫、長髮短裙美少女跟艾沙公主群

 


天氣不好,霧霾嚴重,幾乎看不到101

 


完賽補給:水、舒跑、沙琪瑪、國民便當跟香蕉

 


回程搭接駁車,附贈最後一擊:「天橋考驗」

 

     

獎牌挺好看,浴巾是大條的,Puma排汗衫,動線補給中規中矩,其實賽事已在水準之上。

嚴格講起來,自己是有進步的。以前每次跑完全馬都像浩刧餘生,走路像僵屍;到現在,跑完仍游刃有餘,腿腳不痛不掰咖(但酸軟難免),甚至還有力氣從捷運站騎U-Bike回家(撥瀏海)。而這場最大的感想:別用冬天的狀況推測春夏的成績。氣溫每上升1度,難度幾乎以等比級數提高。而奇妙的是,在能穩穩用3小時跑完30K後(以前全靠拖死狗硬撐,事前根本不知道能跑出什麼成績),反倒對「全馬不是半馬乘以二」有更深刻的體認。

至於SUB4大夢何時能圓?不是在此時,不知在何時,我想大約會是在冬季。

【茶包射手日記】ODP.NET如何找對Oracle Client檔案?

$
0
0

同事報案,某網站部署至新主機出現錯誤:

[OracleException (0x80004005): 提供者與 Oracle 從屬端版本不相容]
   Oracle.DataAccess.Client.OracleInit.Initialize() +517
   Oracle.DataAccess.Client.OracleParameter..cctor() +29

[TypeInitializationException: The type initializer for 'Oracle.DataAccess.Client.OracleParameter' threw an exception.]
   Oracle.DataAccess.Client.OracleParameter..ctor(String parameterName, Object obj) +0

又是ODP.NET跟Oracle Client檔案的匹配老問題,先前處理過兩回,都是改權限加IISRESET解決,這回故技重施卻無效… 是新茶包!趕緊著裝,打綁腿紥S腰帶,提槍快跑前進。

先檢查PATH環境變數:

發現主機上裝了好多版本的Oracle Client!共有D:\OracleR5、D:\Oracle、D:\Oracle2三個目錄,後兩者又各裝了32與64位元版,總共五套。

下一步要找出ODP.NET使用哪一套Oracle Client程式庫。去吧!Process Monitor,就決定是你了。

Process Monitor記錄(註:已篩選Path包含Oracle的項目)有不合理之處:網站程式先取得Oracle2\product\11.2.0\client_32\bin\oci.dll,之後嘗試在同目錄找不到OraOps11w.dll,便依著PATH環境變數找檔案,在Oracle5R找到OraOpsw11.dll,隨後則繼續使用Oracle2下的檔案。

懷疑所謂的「提供者與 Oracle 從屬端版本不相容」是混雜Oracle2與OracleR5兩個不同版本Oracle Client檔案造成。由Process Monitor記錄找到另一則線索:Registry HKLM\SOFTWARE\Wow6432Node\ORACLE\ODP.NET\2.112.1.0\DllPath (註:在64位元Windows,64位元版Oracle Client的Registry在HKLM\SOFTWARE\ORACLE,32位元版則在HKLM\SOFTWARE\Wow6432Node\ORACLE)

打開RegEdit找到DllPath機碼,瞬時恍然大悟!之前只知道32位元版跟64位元版的ODP.NET有能力自己找到對的Oracle Client版本,卻不知其所以然。原來魔法藏在這裡-每個ODP.NET版本有自已的Registry,其中包含DllPath設定指向配合的Oracle Client路徑。

檢查Registry得知2.112.1.0指向D:\Oracle2,2.112.3.0及4.112.3.0指向D:\OracleR5。問題網站使用ODP.NET 11.2.1,故由Registry找到D:\Oracle2,後來發現缺少OraOpsw11.dll,改以PATH環境變數找檔,最後到OracleR5抓交替(但抓到的是2.112.3.0版),由於跟其他2.112.1.0版程式不相容,轟一聲就爆炸了~

追查D:\Oracle2 OraOpsw11.dll缺檔原因,發現D:\Oracle2安裝不完整,少掉不少檔案,D:\Oracle比較像完整的2.112.1 Client安裝目錄。我動手修改上圖中的DllPath,將D:\Oracle2改成D:\Oracle,IISRESET後,錯誤消失。

問題排除,額外收獲是學到ODP.NET尋找Oracle Client檔案的邏輯,收隊!

更開放的國語辭典,教育部準備好了!

$
0
0

七年前,有感於教育部空有傲視全球的國語辭典資料庫,卻對民間應用設下種種限制,我曾投書教育部,寫信給立委,還寫了這篇-更開放的國語辭典,教育部準備好了嗎?

五年後,為了讓我的國語辭典App取得合法授權,抱著「申請表、切結書、計劃書再G8複雜」也要咬牙寫一下的心情,卻發現幾個月前還在網站的申請程序及表格下載網頁忽然人去樓空。透過教育部信箱詢問,得到的回覆是:《重編國語辭典修訂本》及其他線上字辭典正積極研議公開授權及相關事宜,未正式公告前無法授權完整內容供各界使用。而據了解,教育部希望最終能做到公開授權,但由於國語辭典著作權牽涉複雜,故處理過程繁瑣,開放之日難以預期。

最後,國語辭典App改用Google葉平教授所發起零時政府g0v還文於民」計劃,在黑客松所擷取的辭典資料庫(關於一群駭客集體砍站連夜刧回整部辭典的精彩故事,可參考OSDC 2013 唐鳳的演講 [第8分鐘開始]),並仿效萌典做法解決授權問題。

今天意外收到教育部承辦人來信(兩年前承辦的湯先生仍記得我的詢問,熱心告知好消息,就甘心A)。提到教育部已經以「創用CC-姓名標示- 禁止改作 臺灣3.0版授權條款」釋出《重編國語辭典修訂本》、《國語辭典簡編本》與《國語小字典》,允許使用者重製、散布、傳輸著作(包括商業性利用),但不得修改該著作(不得修改指的應是辭典內容,如字義解釋例句等,以免誤導大眾。資料格式調整修改應不在此限,不致影響開發),使用時必須遵照「使用說明」要求。

不需要檢附身分證、咬破手指寫切結書與瞎掰計劃書,任何人都可以直接由網站下載資料動手寫程式應用,也不禁止商業應用,這才是我心中開放資料(Open Data)的典範!

試著下載重編國語辭典資料檔,發現已結構化辭條項目,依注音排序分欄儲存在Excel檔,並已轉好Unicode碼。

Unicode不支援的異體字或罕見字改採圖檔顯示,GIF圖檔也已一併附上了。(但字型好醜 XD)

從今以後,要拿國語辭典發展有趣應用,再也不用擔心授權問題囉~

為教育部按一個大大的「」!

NG筆記24-KendoAutoComplete花式應用

$
0
0

對於Kendo UI AutoComplete的Client端應用,過去的印象還停留在「提供字串陣列作為資料來源」、「指定contains、startswith、endswith進行比對」、「提示項目跟帶入欄位值都來自字串陣列」、「不能限定比對吻合項目上限」,感覺不如jQuery自動完成彈性。如以下範例:Live Demo

若要做到更特殊的比對邏輯或想限制提示筆數,倒是可用serverFiltering實現,將輸入關鍵字以AJAX方式交給伺服器端程式比對再傳回提示項目結果。但對SPA及打算包成App的情境,仰賴伺服器才能運作的自動完成有點掉漆。

最近為了專案的特殊需求,較深入了解KendoAutoComplete後,驚奇發現Kendo UI的架構比想像來得彈性,許多原以為辦不到的規格要求,並不難實現。

以下是KendoAutoComplete小露身手的花式操槍表演,展示重點包含:

  1. 股票資料以JavaScript陣列方式提供,完全在Client執行,不需伺服器端程式
    技巧:使用dataSource,可傳入物件陣列,不限定字串陣列,再以dataTextField指定取用屬性
  2. 提示區塊可指定不同寬度
    技巧:.data("kendoAutoComplete").list.width(…)可指定提示區寬度
  3. 關鍵字搜尋範圍涵蓋股票代號、中文名稱及英文名稱三個欄位
    技巧:dataSource.transport.read()可以自訂JavaScript函式進行比對,取代AJAX呼叫
  4. 提示項目呈現時,符合關鍵字的欄位(可能是代號、中文或英文三欄之一)靠左顯示,代號及中文名稱靠右顯示,關鍵字部分以紅字標示
    技巧:template參數可指定函式,接入資料物件,傳回動態組裝的HTML,完全掌控提示區顯示格式
  5. 選定提示項目時,只將股票代號填入欄位,中文名稱填入另外欄位,顯示於股票代號之後
    技巧:dataTextFiled參數指定要填入,在change事件可取得股票代號值變化,加入自訂邏輯連動其他欄位

程式碼如下:Live Demo


<!DOCTYPEhtml>
<htmlng-app="app">
<head>
<metacharset="utf-8">
<title>kendoAutoComplete範例:強化版</title>
<linkhref="//cdn.kendostatic.com/2014.2.716/styles/kendo.common.min.css"rel="stylesheet"/>
<linkhref="//cdn.kendostatic.com/2014.2.716/styles/kendo.default.min.css"rel="stylesheet"/>
<style>
        .hi {
            color: red;
        }
 
        .hint {
            clear: both;
        }
 
        .f-l {
            float: left;
        }
 
        .f-r {
            float: right;
        }
        .symbol {
            width: 100px;
        }
</style>
</head>
<bodyng-controller="ctrl as vm">
<div>
<inputclass="symbol"kendo-auto-complete="vm.KendoObject"
k-options="vm.Options"ng-model="vm.Symbol"/>
<spanng-bind="vm.Name"></span>
</div>
<scriptsrc="//code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script>
<script src="//cdn.kendostatic.com/2014.2.716/js/kendo.all.min.js"></script>
<script>
var rawData = [
    { s: '1435', c: '中福', e: 'C.F.C.Y.CORP.' },
    { s: '1437', c: '勤益', e: 'GTM' },
    { s: '1471', c: '首利', e: 'SL' },
//...省略...
    { s: '8078', c: '華寶', e: 'CCI' },
    { s: '8101', c: '華冠', e: 'Arima Comm.' },
    { s: '8105', c: '凌巨', e: 'GiantPlus' },
    { s: '8249', c: '菱光', e: 'CSI' },
    { s: '9912', c: '偉聯', e: 'AIC' }
        ];
var book = {};
        $.each(rawData, function(i, item) {
            book[item.s] = item;
        });
 
function myCtrl($scope) {
var self = this;
            self.Symbol = "";
            self.Name = "";
 
function hiliteKeywd(str, keywd) {
return str.replace(keywd, "<span class='hi'>" + keywd + "</span>")
            }
            self.Options = {
                dataTextField: 's',
                filter: "contains",
                template: function (item) {
return"<div class='hint'><span class='f-l'>" + item.match +
"</span><span class='f-r'>" + item.s + " " + item.c + 
"</span></div>";
                },
                open: function() {
//設定指示區寬度
                    self.KendoObject.list.width(250);
                },
                change: function() {
var symbol = this.value();
if (!symbol) 
                        self.Name = "";
else
//股票代號連動名稱
                        self.Name = book[symbol] ? book[symbol].c : "";
                },
                dataSource: {
                    serverFiltering: true,
                    transport: {
                        read: function (e) {
//密技:dataSource.transport可用JavaScript取代Server呼叫
var keywd = e.data.filter.filters[0].value;
//輸入數字時比對s、否則比對c或n
var isNum = !isNaN(keywd);
//設定比對筆數上限
var count = 0, maxCount = 10;
 
//共用比對函式
function check(item, prop) {
if (item[prop].indexOf(keywd) > -1) {
                                    count++;
                                    item.match = item[prop].replace(keywd,
"<span class='hi'>" + keywd + "</span>");
returntrue;
                                }
returnfalse;
                            }
 
var res = isNum ?
                              $.grep(rawData, function (item) {
if (count < maxCount && check(item, "s"))
returntrue;
else
returnfalse;
                              }) :
                              $.grep(rawData, function (item) {
if (count < maxCount && 
                                      check(item, "c") || check(item, "e"))
returntrue;
else
returnfalse;
                              });
//將比對結果傳回
                            e.success(res);
                        }
                    }
                }
            };
        }
        angular.module("app", ["kendo.directives"])
        .controller("ctrl", myCtrl);
</script>
</body>
</html>

補充:Kendo UI有兩個版本,Core版及Profession版。Core版免費並採Apache v2 Open Source授權;Profession版多了Grid、Charts、Editor、TreeView、Upload、Charts等元件,為付費商業元件。AutoComplete包含在Core版。

[NG系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs
Viewing all 2433 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>