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

KO範例26 - ko.computed()的效能考量

$
0
0

ko.computed()能追蹤所依賴的ko.observable()或ko.observableArray(),在其變化時自動重算,開發時依直覺寫出關連邏輯,屬性間便會依預期變化。使用起來固然方便,但是當依賴對象連續變化時,要留意反覆重算的必要性以及對效能的衝擊。用一個範例來說明:

<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js">
</script>
<meta charset=utf-8 />
<title>KO範例26 - 利用throttle改善computed效率(改善前)</title>
</head>
<body>
<div>
    Max: <span data-bind="text: max"></span>
    Min: <span data-bind="text: min"></span>
    Sum: <span data-bind="text: sum"></span>
    Avg: <span data-bind="text: avg"></span>
</div>
<script>
    function myViewModel() {
      var self = this;
      self.items = ko.observableArray();
      self.max = ko.computed(function() {
        var ary = self.items();
        var max = null;
for (var i = 0; i < ary.length; i++) {
if (max == null || ary[i] > max) max = ary[i];
        }
return max;
      });
      self.min = ko.computed(function() {
        var ary = self.items();
        var min = null;
for (var i = 0; i < ary.length; i++) {
if (min == null || ary[i] < min) min = ary[i];
        }
return min;
      });
      self.sum = ko.computed(function() {
        var ary = self.items();
        var sum = 0;
for (var i = 0; i < ary.length; i++) {
          sum += ary[i];
        }
return sum
      });
      self.avg = ko.computed(function() {
        var ary = self.items();
if (ary.length == 0) return 0;
return parseFloat(self.sum()) / ary.length;
      });
    }
    var vm = new myViewModel();
    ko.applyBindings(vm);
for (var i = 1; i < 10000; i++) {
      vm.items.push(i);
    }
</script>
</body>
</html>

在以上範例中,ViewMode用了一個observableArray存放數字陣列,另外有四個屬性: max, min, sum, avg分別用以統計該數字陣列的最大值、最小值、總和及平均值,最直覺的寫法是四個屬性用ko.computed各寫各的,跑迴圈取出陣列的每一個數字處理。但可以預期,只要陣列每次加入新元素,就會觸發max, min, sum各跑一次迴圈(avg直接由sum值除以陣列長度計算,不必跑迴圈)。當我們連續在陣列加入從1到9999,共9999個數字,猜猜ko.computed要執行幾次? 理論上會觸發9999次,再上三組computed跑迴圈,第一次跑1圈,第2次2圈,第3次3圈,...,第9999次跑9999圈,CPU耗用量不少,使用IE10實測,耗時3.814秒。

先撇開Knockout特性不談,程式邏輯面存在一些無效率,先對三組computed跑迴圈的部分開刀,其實只需要跑一次迴圈,就可以一次算出max, min, sum及avg,故可以將max、min、avg都改為純ko.observable(),只留下sum為ko.computed(),加總同時一併求出最大值、最小值及平均,再分別寫入max、min及avg。

function myViewModel() {
var self = this;
      self.items = ko.observableArray();
      self.max = ko.observable();
      self.min = ko.observable();
      self.avg = ko.observable();
      self.sum = ko.computed(function() {
var ary = self.items();
var sum = 0;
var max = null;
var min = null;
for (var i = 0; i < ary.length; i++) {
if (min == null || ary[i] < min) min = ary[i];
if (max == null || ary[i] > max) max = ary[i];
          sum += ary[i];
        }
        self.max(max);
        self.min(min);
        self.avg(ary.length == 0 ? 0 : parseFloat(self.sum()) / ary.length);
return sum;
      });
    }
var vm = new myViewModel();
    ko.applyBindings(vm);
for (var i = 1; i < 10000; i++) {
      vm.items.push(i);
    }

改良後,發現原本會執行39,892次的evaluatePossibleAsync減少到9,955次,而耗時也由3.8秒降低到2.351秒。

還有改善空間嗎? 當然有! 我們關心的是最大值、最小值、加總、平均的最後計算結果,在將1-9999塞入陣列期間的變化過程一點也不重要,因此並不需要每塞一個數字就重新計算一次,等資料全部就緒再計算就好。先前在AJAX範例介紹過的throttle擴充方法是解決這個問題的好方法,只需在computed後方套上throttle,則observableArray的元素有變化時,不會立刻重算,會等待一小段時間,確認資料不再變化後才進行重算,如此便可抑制連續新增元素期間多餘的重算動作,有效改善效能。

function myViewModel() {
var self = this;
      self.items = ko.observableArray();
      self.max = ko.observable();
      self.min = ko.observable();
      self.avg = ko.observable();
      self.sum = ko.computed(function() {
var ary = self.items();
var sum = 0;
var max = null;
var min = null;
for (var i = 0; i < ary.length; i++) {
if (min == null || ary[i] < min) min = ary[i];
if (max == null || ary[i] > max) max = ary[i];
          sum += ary[i];
        }
        self.max(max);
        self.min(min);
        self.avg(ary.length == 0 ? 0 : parseFloat(self.sum()) / ary.length);
return sum;
      }).extend({ throttle: 200 });
    }
var vm = new myViewModel();
    ko.applyBindings(vm);
for (var i = 1; i < 10000; i++) {
      vm.items.push(i);
    }

最終改良版果然沒讓我們失望,耗時由2.351秒一舉縮短到136ms,而樹狀節點中的setTimeout、clearTimeout,便是throttle透過延遲執行改善效能的痕跡。

【結論】在設計ko.computed()時,記得評估其被呼叫次數與時機,避免短時間反覆大量執行,必要時可使用throttle擴充方法改善,才不會寫出吃光CPU的怪獸網頁。

[KO系列]

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

【CSS新手筆記】小探position: absolute應用

$
0
0

寫了好幾年網頁,卻一直是CSS新手,最近一年才比較認真研究個中奧妙,也慢慢解開一些原本搞不定的小眉角,說穿了不複雜,但相信我肯定不是地球最後一個發現的網頁攻城獅,故筆記分享,如高手發現有誤或另有妙法,請不吝指正。

我對於position: absolute的認識是很粗淺的,印象還停留在ASP.NET 1.1時代WebForm的Grid Layout Mode,啟用後網頁的控制項會變成像Window Form一樣採絕對座標,可拖拉擺放到任意位置。當時知道背後是透過position: absolute再加上top、left等CSS設定完成的,近十年下來一直以為所謂的position: absolute就是"以整個網頁為座標基準擺放元素"。

直到最近,我才學習到: position: absolute的定址基準,不一定是整個網頁,也可以相對於父元素;另外,定址依據並非只有top及left,還有right及bottom,如以下的例子:

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>position absolute的應用</title>
<style>
  #out,#in { border: 2px solid gray; }
  #out {
      margin: 100px;
      background-color: #ddd;
      width: 200px;
      height: 200px;
      position: relative;
  }
  #in {
      background-color: red;
      width: 50px;
      height: 50px;
      position: absolute;
      right: 10px; bottom: 10px;
  }    
</style>
</head>
<body>
<divid="out">
<divid="in"></div>
</div>
</body>
</html>

內部的紅色<div>被設為position: absolute、right: 10px、bottom: 10px,便可將其定位在灰色容器<div>右下角。線上展示

但其中有點要注意,灰方塊必須設定成position: relative,紅方塊才會以它計算座標,要是拿掉relative設定,紅方塊會變成貼在整張網頁的右下角。線上展示

position: absolute的文件,說明了此一行為的依據:

Absolute positioning

Elements that are positioned relatively are still considered to be in the normal flow of elements in the document. In contrast, an element that is positioned absolutely is taken out of the flow and thus takes up no space when placing other elements. The absolutely positioned element is positioned relative to nearest positioned ancestor. If a positioned ancestor doesn't exist, the initial container is used.

關鍵在其容器或其再上層的容器元素是否為"positioned ancestor",如果不是,便會以"initial container"(<body>,也就是整張網頁)為準。而positioned元素的定義為: an element whose computed position property is relative, absolute, or fixed. 這就是灰方塊設定relative才能成為紅方塊定位基準的理由。

而以上特性,還可做出單靠CSS實現隨容器尺寸自動放大縮小的效果! 紅方格不要指定width、height,透過position: absolute並指定top/left/right/bottom,就能產生類似Window Form Docking+Margin的結果: 線上展示

雖然自己都覺得Lag嚴重,但現在學會總比還不知道好,哈!

KO範例27 - ViewModel物件的擴充

$
0
0

為了確保Server端及Client端的ViewModel一致,在專案中我使用T4自動產生對應C# Class及JavaScript function,如此可確保ASP.NET端拋出的ViewModel與Client端的ViewModel完全一致,雖然這點Knockout Mapping Plug-In也辦得到,但自己產生的JavaScript ViewModel還可以加上JavaScript Documentation註解,配合Visual Studio強大的JS Intellisense功能,爽度是無法相比滴~

但有個小問題: 除了自動產生的屬性外,常會因開發需要還得額外加入UI控制用途的ko.observable或ko.computed,這部分在開發過程常會機動修改調整,不適合綁進自動產生程序,事後外加是較好抉擇。一開始我想得天真,以為用vm.prototype.anotherProp就可輕鬆搞定,後來卻發現這招處理ko.observable可行,遇到ko.computed時會因無法存取當下Instance而陷入困境。在ko.computed函數中,我們無法透過this取得當時物件,而宣告prototype時物件個體尚不存在,故也不可能當成ko.computed的第二個參數傳入,結果空有ko.computed卻無法依賴其他屬性進行運算。

<!DOCTYPEhtml>
<html>
<head>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js">
</script>
<meta charset=utf-8 />
<title>KO範例27 - 擴充ViewModel(失敗)</title>
</head>
<body>
<input data-bind="value: foo" />
<div>fooPlusX = <span data-bind="text: fooPlusX"></span></div>
<script>
//透過CodeGen自動產生的ViewModel
function VMBoo() {
var self = this;
      self.foo = ko.observable(1);
    }
//事後想為ViewModel多加一個fooPlusX屬性,
//天真地想透過prototype加掛ko.computed直接搞定
    VMBoo.prototype.fooPlusX = ko.computed(function() {
try {
//問題來了,在ko.computed中無從存取當時的instance
returnthis.foo() + "X";
      }
catch (err) {
return err.message;
      }
    }); //instnace在此時當不存在,也無法當成參數傳給ko.computed
var vm = new VMBoo();
    ko.applyBindings(vm);
</script>
</body>
</html>

在以上的失敗範例中,只會得到fooPlusX = Object [object global] has no method 'foo'的結果。線上展示

最後我找到一個解法,先在ViewModel的建構函式中埋下伏筆:
if (this.init) this.init(self);

而這個init函式可以透過vm.prototype.init = function(self) { … }加以定義,如此在init便能取得self(當時的物件個體)為所欲為,跟在建構式的寫法一致。線上展示

<!DOCTYPEhtml>
<html>
<head>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js">
</script>
<meta charset=utf-8 />
<title>KO範例27 - 擴充ViewModel(成功)</title>
</head>
<body>
<input data-bind="value: foo" />
<div>fooPlusX = <span data-bind="text: fooPlusX"></span></div>
<script>
//透過CodeGen自動產生的ViewModel
function VMBoo() {
var self = this;
      self.foo = ko.observable(1);
//CodeGen時額外呼叫透過prototype定義的init
if (this.init) this.init(self);
    }
//宣告額外的init函式
    VMBoo.prototype.init = function(self) {
      self.fooPlusX = ko.computed(function() {
return self.foo() + "X";
      });
    };
var vm = new VMBoo();
    ko.applyBindings(vm);
</script>
</body>
</html>

就醬,又排除掉偉大航道上的一個小障礙囉~

[KO系列]

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

【CSS新手筆記】左欄固定,右欄佔滿剩餘空間

$
0
0

寫了好幾年網頁,卻一直是CSS新手,最近一年才比較認真研究個中奧妙,也慢慢解開一些原本搞不定的小眉角,說穿了不複雜,但相信我肯定不是地球最後一個發現的網頁攻城獅,故筆記分享,如高手發現有誤或另有妙法,請不吝指正。

有個常遇到的需求,我卻從沒弄懂過: 分成多個直欄,左方直欄寬度固定,最右欄佔滿剩餘寬度。直到後來學會了利用float排列,才慢慢找到解法:

以上解法是將左側的兩個<div>設成float: left,使其向左貼齊排列,而最後一個<div>要使用overflow: hidden的密技,讓它變成一個BFC(Block Formatting Context),BFC會自成一個區塊,不允許其中的浮動元素跨出去,也不准相臨的浮動元素跨進來,於是最後一個div變成一般區塊,吃下前兩個float div以外的空間。(延伸閱讀: Expand div to take remaining widthThe magic of “overflow: hidden” )

完整程式碼如下:

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>佔滿剩餘寬度(float法)</title>
<style>
    html,body { height: 100%; }
    .container 
    {
      height: 80%;
      width: 80%;
      padding: 0px;
    }
    .container>div
    { 
      background-color: white; 
      border: 1px solid purple;
      padding: 6px;
      height: 100%;
    }
    .col-left { 
      float: left; 
      width: 100px; 
    }
    .col-remaining { 
      /* 利用overflow: hidden使其成為BFC */
      overflow: hidden;
    }
</style>
</head>
<body>
<divclass='container'>
<divclass='col-left'>
        LEFT 100px Wide
</div>
<divclass='col-left'>
        LEFT 100px Wide
</div>
<divclass='col-remaining'>
        REMAINING
</div>
</div>
</body>
</html>

除了利用float,還有第二種做法,是利用position: absolute,後兩個div算好前面元素佔用的空間,調整left值使其接在後面顯示,只是這種做法需要預先掌握左側元素的寬度(記得要加上margin、padding),運用起來不若float法便利,但也是種解決方案。完整範例如下: 線上展示

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>佔滿剩餘寬度(absolute法)</title>
<style>
    html,body { height: 100%; }
    .container 
    {
      position: absolute;
      height: 80%;
      width: 80%;
      padding: 0px;
    }
    .container>div
    { 
      /* 絕對定位,指定top/bottom為0可使其佔滿容器高度 */
      position: absolute;
      top: 0px; bottom: 0px;
      background-color: white; 
      border: 1px solid purple;
      padding: 6px;
    }
    .col-one,.col-two { 
      width: 100px; 
    }
    .col-two { 
      left: 112px; /* 寬度100還需加上左右padding 6*2 */
    }
    .col-remaining { 
      left: 224px;
      /* 指定right使其向右擴展到容器右側 */
      right: 0px;
    }
</style>
</head>
<body>
<divclass='container'>
<divclass='col-one'>
        LEFT 100px Wide
</div>
<divclass='col-two'>
        LEFT 100px Wide
</div>
<divclass='col-remaining'>
        REMAINING
</div>
</div>
</body>
</html>

CODE-為Self-Hosting ASP.NET Web API加上Log功能

$
0
0

自從學會Self-Hosting ASP.NET Web API,遇到Console Application/Window Form/WPF程式需提供API介面的場合,我都二話不說召喚它出來打怪,開開心心地在EXE裡寫Web,好不快意~

最近有個情境,需要為Web API保留存取Log記錄,查了一下,HttpSelfHostServer似乎沒提供內建的Logging機制。沒想太多,決定利用MessageHandler配合NLog自己寫一個。

LogHandler只需繼承DelegatingHandler實作SendAsync(),攔截Request及Reponse輸出到NLog,程式碼不多:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.ServiceModel.Channels;
using System.Text;
using NLog;
 
namespace MySelfHostWebApi
{
publicclass LogHandler : DelegatingHandler
    {
privatestatic Logger logger = NLog.LogManager.GetCurrentClassLogger();
 
//REF: http://blog.kkbruce.net/2012/05/aspnet-web-api-8-http-http-message.html
protectedoverride System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, 
            System.Threading.CancellationToken cancellationToken)
        {
//取得Method、IP、Url等等,稍後寫入NLog
string method = request.Method.ToString();
//REF: http://bit.ly/16lpGKM
//在Self-Hosted模式,透過RemoteEndpointMessageProperty取得IP
string ip = "Unknown";
if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
                ip = ((RemoteEndpointMessageProperty)
                    request.Properties[RemoteEndpointMessageProperty.Name]).Address;
string url = request.RequestUri.ToString();
returnbase.SendAsync(request, cancellationToken)
                .ContinueWith((task) =>
                {
//Response回傳時,連同StatusCode一起寫入NLog
                    HttpResponseMessage resp = task.Result as HttpResponseMessage;
                    logger.Log(LogLevel.Trace, string.Format("{0} {1} {2} {3}", 
                        ip, method, url, (int)resp.StatusCode));
return resp;
                });
        }
    }
}

使用方法很簡單,將LogHandler加進HttpSelfHostConfiguration.MessageHandlers,再設妥NLog.config,就大功告成囉!

//設定路由
            config.Routes.MapHttpRoute("API", 
"{id}",
new { 
                     controller = "download",
                     action = "index"
                 });
//加上LogHandler,記錄存取Log
            config.MessageHandlers.Add(new LogHandler());
 
            httpServer = new HttpSelfHostServer(config);

記錄的Log檔範例如下:

2013-08-01 05:14:46.4144 192.168.1.5 GET httq://192.168.1.1:4567/test.jpg 404
2013-08-01 05:18:15.9233 192.168.1.18 GET httq://192.168.1.1:4567/66fd.png 200

【延伸閱讀】

使用自訂確認對話框取代window.confirm

$
0
0

專案規格有一條機車要求: 對於刪除或覆寫資料前的確認程序,希望以自訂風格的確認對話框取代簡陋的window.confirm()。

舉例來說,按鈕後原本要透過window.confirm()請使用者確認後再執行,現在要改用自訂HTML元素呈現確認文字、按鈕進行確認,就如以下改用Kendo UI Window實作確認對話框的效果:

用HTML打造自訂對話框並在適當時機顯示是小事一椿,較有挑戰性的部分是原本window.confirm()執行為同步式,程式碼會停住等使用者回應再繼續往下走。想依confirm()結果決定不同動作只需寫成:

if (window.confirm("確定嗎?")) {
    //…使用者回答【是】時的動作
} else {
    //…使用者回答【否】時的動作
}

但要在JavaScript做到"卡住流程直到特定條件再繼續"並非易事,曾看過一種做法是跑while無窮迴圈並於特定時機跳出,但因JavaScript不像C#有Thread.Sleep可用,無窮迴圈會莫名吃光CPU很不環保。也有人想出藉由同步式XHR到Server端存取虛設延遲網頁模擬Thread.Sleep的招術,但靠著無謂網路傳輸來節省CPU,還徒增Server負擔,想來也不怎麼高明。最後,還是決定用jQuery的Deferred來處理非同步。

原理是先宣告Deferred物件,當呼叫確認對話框時,傳回Deferred.promise()給呼叫端,呼叫端可透過.done()指定使用者按【是】時要執行的動作、在.fail()指定使用者按【否】時要執行的動作。如此,再依使用者按鈕決定呼叫Deferred.resolve()或Deferred.reject(),就能控制該觸發done()還是fail(),達到依操作結果決定不同執行動作的效果。先不管華麗的UI元素,以下是示範用Deferred依按鈕結果決定動作的簡單範例: 線上展示

<!DOCTYPEhtml>
<html>
<head>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<meta charset=utf-8 />
<title>使用Deferred建立自訂確認對話框</title>
<script>
function myConfirm(msg) {
var df = $.Deferred(); //建立Deferred物件
var $div = $("<div id='C'></div>");
//由樣版複製建立一次性div元素
      $div.html($(".dialog").html())
//加上按鈕事件
      .on("click", "input", function() {
        $div.remove(); //將對話框移除
if (this.value == "Yes") 
          df.resolve(); //使用者按下Yes
else
          df.reject(); //使用者按下No
      })
      .find(".m").text(msg); //設定顯示訊息
//將div加入網頁
      $div.appendTo("body");
return df.promise();
    }
    $(function() {
      $("#btnTest").click(function() {
        myConfirm("Are you sure?")
        .done(function() { //按下Yes時
          alert("You are sure");
        })
        .fail(function() { //按下No時
          alert("You are not sure");
        });
      });
    });
</script>
</head>
<body>
<inputtype='button'value='Test'id='btnTest'/>
<divclass='dialog'style='display:none'>
<divstyle='border: 1px solid blue; padding: 12px;'>
<spanclass='m'></span>
<inputtype='button'value='Yes'/>
<inputtype='button'value='No'/>
</div>
</div>
</body>
</html>

其操作結果如下:

最後,運用同樣原理再招喚Kendo UI的Window套件上場,就能實現一開始展示的華麗版確認對話框囉~ 完整程式碼如下: 線上展示

<!DOCTYPEhtml>
<html>
<head>
<title>使用Deferred建立自訂確認對話框(Kendo UI版)</title>
<linkhref="http://cdn.kendostatic.com/2013.2.716/styles/kendo.common.min.css"
rel="stylesheet"type="text/css"/>
<linkhref="http://cdn.kendostatic.com/2013.2.716/styles/kendo.default.min.css"
rel="stylesheet"type="text/css"/>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://cdn.kendostatic.com/2013.2.716/js/kendo.web.min.js"></script>
<meta charset=utf-8 />
<style>
    .cnfrm-msg { color: red; padding: 12px; font-size: 12pt; }
    .cnfrm-yes,.cnfrm-no { }
</style>
<script>
//參考: http://jsfiddle.net/gyoshev/HRcKK/
    (function($) {
var h = [];
      h.push("<div class='cnfrm-block'>");
      h.push("<div class='cnfrm-msg'></div>");
      h.push("<input type='button' class='cnfrm-yes' />");
      h.push("<input type='button' class='cnfrm-no' />");
      h.push("</div>");
var html = h.join("");
      $.kendoConfirm = function(title, msg, yesText, noText) {
var $div = $(html);
        $div.find(".cnfrm-msg").text(msg);
        $div.find(".cnfrm-yes").val(yesText || "Yes");
        $div.find(".cnfrm-no").val(noText || "No");
var win = $div.kendoWindow({
          title: title || "Confirmation",
          resizable: false,
          modal: true,
          deactivate: function() {
this.destroy(); //remove itself after close
          }
        }).data("kendoWindow");
        win.center().open();
var dfd = $.Deferred();
        $div.find(":button").click(function() {
          win.close();
if (this.className == "cnfrm-yes")
            dfd.resolve();
else
            dfd.reject();          
        });
return dfd.promise();
      };
    })(jQuery);
    $(function() {
      $("#btnTest").click(function() {
var dfd = 
        $.kendoConfirm(
"Please confirm...",
"Are you sure to delete it?",
"Yeeees", "No no no");
        dfd.done(function() { //按下Yes時
          alert("You are sure");
        })
        .fail(function() { //按下No時
          alert("You are not sure");
        });
      });
    });
</script>
</head>
<body>
<inputtype='button'value='Test'id='btnTest'/>
<divclass='dialog'style='display:none'>
<divstyle='border: 1px solid blue; padding: 12px;'>
<spanclass='m'></span>
<inputtype='button'value='Yes'/>
<inputtype='button'value='No'/>
</div>
</div>
</body>
</html>

Kendo UI AutoComplete一次搜尋多欄

$
0
0

專案用到Kendo UI的自動完成,提示資料的物件陣列存在Client端,資料有多個欄位,希望做到任一個欄位包含關鍵字就顯示,AutoComplete有個filter參數,設定為"contains"就可以做到"包含"就算數,但搜尋對象參數dataTextField卻只能指定單一欄位,無法一次查詢多欄。

有個解決辦法是自行在物件上將多個欄位另外併成一欄,例如以下的codePage物件,textForSearch屬性等於代碼加空白加名稱:

var $code = $("#code");
function codePage(code, name) {
this.codePage= code;
this.name = name;
this.textForSearch = code + " " + name;
      }
var data = [
new codePage("869", "Greek"),
new codePage("932", "Japanese"),
new codePage("936", "Simplified Chinese"),
new codePage("949", "Korea"),
new codePage("950", "Traditional Chinese")
      ];
      $code.kendoAutoComplete({
        dataTextField: "textForSearch",
        dataValueField: "codePage", 
        dataSource: data,
        filter: "contains",
        select: function (e) {
//get index of <LI>
var idx = $.inArray(e.item[0], e.sender.items());
var data = e.sender.dataItem(idx);
//set name
            $("#name").text(data.name);
        }
      });

如此,將dataTextField設為textForSearch就能同時搜尋CodePage代碼及名稱:

但有個問題,雖然欄位輸入CodePage代碼或名稱都能搜尋,但我們希望自動完成欄位固定只輸入代碼,名稱則由另一個<SPAN>顯示。當AutoComplete的dataTextField設為textForSearch,點選提示項目後欄位填入的也會是textForSearch的值,會同時有代碼及名稱,與期望不符。

要解決這個問題,有個不錯的方法是透過dataValueField參數指定要填入<INPUT>的屬性名稱。要增加dataValueField功能得修改Kendo UI原始碼,不過修改3rd Party程式庫會增加版控複雜性,追完原始碼,我找到事後置換kendoAutoComplete物件的_select函式的玩法,可不修改原始碼JS就加入新功能,於是用以下的"Patch"函式,傳入data("kendoAutoComplete")取得的物件,將.select換成修改版(其實只改了一行),完成Hacking:

var SELECTED = "k-state-selected", List = kendo.ui.List;
function patchKendoAutoComp(autoCompObject) {
//add optionValueField support to 2013.2.716 version 
        autoCompObject._select = function (li) {
var that = this,
                separator = that.options.separator,
                data = that._data(),
                text,
                idx;
 
            li = $(li);
 
if (li[0] && !li.hasClass(SELECTED)) {
                idx = List.inArray(li[0], that.ul[0]);
 
if (idx > -1) {
                    data = data[idx];
//if dataValueField provided, use _value
                    text = that.options.dataValueField ? that._value(data) : 
                           that._text(data);
 
if (separator) {
                        text = replaceWordAtCaret(caretPosition(that.element[0]), that._accessor(), text, separator);
                    }
 
                    that._accessor(text);
                    that.current(li.addClass(SELECTED));
                }
            }
        }
    }

為AutoComplete增加dataValueField參數有助提高應用彈性,在官方討論區PO了提議,希望未來有機會加入規格,就不需要這個額外Hacking囉!

線上展示

解決Kendo UI NumericTextBox注音輸入法數字輸入問題

$
0
0

自己用是倉頡輸入法,Kendo UI數字欄位(NumericTextBox)一直用得很開心,直到同事回報: NumericTextBox配上IE瀏覽器,使用注音輸入法時無法用鍵盤數字區(鍵盤右方的數字盤)輸入數字,使用Chrome或Firefox則無此問題。[Bug示範 (用IE+注音輸入法可重現問題)]

追進原始碼,判斷問題出在NumericTextBox的keydown事件,使用注音輸入法時,數字盤的按鍵代碼在IE與其他瀏覽器可能不同導致差異。原本已經捲好袖子要開始Hacking,福至心靈想到另一個巧妙解法: -ms-ime-mode!

既然是IE獨有的問題,用IE獨有的功能解決剛剛好! 遇到數字欄位時強迫把中文輸入法關掉,就不用煩惱注音輸入法的按鍵代碼跟別人有什麼不同。在CSS中加入一條新規則:

/* kendoNumericTextBox 注音數字輸入問題修正 */
.k-numeric-wrap > input
{
/* 在IE下啟用注音輸入法無法用數字盤輸入,故停用IME */
    -ms-ime-mode: disabled;
    ime-mode: disabled;
}

靠著一條CSS規則,談笑間茶包灰飛煙滅,不亦快哉! 線上展示


【CSS新手筆記】Box-Sizing

$
0
0

為了解決padding影響寬度造成破版的問題,新學會一個CSS屬性: Box-Sizing

Box-Sizing只決定一事件: 矩型元素在計算寬度及高度時,border及padding為內含還是外加? (參考: Box Model) 而其設定值有三種: content-box、border-box及padding-box。預設值為content-box,意指元素實際寬高等於CSS指定寬高再加上border/padding(外加)、border-box時則border/padding為內含,元素的實際寬高即為CSS所指定寬高,而扣除border/padding後才是呈顯內容元素的範圍。支援padding-box的瀏覽器還不多,暫且忽略。

舉例來說: 一個width 200px的<DIV>,若padding為10px、border也是10px,預設box-sizing: content-box時,<DIV>實際寬度將為220px(200+10+10);若box-sizing改為border-box,則<DIV>實際寬度為200px,其內容物範圍寬度為180px(200-10-10)。

來個實例進行驗證:

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>CSS3 Box-Sizing 示範</title>
<style>
    .width200 {
      border: 10px solid #444;
      margin: 20px;
      padding: 10px;
      width: 200px;
      height: 100px;
      background-color: yellow;
    }
    .width200 > div {
      height: 100%;
      background-color: gray;
    }
</style>
</head>
<body>
<divclass="width200"
style="box-sizing: border-box; margin-left: 40px">
<div></div>
</div>
<divclass="width200">
<div></div>
</div>
</body>
</html>

圖中的兩個<DIV>都被設為width: 200px; height: 100px; padding: 10px; border: 10px;,差別在於上方box-sizing設成border-box(另外加了margin-left: 20px以便與下方左右標齊),下方則不指定box-sizing,維持預設值(content-box)。

由結果可看出,設為border-box時,<DIV>尺寸為200x100,而內部淺灰區域被壓縮到只剩180x80;而content-box時,<DIV>尺寸為220x120,內部淺灰內容區域維持200x100,面積與上方<DIV>相同,由此例可看出border/padding外加與內含的差異,也說明box-sizing的可應用時機。

網頁元素異動在IE9出現延遲顯示現象

$
0
0

在IE9遇到一個問題: 透過Knockout text繫結變更SPAN內容,IE9無法立即顯示更新結果,但開啟IE Dev Tools要開始偵查時才回神連忙顯示。切到IE8/7相容模式時不會發生,在其他版本IE(IE10、IE8)或Chrome、Firefox也不曾遇過,推測是IE9渲染(Render)引擎的Bug。

KO範例7剛好可以用來重現問題。理論上勾選最前方的Checkbox,後方的"完成!"字樣應立刻顯現、取消勾選應立即消失,但如以下示範,使用IE9測試時必須要將滑鼠移至後方文字處點擊一下,"完成!"文字才會顯示或消失。(猜想是點擊觸發了Repaint之類的事件,強迫IE重新產生內容)

嘗試後,找到一個簡單的Workround – 將<SPAN>設為position: relative,顯示結果未變但IE渲染運算的邏輯有別,似乎就避開有Bug的程式碼,問題就消失囉~

CODE-封裝Office繁簡轉換服務

$
0
0

手邊的專案涉及多國語系,之前研究過使用Excel維護多國語系字串資源檔,意外發現Office的繁簡轉換功能威猛過人,不單只是置換字元編碼,還能做到詞彙轉換,將字彙轉換成對應的說法,例如: 交易資料->事务数据、預設記憶體->默认内存... 等等,放著神兵利器不用,豈不暴殄天物? 於是,延續先前開發Word套表服務的概念,裝著Word當引擎的裝甲車登場囉~

程式碼的重點在於Word Document物件的共用與資源確實回收(操作Office Interop的注意事項先前也討論過)。至於轉換核心,一行搞定 -- TCSCConverter

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Office.Interop.Word;
 
publicclass ChineseStrConverter : IDisposable
{
private Application wordApp = null;
private Document doc = null;
 
public ChineseStrConverter()
    {
        wordApp = new Application();
        wordApp.Visible = false;
        doc = wordApp.Documents.Add();
    }
 
publicstring ConvChineseString(string src, bool chs2cht = false)
    {
string result = null;
try
        {
            doc.Content.Text = src;
            doc.Content.TCSCConverter(
                chs2cht ? //由參數決定簡->繁或是繁->簡
                WdTCSCConverterDirection.wdTCSCConverterDirectionSCTC : 
                WdTCSCConverterDirection.wdTCSCConverterDirectionTCSC,
true, true);
//取回轉換結果時,結尾會多出\r
            result = doc.Content.Text.TrimEnd('\r');
        }
catch (Exception ex)
        {
            result = "Error: " + ex.Message;
        }
return result;
    }
 
 
publicvoid Dispose()
    {
//確實關閉Word Application
try
        {
//關閉Word檔
object dontSave = WdSaveOptions.wdDoNotSaveChanges;
            ((_Document)doc).Close(ref dontSave);
//確保Document COM+釋放
if (doc != null)
                Marshal.FinalReleaseComObject(doc);
            doc = null;
            ((_Application)wordApp).Quit(ref dontSave);
        }
finally
        {
            Marshal.FinalReleaseComObject(wordApp);
        }
    }
}

不過,詞彙轉換涉及一定複雜度,故得留意轉換效率。網路上有一篇很棒的文章: C# 繁簡轉換效能大車拚,比較了四種不同繁簡轉換方式的速度,其中Word是最慢的,但也是唯一支援詞彙轉換的,所以沒啥好抉擇,了解其執行效率,應用時心裡有數就好。

寫個小程式測試,轉換功能正常,至於速度,在我的環境(CPU i7-2600 + Win2008R2 + Word 2010),對字串(約30個字元)進行200次轉換(繁轉簡、簡轉繁各100次)耗時3.6秒,故每次轉換約18ms;762個字元的繁體文章轉為簡體100次,耗時也差不多3.6秒,每次轉換36ms。此一效能表現用於批次或預先轉換,應是綽綽有餘了!

又到了呼口號時間,大家一起為本日MVP歡呼: Office 好威呀!

【茶包射手筆記】Word無法在Windows Service中執行

$
0
0

嘗試將先前提過的Word套表服務寫成Windows Service,原本使用Console Application + Self-Hosting ASP.NET Web API測試無誤,移進Windows Service卻壞了。試過將服務執行身分調為本機系統(LocalSystem)或AD網域帳號,錯誤依舊。使用Visual Studio追蹤進程式找到錯誤源頭:

doc = wordApp.Documents.Open(FileName: ref filePath, ReadOnly: ref readOnly);
doc.Activate();

filePath指向的範本docx確定存在,Documents.Open()也未出錯,但執行完doc == null,導致下一列要執行doc.Activate()時爆出NullReferenceException。

同一服務裝在Windows 2003運作順利,在我自己的Windows 2008 R2上也沒問題,問題只發生在另一台Windows 7 32bit開發機器上,一時鬼打牆,瞎抓好一陣子才想到是老問題: c:\windows\syswow64\config\systemprofile\desktop (開始曾一度懷疑,但不知怎麼錯記成它只發生在64bit,連口供都沒問就把人放走... orz)

在32位元Windows 7上開個C:\Windows\System32\config\systemprofile\Desktop資料夾,搞定收工。

補記一點: 掛成Windows Service的程式還能逐行偵錯嗎? Yes, You Can!

執行Windows Service後,用DEBUG/Attach to Process找到該程序掛上去,接下來設中斷點、逐步偵錯的做法就跟直接由VS啟動沒有兩樣囉~ 向地表上最強大的IDE – Visual Studio敬禮!!

程式師語錄 - 黑暗選集

$
0
0

語錄有兩種,有一種在於闡述智慧哲理,使人樂觀向上,積極進取;另一種則直搗人生無奈,看完會心一笑,撫慰心靈。方式不同,但都是能帶來正面能量的心靈雞湯。最近看到網友but翻譯自日本程式鄉民的開發感想彙整,顯然是後者,讓搞程式專案快20年的我,讀來心有戚戚焉,忍不住頭點如搗蒜。

摘要整理一些特別有fu的(部分有額外融入自已的觀點及體驗,作了改寫),預備於未來身處專案煉獄或遭程式花惹發纏身時咀嚼回味,追求心靈平靜,有消彌輕生念頭之效... (與讀金剛經有相同功效來著 XD)

  • 每天有24小時。所謂的「今天之內」是指明天早上9點之前。
  • 程式不會照你所想的方式跑,只會照你所寫的方式跑。
  • 多想10秒鐘,你可以不用說「嗯,這個做得到」。
  • 需求規格在程式寫完後才會敲定;基本規格要客戶看到成品後才會決定;詳細規格要使用者用過後才會確定。
  • 軟體設計的奧義: 讓軟體設計單純到很明顯不會出錯,不然就是複雜到有錯也看不出來。
  • 先說「沒辦法」的人贏,有意見的話你自己寫。
  • 要殺一個程式設計師不需要刀,改三次規格就好囉!
  • 詳細設計要寫在程式碼註解裡,註解常是唯一救贖,記得要讓自己看得懂。
  • 程式異常該稱為「Bug」還是「規格限制」是看Deadline還有多久決定。
  • 地獄期在一段時間後,充滿殺氣的怒吼會變多;
    再持續一段時間,說話變少牢騷會變多,開始壟罩在凝重氣氛裡。
    再持續下去反而會海闊天空,四周洋溢充滿活力的聲音。
    這種狀態稱為「Programmer’s High」,也是開始有人倒下的時候。
    (註: 馬拉松有所謂Runner's High,指跑者到後期反而精神舒暢的迴光返照期)
  • 老手用來提振精神的魔法格言:「比起以前來說這算不了什麼…」
    新人用來提起幹勁的魔法格言:「把這件工作做完的話…」(他們還不知道工作是沒有終點的。)
  • 能夠迅速想到解法的程式設計師太多了,他們用一分鐘想到方法,用一天去寫程式;而不是花一小時想解法,再用一小時去寫程式。
  • 改掉舊的Bug總是會導致新的Bug -- 這是所謂的「Bug不滅定律」。
  • 無論規格多晚確定,結案期限永遠不會變 -- 這是所謂的「期限守恆定理」。
  • 不懂電腦的操作者是找出Bug的天才,很遺憾,同一個Bug他們通常沒法表演第二次。
  • 規格與規格書是兩碼事。
  • 將沒發現任何Bug的系統送上線 -- 恐怖啊! 恐怖到了極點~
  • 當某人寫的程式碼出現Bug,當事人通常不在(墨菲定理?)
  • 系統交付日是為了延後而存在的。
  • 當你有「啊,加上這功能吧」的念頭時,還是不要想太多,早點去睡結果會比較好。
  • 熟悉程式語言不表示就會寫軟體;徹底熟悉程式語言後,軟體會寫得更慢~
  • 發現問題如何解決不是最重要的,發現哪裡是問題比較重要。
  • 還沒付錢的,不是客戶;已經付清的,也不是客戶。
  • 認真聽足客戶抱怨兩小時,問題就算解決一半,有時連程式都不用改就可結案。
  • 接受「口頭規格」,就像開一張空白支票給別人一樣。
  • 要證明 Bug 不是自己的責任,往往比修好它更花時間。
  • Bug是很害羞的,跟你獨處時明明很大方,有別人來看時就躲起來了。
  • 程式的養分來自程式設計師的鮮血。
  • 當程式碼的規模超過臨界點,就會脫離程式設計師的手擁有自己的意志。
  • SA總是很扯地跟程式師說: "怎麼會沒辦法?";
    業務總是沒辦法跟客戶說: "這個很扯"
  • 機器感受到龐大的時程壓力,所以它也崩潰了 orz
  • 沒被發現的 Bug,就不算Bug!

原始出處:
http://but.tw/2008/10/programmers_rule/
http://but.tw/2011/10/programming-rule-next/

使用PhantomJs產生網頁擷圖

$
0
0

將網頁內容另存圖檔是專案裡三不五時會冒出的需求,但一直沒找到順手好使的兵刃。

不久前介紹過HTML轉PDF的元件 -- Pechkin套件,網頁存成PDF已多少有保留擷圖的意義,但文末對本部落格的實測讓人失望,失真嚴重。最近的專案又被逼著設法將現成網頁(由JavaScript動態產生內容)轉存圖檔供其他系統應用,省去另外開發匯出模組的工程。再次Survey解決方案,想起先前流浪小風在Chutzpath介紹提過另一個webkit核心的網頁操作引擎 – PhantomJs

小試之後驚為天人,簡便易用卻威力無窮,PhantomJs可輕易實現以下功能:

  1. 不依賴任何瀏覽器,直接以命令列工具方式載入網頁,並開放以指令存取網頁DOM。這個基礎建立後,衍生的好玩應用就多了。
  2. 能操作DOM物件並檢查結果,要做到網頁自動測試不是夢。(有個術語叫Headless Website Testing,意指不需瀏覽器不顯示任何網頁UI就完成測試)
  3. 一行指令就能將網頁顯示結果另存成圖檔。
  4. 除了HTML5及CSS3,還支援Canvas及SVG。
  5. 監控DOM載入及AJAX傳輸過程,進行效能分析。
  6. 提供互動式操作模式(REPL),可逐次下指令、看結果,即興演出。
  7. 除了測試網頁,撰寫程式操作網頁還能用來幹些邪惡的勾當執行特殊任務,例如: 搶春節車票、在馬拉松報名秒殺戰裡突圍、發動網路灌票... 等等。理論上用瀏覽器外掛或寫程式模擬POST Request也能達成相同效果,用PhantomJs在效能及簡便性上具一定的優勢。

使用PhantomJs不需安裝,到網站下載ZIP檔,解壓縮後有個phantomjs.exe,靠它就可以開始變戲法。例如,將以下幾行程式存成lab.js,放在phantomjs.exe同目錄下,開啟命令列視窗,CD路徑到phantomjs.exe所在資料夾,執行phantomjs lab.js,就能抓取本部落格的擷圖:

var page = require('webpage').create();
page.open('http://blog.darkthread.net', function() {
    page.render('d:\\darkthread.png');
    phantom.exit();
});

產生的圖檔十分逼近網頁的實際檢視結果! (主要只差Flash部分無法顯示)

另外,我還寫了一個簡單的Canvas + SVG測試網頁,用PhantomJs也成功儲存顯示結果,令人感動到起雞皮疙瘩~

新武器入手,未來搞網站自動測試、綱頁擷圖、為非作歹突圍奇襲又多了神兵利器可用。啊~~ 福氣啦!

KO範例28 - 轉換對象(排除自己)之下拉選單連動

$
0
0

專案需求一枚,用於匯率換算。來源幣別及目標幣別以下拉選單方式顯示,想當然爾,目標幣別跟來源幣別相同還轉換個屁,因此規格提到: 目標幣別的下拉選項應包含所有幣別,但排除來源幣別當下的選取幣別,特殊的下拉選單連動需求應運而生。

要用KO實現此一連動並不困難: 為兩個<SELECT>各宣告一個observableArray作為options繫結對象,也各宣告一個observable作為value的繫結對象以對應下拉選單選取值;接著宣告一個computed函式建立來源幣別選取值與目標幣別observableArray的關聯,一旦來源幣別變動,複製來源幣別的observableArray,剔除當下來源幣別選取值後將陣列設定成目標幣別observableArray的內容,大功告成。

進一步分析,前述的computed只有單一訂閱對象及單一更新對象,可再簡化,將目標幣別options直接繫結到一個computed(訂閱來源幣別選取值,傳回剔除選取值後的幣別陣列),便可以computed取代observableArray + computed。

最後提一下jQuery.map這個好東西,在JavaScript中處理陣列,許多初學者直覺的做法會宣告新的空陣列,跑迴圈逐一處理陣列元素再放入新陣列。例如: 需求為1-10的數字陣列只留單數並+10,變成11, 13, 15..,通常會寫成:

var orig = [1,2,3,4,5,6,7,8,9,10]; 
  var res = []; 
  for (var i = 0; i < orig.length; i++) { 
    var v = orig[i]; 
    if (v % 2 == 0) continue; 
    res.push(v + 10); 
  }

而jQuery.map可簡化上述程式,變成:

var orig = [1,2,3,4,5,6,7,8,9,10]; 
var res = $.map(orig, function(v, i) { 
  if (v % 2 == 0) return null; //表示剔除 
  return v + 10; 
});

很精省吧?

完整程式碼如下: 線上展示

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>KO範例28 - 轉換對象(排除自己)之下拉選單連動</title>
</head>
<body>
<div>
    From: 
<selectdata-bind="options: ListA, value: PropA, optionsText: 't', optionsValue: 'v'"></select>
    (<spandata-bind="text: PropA"></span>)
&nbsp;&nbsp;&nbsp;To:
<selectdata-bind="options: ListB, value: PropB, optionsText: 't', optionsValue: 'v'"></select>
    (<spandata-bind="text: PropB"></span>)
</div>
<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.PropA = ko.observable();
      self.ListA = ko.observableArray([
        { v:"TWD", t:"台幣" }, 
        { v:"USD", t:"美金" },
        { v:"EUR", t:"歐元" },
        { v:"JPY", t:"日幣" }, 
        { v:"HKD", t:"港幣" }
      ]);
      self.PropB = ko.observable();
      self.ListB = ko.computed(function() {
var a = self.PropA();
return $.map(self.ListA(), function(item) {
if (item.v == a) returnnull;
return item;
        });
      });
    }
var vm = new myViewModel();
    ko.applyBindings(vm);
</script>
</body>
</html>

[KO系列]

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

【茶包射手日記】VS2012 Update 3安裝失敗

$
0
0

在一台新裝的Windows 7安裝VS2012,啟動後出現VS2012 Update 3更新通知,順勢執行Update 3安裝程式,遇到以下錯誤:

Setup Failed! Install cannot continue because some required components failed.
Setup Engine: The pipe is being closed.

錯誤訊息對話框還算貼心,可直接導引到Log檔,看到一堆錯誤如下:

Error 0x800700e8: Failed to write message type to pipe.
Error 0x800700e8: Failed to write send message to pipe.

由關鍵字0x800700e8很快找到MSDN討論區的文章,遇到類似問題的朋友不少,判斷是Web Installe的問題,微軟的論壇客服建議直接下載ISO檔安裝。下載ISO檔(大小約2.08GB)執行VS2012.3.exe即可順利安裝,搞定收工!

Json.NET技巧兩則: 忽略屬性及列舉轉字串

$
0
0

開發主戰場漸漸移到前端,應用Json.NET的深度也逐步增加。今天學會Json.NET技巧兩則,筆記如下:

情境1:

待序列化物件的部分屬性純粹供.NET端應用,不需傳到前端,希望能予以排除以縮短JSON字串,減少頻寬浪費。

解決方案

方法1 - 在屬性加上[JsonIgnore],負向表列哪些屬性不要序列化。
方法2 - 在類別加上[DataContract],為需序列化屬性加上[DataMember],正向表列哪些屬性需要序列化。
參考: Efficient JSON with Json.NET – Reducing Serialized JSON Size

情境2

預設列舉(Enum)屬性會被序列化為對應數值(int),但在許多應用場合,轉為列舉項目文字更有可讀性。

解決方案

為列舉屬性加上[JsonConverter(typeof(StringEnumConverter))]就搞定囉!

來個範例程式展示效果:

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication1
{
class Program
    {
publicenum Options
        {
            Option1, Option2
        }
 
publicclass Entity
        {
public Options A1 { get; set; }
public Options A2 { get; set; }
            [JsonConverter(typeof(StringEnumConverter))]
public Options B1 { get; set; }
            [JsonConverter(typeof(StringEnumConverter))]
public Options B2 { get; set; }
publicstring P1 { get; set; }
            [JsonIgnore]
publicstring P2 { get; set; }
        }
 
 
staticvoid Main(string[] args)
        {
            Entity ent = new Entity()
            {
                A1 = Options.Option1,
                A2 = Options.Option2,
                B1 = Options.Option1,
                B2 = Options.Option2,
                P1 = "P1",
                P2 = "P2"
            };
            Console.WriteLine(
                JsonConvert.SerializeObject(ent, Formatting.Indented));
            Console.Read();
        }
    }
}

執行結果如下,其中A1, A2兩個列舉屬性被轉成0與1(Json.NET的預設行為),B1, B2列舉屬性因加上[JsonConverter(typeof(StringEnumConverter))]宣告,被轉為"Option1"及"Options2";加上[JsonIgnore]的P2屬性,則被排除在JSON字串之外:

{
  "A1": 0,
  "A2": 1,
  "B1": "Option1",
  "B2": "Option2",
  "P1": "P1"
}

原本擔心要費點手腳的兩個需求,透過Attribute宣告就輕易搞定,不禁讚嘆Json.NET考慮周詳,很好很強大,實在該直接收入.NET Framework才對!!

【答客問】Json.NET-動態決定屬性是否序列化

$
0
0

昨天提到Json.NET屬性序列化設定,接獲讀者森哥留言:

請問黑大,
針對不需要序列化的「屬性」是否可以透過程式「動態」設定或是過濾?

有預感遲早也會遇到這個靠杯火盃的考驗,決定打鐵趁熱,馬上來練習。所幸,Json.NET真的很強大,早就料想到此一需求,提供ContractResolver以實現神乎奇技的高度動態化。

我寫了一個範例,展示兩種動態決定應序列化屬性的情境:

  • Serialize時傳入屬性名稱陣列作為參數,正向表列JSON應包含的屬性。
  • 由物件屬性值決定屬性是否要序列化,例如: 如果是女生就不包含年齡。(這幾乎已彈性到極點,雖然實務上不常用到)

程式的做法是宣告兩個繼承自DefaultContractResolver的類別: LimitPropsContractResolver在建構時傳入string[]參數列出要序列化的屬性名稱,並覆寫CreateProperties方法,過濾base.CreateProperties()傳回的IList<JsonProperty>,只保留前述string[]有列出的屬性;HideAgeContractResolver則覆寫CreateProperty()方法,由base.CreateProperty()取得JsonProperty,JsonProperty有個ShouldSerialize屬性可以傳入Lambda運算式,逐筆處理每個要序列化的物件,在Lambda運算式中可將物件轉型為原型別進行判斷,若不要序列化就傳回false。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
 
namespace ConsoleApplication1
{
class Program
    {
publicenum Gender 
        {
            Male, Female
        }
 
publicclass Person
        {
publicstring Name { get; set; }
            [JsonConverter(typeof(StringEnumConverter))]
public Gender Gender { get; set; }
publicint Age { get; set; }
public Person(string name, Gender gender, int age)
            {
                Name = name; Gender = gender; Age = age;
            }
        }
 
publicclass HideAgeContractResolver : DefaultContractResolver
        {
//REF: http://james.newtonking.com/projects/json/help/index.html?topic=html/ContractResolver.htm
protectedoverride JsonProperty CreateProperty(MemberInfo member, 
                MemberSerialization memberSerialization)
            {
                JsonProperty p = base.CreateProperty(member, memberSerialization);
if (p.PropertyName == "Age")
                {
//依性別決定是否要序列化
                    p.ShouldSerialize = instance =>
                    {
                        Person person = (Person)instance;
return person.Gender == Gender.Male;
                    };
                }
return p;
            }
        }
 
publicclass LimitPropsContractResolver : DefaultContractResolver
        {
string[] props = null;
public LimitPropsContractResolver(string[] props)
            {
//指定要序列化屬性的清單
this.props = props;
            }
//REF: http://james.newtonking.com/archive/2009/10/23/efficient-json-with-json-net-reducing-serialized-json-size.aspx
protectedoverride IList<JsonProperty> CreateProperties(Type type, 
                MemberSerialization memberSerialization)
            {
                IList<JsonProperty> list = 
base.CreateProperties(type, memberSerialization);
//只保留清單有列出的屬性
return list.Where(p => props.Contains(p.PropertyName)).ToList();
            }
 
        }
 
staticvoid Main(string[] args)
        {
            List<Person> list = new List<Person>();
            list.Add(new Person("George", Gender.Male, 18));
            list.Add(new Person("Mary", Gender.Female, 40));
//正常輸出
            Console.WriteLine(JsonConvert.SerializeObject(
                list, Formatting.Indented));
            var settings = new JsonSerializerSettings();
//加上ContractResolver,正向表列哪些屬性要序列化
            settings.ContractResolver = 
new LimitPropsContractResolver("Name,Age".Split(','));
            Console.WriteLine(JsonConvert.SerializeObject(
                list, Formatting.Indented, settings));
//加上ContractResolver,依物件的屬性值動態決定要不要序列化
            settings.ContractResolver = new HideAgeContractResolver();
            Console.WriteLine(JsonConvert.SerializeObject(
                list, Formatting.Indented, settings));
            Console.ReadLine();
 
        }
    }
}

程式執行結果如下,共有三段輸出,第一段為正常版;第二段套用LimitPropsContractResolver("Name,Age".Split(',')),故JSON中只見Name及Age,Gender被隱藏;第三段套用了HideAgeContractResolver(),如結果所示,Mary的JSON內容不包含年齡,George則包含。

[
  {
    "Name": "George",
    "Gender": "Male",
    "Age": 18
  },
  {
    "Name": "Mary",
    "Gender": "Female",
    "Age": 40
  }
]
[
  {
    "Name": "George",
    "Age": 18
  },
  {
    "Name": "Mary",
    "Age": 40
  }
]
[
  {
    "Name": "George",
    "Gender": "Male",
    "Age": 18
  },
  {
    "Name": "Mary",
    "Gender": "Female"
  }
]

演練完畢,內心激動澎湃,對Json.NET的景仰如淊淊江水,綿綿不絕~

如果奧斯卡有最佳元件獎,我提名它!

【笨問題】IE9的「console未被定義」錯誤

$
0
0

一直以來,有個鬼現象纏著我揮之不去,console.log常因不明原因在IE9出現SCRIPT5009: 'console' is undefined (console未被定義) 錯誤!

我當然知道IE從IE8+才支援console物件,但如上圖所示,網頁明明是IE9標準模式,為什麼IE9卻說console物件不存在? 但進行偵錯,console.log()卻又正常!

笨了好久,今天才解開謎團:

 IE8/IE9要先按F12開啟IE Dev Tools才能存取console物件啦! 笨蛋!

參考來源: http://msdn.microsoft.com/en-us/library/ie/gg589530(v=vs.85).aspx

… You use the console object to send a message to the console from your code. Using the console instead of "window.alert()" when testing code is less obtrusive and doesn't stop you with a modal dialog box. This object provides a number of forms so that you can distinguish between informational messages and error messages if you want. When you use the console object, make sure that the F12 tools are open. To avoid executing needless code, use the following feature test …

所以,如果使用環境包含IE8/9,請養成良好習慣,用if (window.console) { ... }包住console.log()動作,切忌把IE8/9想成Chrome/Firefox,以為永遠有window.console可用!

PS: 終於,IE10改邪歸正向Chrome/Firefox看齊,console不再像段譽的六脈神劍時有時無。但只要IE8/9還在一天,console檢查還是不能少。

讓IE直接顯示JSON結果

$
0
0

IE有個討人厭的行為。

當網頁程式以JSON格式傳回結果(JSON字串,且Content-Type設為application/json),在IE需下載另存檔案才能檢視,不像Chrome或Firefox可直接看結果。當需要反覆測試,"重新整理網頁->選取位置另存檔案->開檔看結果"的迴圈容易誘發焦慮、煩躁、爆怒等症狀,而一般患者多會啟動生物本能,默默關上IE改開Chrome尋求解脫...

例如,以下MVC Controller的GetJson()會以JSON格式傳回字串:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MyWeb.Controllers
{
publicclass HomeController : Controller
    {
public ActionResult Index()
        {
return View();
        }
 
public ActionResult GetJson()
        {
return Json("This is a test", JsonRequestBehavior.AllowGet);
        }
    }
}

使用IE測試,會彈出下載存檔提示:

手邊的專案需用IE測試,又不想為了測API切換不同瀏覽器,爬文求解,在stackoverflow找到很棒的解法。其原理是修改Registry,將application/json、text/json兩種Content-Type開啟設定調成與GIF/PNG/HTML一致,改為直接用瀏覽器檢視。

將以下內容存檔為ie-json-fix.reg,在Windows中點擊執行安裝即可完成Registry修改。

Windows Registry Editor Version 5.00
;
; Tell IE to open JSON documents in the browser.  
; 25336920-03F9-11cf-8FD0-00AA00686F13 is the CLSID for the "Browse in place" .
;  
 
[HKEY_CLASSES_ROOT\MIME\Database\Content Type\application/json]
"CLSID"="{25336920-03F9-11cf-8FD0-00AA00686F13}"
"Encoding"=hex:08,00,00,00
 
[HKEY_CLASSES_ROOT\MIME\Database\Content Type\text/json]
"CLSID"="{25336920-03F9-11cf-8FD0-00AA00686F13}"
"Encoding"=hex:08,00,00,00

修改後,IE就能會Chrome一樣直接顯示JSON內容囉! It Rocks!

Viewing all 2458 articles
Browse latest View live


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