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

NG筆記14-Checkbox清單

$
0
0

練習用NG實現KO範例10 - checked繫結

Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 10 - checked繫結</title>
<style>
    body, input
    {
        font-size: 9pt;
    }
    fieldset
    {
        margin-top: 10px;
        width: 200px;
        padding: 5px;
    }
    dt, dd
    {
        float: left;
        width: 80px;
        height: 20px;
    }
</style>
</head>
<bodyng-controller="defaultCtrl">
 
<div>
連絡方式選擇: 
<inputtype="radio"name="contactOption"value="phone"ng-model="model.contOption"/>
電話
<inputtype="radio"name="contactOption"value="email"ng-model="model.contOption"/>
        Email
</div>
<div>
        (與下拉選單連動: 
<selectng-model="model.contOption">
<optionvalue="phone">電話</option>
<optionvalue="email">Email</option>
</select>)
</div>
<div>
單一Checkbox:
<inputtype="checkbox"ng-model="model.singleCbx"/>
</div>
<div>
多Checkbox對應陣列: 
<inputtype="checkbox"check-list="model.cbxItems"value="PC"/>PC
<inputtype="checkbox"check-list="model.cbxItems"value="NB"/>NB
<inputtype="checkbox"check-list="model.cbxItems"value="Phone"/>Phone
</div>
<fieldset>
<legend>物件屬性檢視</legend>
<dl>
<dt>contOption屬性</dt>
<ddng-bind="model.contOption"></dd>
<dt>singleCbx屬性</dt>
<ddng-bind="model.singleCbx"></dd>
<dt>cbxItems屬性</dt>
<dd>
<div>
                    {{ model.cbxItems.join(', ') }}
</div>
</dd>
</dl>
</fieldset>
 
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
//REF: http://stackoverflow.com/a/14519881
    .directive('checkList', function() {
return {
        scope: {
          list: '=checkList',
          value: '@'
        },
        link: function(scope, elem, attrs) {
var handler = function(setup) {
varchecked = elem.prop('checked');
var index = scope.list.indexOf(scope.value);
if (checked&& index == -1) {
if (setup) elem.prop('checked', false);
else scope.list.push(scope.value);
            } elseif (!checked&& index != -1) {
if (setup) elem.prop('checked', true);
else scope.list.splice(index, 1);
            }
          };
var setupHandler = handler.bind(null, true);
var changeHandler = handler.bind(null, false);
          elem.on('change', function() {
            scope.$apply(changeHandler);
          });
          scope.$watch('list', setupHandler, true);
        }
      };
    })
    .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
//Radio對應到Value值
        self.contOption = "phone";
//單一Checkbox對應到true/false
        self.singleCbx = true;
//多個Checkbox對應到Value值組成的字串陣列
        self.cbxItems = ["PC", "NB"];
      }
      $scope.model = new myViewModel();
    });
</script>
</body>
</html>

在NG中,可用<input type="checkbox" ng-model="prop">繫結checked屬性到prop上,prop預設為Boolean,但可用ng-true-value/ng-false-value以指定值取代true/false[參考]。對於RadioButton,<input type="radio" value="theValue" ng-model="prop">會將prop繫結到已選取項目的Value值。

至於Checkbox多重選取,KO比NG方便,只要用data-bind="checked: 某個observableArray"就可以得到勾選項目Value所組成的字串陣列。NG未內建Chebkbox多重選取支援,同樣效果需靠自訂Directive。在此借用網友寫的範例,在sampleApp新增名為"checkList"的Directive,跟先前介紹的簡易Directive不同,checkList除了Link函式外,又多宣告了scope: { list: "=checkList", value="@" },為checkList建立Local Scope,其中包含兩個屬性list及value,其中list設為"=checkList",雙向繫結到上層Scope的checkList屬性(亦即HTML標籤check-list="…"所設定的屬性,本例為CheckList選取結果組成的字串陣列);而value="@"代表Local Scope的value來自HTML標籤value="..."所指定的值。(還有一個&符號用來指向可執行的表示式,例如: localFunc: "&action",HTML寫成func="doSomething(arg)",呼叫localFunc({ boo:"Foo" })等同執行doSomething({ boo:"Foo" })。(詳細說明請參考官方文件)

而Link函式以$watch()監測list集合,負責將集合異動反應至Checkbox;同時也透過jQuery change事件監控Checkbox狀態變化,將選取結果更新到list,達成勾選方格與list間的雙向繫結。

最後,將Checkbox寫成<input type="checkbox" check-list="model.cbxItems" value="PC" />,就能實現KO data-bind="checked: cbxItems"相同的效果。

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

NG筆記15-整合KendoUI

$
0
0

當初評估由KO轉換成NG,與Kendo UI的整合度也是重要考慮依據,KO有社群發展的knockout-kendo可用,Kendo UI則是Kendo UI Labs Team推出Angular Kendo UI,雖然未納入正式官方支援,但可算是經官方認可的程式庫。

關於Angular Kendo UI的使用方法,可以參考這篇Telerik部落格文章,另外有一個精美的展示網站,資訊蠻完整的,這裡僅簡單整理重點:

  1. angular-kendo.js可由Github取得
  2. 除了用<script src="…">引用angular-kendo.js,模組註冊時不要忘記引用"kendo.directives"
    angular.module("someApp", ["kendo.directives"]).controller(…
  3. 加上kendo-* Directive,HTML元素就會自動轉成Kendo UI控件,例如: <input kendo-date-picker>就變成日期選擇器
  4. 基本上,Kendo UI API文件Configuration裡提到的參數,可以用k-*="…"的方式寫在HTML上。例如: DropDownList有dataTextField參數,使用Angular Kendo UI時可以寫成<select kendo-drop-down-list k-date-text-field="'name'" …>,k-* Directive填入的內容也可以是$scope的屬性,例如: <select kendo-drop-down-list k-data-source="dataSourceObjectInScope" >
  5. 另一種做法,是將全部設定變成$scope下的物件(例如: theOptions),然後用k-options一次搞定,例如: <select kendo-drop-down-list k-options="theOptions">
  6. ng-model=""繫結到HTML欄位的Value(永遠是字串型別),如果要繫結到Kendo UI控件提供的結果資料型別(例如: 日期選擇器時取得日期物件、數字輸入欄位時要取得數字型別),則要用k-ng-model=""
  7. 寫成<div kendo-window="win">,相當於將$("…").data("kendoWindow")傳回的Kendo UI物件繫結到$scope.win,後續可用$scope.win.open()加以操控。
  8. 加入k-rebind="屬性名稱",讓Kendo UI在該屬性改變時,重新套用所有k-*設定。
  9. k-ng-delay="屬性名稱"跟k-rebind有點像,應用場合是一開始資料還未備齊前先不要建立Kendo UI物件,當指定屬性名稱改變時才初始化。

另外,有一些應用實務提示: 參考

  1. 如同KO有observable/observableArray/computed、NG有$apply(),如果希望資料異動能即時反應到Kendo UI控件,需改用kendo.data.ObservableObject()kendo.data.ObservableArray()kendo.data.DataSource()
  2. 透過k-*指定事件時,不要忘記傳入事件參數物件,例如: <select kendo-drop-down-list k-on-change="change(kendoEvent)">
  3. 在Kendo UI事件更動$scope屬性後,記得使用$scope.$apply()將異動反應到Angular繫結的元素上。
  4. k-*在指定字串值時不要忘記加上單引號,否則將被視為$scope的屬性名稱。例如: <select … k-data-text-field="name">等同$scope.name,應寫成<select … k-data-text-field="'name'">
  5. 善用<div kendo-window="win">將Kendo UI物件保存在$scope.win,比$("#win").data("kendoWindow")省事,也免除從ViewModel存取UI的疑慮。
  6. Kendo UI常會出現容器從屬關係,例如: KendoWindow內含KendoGrid,此時應留意二者的Scope間的繼承關係。
[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

善用LINQ Except比對產生資料庫增刪指令

$
0
0

手邊有個系統包含轄區概念,每隔一陣子就會微調,把主管A的管區1移給主管B,主管B的管區2移給主管C... 玩一場大風吹。由於提供資料直接交換管道,我們只能依使用者提供的文字檔更新資料庫(實際上使用者提供的檔案格式沒標準化,有時是Excel、有時是PDF,順便大推Word 2013直接開啟PDF編輯的功能,很神!)。因此我的做法是將資料庫現有設定與使用者提供的新設定都轉成格式一致的文字,再設法比對二者差異產生DELETE、UPDATE、INSERT指令(全部刪除再新增是種簡便做法,但缺點時無法產生異動報表供使用者確認)。過去我多半會寫出兩個迴圈,巡一次新資料,找出要變更或新增的項目,再巡一次舊資料,找出要刪除的項目,這回嘗試用LINQ的Except比對,程式異常簡潔,特筆記分享:

來胡搞一個範例,假設三國時代原本統治範圍如下:

曹操:冀州、兗州、青州、徐州、梁州、雍州、豫州
孫權:揚州
劉備:益州、荊州
黑大:潮州
梅西:美州

經過一番爭戰後變更如下:

曹操:冀州、兗州、青州、潮州、梁州、雍州、豫州
孫權:揚州、荊州、交州
劉備:益州
黑大:徐州
魯本:美州

程式如下。先將設定解析成Dictionary<string, List<string>>,接著利用Union()取得新舊設定七位領袖清單,逐一檢查其在新舊設定的轄區集合(若無轄區資料則視為空集合),透過新轄區集合.Except(舊轄區集合)即為待新增項目,而用舊轄區集合.Except(新轄區集合)則可取得待刪除項目,就能輕鬆產生DELETE與INSERT指令囉~ (註: 在本案例假設資料來自內部並經人工檢查,無SQL Injection風險,故採SQL字串組裝以求簡便。在輸入資料來源無法100%掌握的情境裡,絕對不可這麼做。再補充一次SQL Injection安全宣導)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
publicclass Program
{
static Dictionary<string, List<string>> parse(string input)
    {
        var res = new Dictionary<string, List<string>>();
foreach (string line in input.Replace("\r", "").Split('\n'))
        {
            var a1 = line.Split(':');
            var a2 = a1.Last().Split('、');
            res.Add(a1.First(), new List<string>(a2));
        }
return res;
    }    
publicstaticvoid Main()
    {
string srcOrig = 
@"曹操:冀州、兗州、青州、徐州、梁州、雍州、豫州
孫權:揚州
劉備:益州、荊州
黑大:潮州
梅西:美州";
string srcNew = 
@"曹操:冀州、兗州、青州、潮州、梁州、雍州、豫州
孫權:揚州、荊州、交州
劉備:益州
黑大:徐州
魯本:美州";
//解析轉為Dictionary<string, List<string>>資料結構
        var origTerritory = parse(srcOrig);
        var newTerritory = parse(srcNew);    
        StringBuilder sb = new StringBuilder();
//利用Union結合新舊設定所有領袖
foreach (string leader in origTerritory.Keys.Union(newTerritory.Keys))
        {
//取得轄區清單,無資料時取空集合
            var origList = origTerritory.ContainsKey(leader) ? 
                            origTerritory[leader] : new List<string>();
            var newList = newTerritory.ContainsKey(leader) ? 
                            newTerritory[leader] : new List<string>();
//從新清單剔除舊清單已有項目,即為待增加項目
            var toAdd = newList.Except(origList);
//從舊清單剔除新清單已有項目,即為待刪除項目
            var toDel = origList.Except(newList);
//產生註解
            sb.AppendFormat("--{0} 刪:{1} 增:{2}\n", leader,
string.Join(",", toDel.ToArray()), 
string.Join(",", toAdd.ToArray()));
//【資安提醒】
//  為求簡化,本例假設資料源自內部系統,已排除SQL Injection風險,
//  若無法確認資料可靠性時,請勿使用外部輸入資料組裝SQL指令
foreach (var d in toDel) 
            {
                sb.AppendFormat(
"DELETE FROM Territory WHERE Leader='{0}' AND State='{1}'\n",
                    leader, d
                );
            }
foreach (var a in toAdd) 
            {
                sb.AppendFormat(
"INSERT INTO Territory VALUES ('{0}','{1}')\n",
                    leader, a
                );
            }
        }
        Console.Write(sb.ToString());
    }
}

執行結果如下:

--曹操 刪:徐州 增:潮州
DELETEFROM Territory WHERE Leader='曹操'ANDState='徐州'
INSERTINTO Territory VALUES ('曹操','潮州')
--孫權 刪: 增:荊州,交州
INSERTINTO Territory VALUES ('孫權','荊州')
INSERTINTO Territory VALUES ('孫權','交州')
--劉備 刪:荊州 增:
DELETEFROM Territory WHERE Leader='劉備'ANDState='荊州'
--黑大 刪:潮州 增:徐州
DELETEFROM Territory WHERE Leader='黑大'ANDState='潮州'
INSERTINTO Territory VALUES ('黑大','徐州')
--梅西 刪:美州 增:
DELETEFROM Territory WHERE Leader='梅西'ANDState='美州'
--魯本 刪: 增:美州
INSERTINTO Territory VALUES ('魯本','美州') 

Online Demo (順便推薦好用的.NET Fiddle)

HttpRequest相關程式如何做單元測試?

$
0
0

雖然身為開發老兵,最近才開始認真練習單元測試,不小心學到新技巧還會高興半天,嗯,那是一種,說不出來,但,確實,真的,又存在的感覺,是彷彿在魁北克無邊無際的,沙漠,之中的冰原般,的寂寞向日葵,專屬的小確幸(抱歉! 誤啟假文青模式)。今天就來分享一則幼幼班經驗 -- 當程式用到HttpRequest,要如何寫單元測試?

假設我們有個RequestParser類別,提供Parse(HttpRequestBase request)方法,由HttpRequestBase中取出指定Cookie、客戶端IP及UserAgent。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
 
namespace WebHelper
{
publicclass ClientInfo
    {
publicstring SrcCookie;
publicstring ClientIp;
publicstring UserAgent;
    }
 
publicclass RequestParser
    {
publicstatic ClientInfo Parse(HttpRequestBase request)
        {
returnnew ClientInfo()
            {
                SrcCookie = (request.Cookies["src"] ?? 
new HttpCookie("src", string.Empty)).Value,
                ClientIp = request.UserHostAddress,
                UserAgent = request.UserAgent
            };
        }
    }
}

講到這裡,應該已經有WebForm專修班同學舉手發問: "為什麼是用HttpRequestBase,不是HttpRequest嗎?"

故事是這樣的: HttpRequest誕生於.NET 1.1時代,屬於System.Web命名空間,可透過Page.Request及UserControl.Request存取,具唯讀性質。意思是你只能從Page或UserControl取得HttpRequest物件,無法自己建立或修改,這大大限制了它的應用彈性,難以進行單元測試,除非丟到網站,你很難生出一個HttpRequest物件執行測試案例。

有鑑於此,.NET 3.5 SP1加入了HttpRequestBase,具備HttpRequest所有屬性,但差別在於它是抽象類別,開發者可以繼承它寫出衍生類別加入客製邏輯。原本依賴HttpRequest的程式只需改用HttpRequestBase,程式碼毋須修改(因HttpRequstBase的介面與HttpRequest完全相同),這允許我們自行建立及操控HttpRequestBase參數進行測試及應用。而在IIS環境執行時,可使用HttpRequestWrapper將HttpRequest轉為HttpRequestBase,傳給改用HttpRequestBase型別的方法。像在ASP.NET MVC,Controller裡已不見HttpRequest蹤跡,全面改用HttpRequestBase。

寫個WebForm測試RequestParser:

using System;
using System.Web;
 
namespace Web
{
publicpartialclass Default : System.Web.UI.Page
    {
protectedvoid Page_Load(object sender, EventArgs e)
        {
            var client = WebHelper.RequestParser.Parse(
new HttpRequestWrapper(this.Request));
            Response.Write("<li>Client IP=" + client.ClientIp);
            Response.Write("<li>Cookie=" + client.SrcCookie);
            Response.Write("<li>UserAgent=" + client.UserAgent);
            Response.End();
 
        }
    }
}

執行無誤!

把場景切到單元測試,在純Class Library中,要怎麼生出一個HttpRequestBase呢?

繼承HttpRequestBase寫一個MyHttpRequest是種做法,但我們的測試只用到Cookies、UserHostAddress、UserAgent三個屬性,為此寫一顆HttpRequestBase衍生類別實作數十個屬性方法形同"為了拔一根牛毛養一頭牛",人生苦短,嗯湯啊嗯湯。在測試世界裡,通常會用Mock、Stub及Fake技巧解決問題。

爬文得知目前較多人用的.NET Mock元件有Rhino Mocks及Moq,該用哪一個呢? 就使用"拿香跟著拜理論"決定吧!

RhinoMocks 最後更新於 2012/3/18,下載次數26萬次

Moq 最後更新於 2014/2/21,下載次數145萬次

二者相比,猶如德國 7: 1 踢垮巴西,用膝蓋也知道該怎麼選。更重要的一點是,Moq API大量使用Lambda,令LINQ成癮的我心醉,就用Moq吧!

Moq的使用方法很簡單,使用new Mock<HttpRequestBase>()可建立HttpRequestBase的Mock物件,接著以三個SetupGet()設定UserHostAddress、Cookies及UserAgent屬性的讀取行為。SetupGet()使用x => x.SomeProperty的Lambda語法指定要模擬的屬性。而如下圖所示,Moq巧妙地透過泛型宣告讓輸入Lambda時也有Intellisense支援,想打錯字都難(按個讚!)。每個SetupGet()後接上Returns()決定傳回結果。就這樣,我們模擬出一個只有HostUserAddress、Cookies、UserAgent三個屬性有效的陽春HttpRequestBase物件。

測試時,Mock<HttpRequestBase>.Object可當成HttpRequestBase使用,執行結果也如同預期。

藉由Mock物件,我們可以自由捏塑出所需參數型別並指定其行為,不需太多周邊環境配合,就能完成單元測試囉~ 關於Moq的更多說明,可參考官方文件

2014星光馬拉松~

$
0
0

第17馬,2014府城安平仲夏夜浪漫星光馬拉松。

原本不在計劃內,星光馬的好口碑加上有人慫恿,一時意志不堅便報了名,又一場七月盛夏的魔鬼馬拉松。

下午四點起跑,與友人早上坐客運南下,跑完再坐夜車回家,來個24小時閃電遠征。或許沒人相信,年過40卻是第一次坐統聯、第一次踏上台南,對土包子來說,這趟旅程充滿新鮮感。

早上七點在市府轉運站搭車,跑馬拉松不用凌晨三四點起床是種幸福,三排座高層巴士坐起來頗舒適,一趟台北台南票價還不及坐到淡水的計程車錢,心裡萌生"退休後就坐著客運四處旅行吧!"的念頭 XD

適逢假日部分路段小塞,原本預計5.5小時的車程,晚了近一個小時才抵達,在火車站附近匆匆吃了午餐,火速趕搭接駁車。氣象預報說台南的天氣多雲,降雨機率30%,但顯然不是這麼一回事,天好藍,我好怕,今天恐怕要曬成人乾。

近兩點半到接駁點,現場已有長長人龍,算算時間有點緊迫,所幸沒多久排上車,3:30左右抵達會場。(事後得知有跑友4:00仍在接駁車上,只能就地倒數)

 

到達會場,時間所剩無幾,匆忙換鞋寄物,起跑線跑友早已集結,別提熱身伸展,連找個立足之地都很難,不愧是破萬人的規模。

起跑區有個神奇道具,不斷噴出五顏六色的紙花,大會還出動了數台六軸飛行器空拍,把氣氛炒熱到最高點,根本是場嘉年華。

起跑沒多久轉進工業區,大家就被熱醒了,從開跑的歡樂氣氛冷靜下來... 媽呀! 這是什麼鬼天氣,熱翻了加上幾乎無風(有跑友測得37度的駭人氣溫), 甭說加速,連跑七分速都有種喘不過氣的灼熱感。

大會的貼心服務,有好幾個噴霧灑水閘門,路旁也備有水桶水杓以供淋水降溫,熬不住酷熱,狠狠地抓了一瓢水淋在衣服上,瞬間清涼無比。但一時貪涼報應就來了 -- 襪子濕了襪子濕了襪子濕了... 心中雖知不妙,但為時已晚,種下後半場陷入腳痛地獄的惡果。

星光馬果真名不虛傳,補給陣容無比堅強 -- 水、舒跑、啤酒、果汁、冬瓜茶、香蕉、荔枝、西瓜、葡萄、芭樂(超甜!)、牛奶布丁、豆花、炸雞塊、碗粿、水餃、涼麵... 種類多到根本數不完,若每樣都吃一口,還沒跑完就會倒在路邊喘了,如果有人想出一本台灣馬拉松補給圖鑑,來這場考察就對了! 而我看到最誇張的 -- 某個水站竟生了炭火烤肉...

坦白說,我也沒吃太多固體食物,主要是喝水跟運動飲料,外加一些檸檬沾鹽。天氣熱狂飆汗,一直處於極度口渴是原因之一,而更關鍵的因素在於: 萬人跑馬實在太擠,補給站成了補"擠"站,每張補擠桌都被跑友完美包圍,密不透風,不管想拿什麼都得死命硬擠突圍(幸好大家渾身是汗,潤滑效果良好,噁~)。志工補水速度趕不上跑友消耗量,變成半自助服務,自己找杯子自己倒,順手再幫其他人倒,倒完再傳給其他跑友接力倒,充分發揮互助合作棈神。:P

在一個水站沒擠到運動飲料桌,便拿了罐啤酒加減喝順便拍照留念,之後又吃了豆干滷海帶,可惜差個花生米就湊成大三元。另外,拍這張照片時,路旁狂放煙火,數分鐘不絕,但煙霧嗆人,本來埋怨大會怎會來這招,後來才發現是遇上廟會活動,後來還看到七爺八爺與神轎。據說有人還吃到了超級補給 -- 一整排鋼管舞女郎車隊,我跑得不夠快失之交臂,扼腕不已。

忽然隊伍慢了下來,轉進狹小的巷子,我深深地體會到"Bottleneck"的意義。

台南鄉親朋友真是熱情,途中有好多阿公阿媽、小朋友、年輕同學朝氣十足地列隊加油,還有一些住戶拉了自家水管灑水供跑友降溫,人情味十足。

六點了,太陽絲毫沒有收手的意思,繼續火力四射,而襪子濕掉的後座力來了。腳底傳來異物感,心想不妙,泡水腳腫在鞋裡擠壓,很快就形成明顯痛感,原本只是跑不快,進一步淪落到只能跑跑走走。

在海岸一帶看到佛朗明哥舞者(應該是吧?),就在水站旁,不知是否為大會安排的補給項目之一。

賽前盤算著,21K左右路過海邊,如果能在七點左右經過,剛好能看到夕陽。酷暑加腳痛,想快也快不了,果真在近七點時通過海岸線,看到美麗夕陽。

來到海邊,多了海風消暑,加上美景當前,就輕鬆散步享受一番。

來回車資,700元,報名費,1000元,看著夕陽從水平面緩緩落下,無價!

此時此刻,讓人有不虛此行的讚嘆~

日落後氣溫下降,但也離開輕風微拂的海岸,跑進一段防風林間棧道。暗! 這些防風林真講義氣,防到一點風都沒有... 超悶熱的。

之後記憶開始模糊,只記得腳底好痛,怎麼沒風? 腳底好痛,路好窄人好多,怎麼沒風?

路過一艘軍艦。事後查才知是德陽艦軍艦博物館,如果外星人來襲能開出去作戰嗎?

適逢農曆十五,月亮又圓又亮。

30公里時經過終點站,應該擊潰不少人的鬥志,放棄最後12K直接領殘念證書結束折磨! 腳雖痛,但近年累積的耐力要完賽不是問題,就用兩小時吞下這12K吧! 沒想到跑沒多久,低頭想看錶算時間,天色昏暗沒留意前方有個水泥花盆還是石椅,右腳絆了一下人便飛了出去,所時及時回神,趕緊收起四肢背部朝下,在台南街頭示範了霹靂舞的背轉動作(來賓請掌聲?勵),把旁邊跑友嚇了一大跳,快步過來要扶我。幸好反應夠快沒讓四肢在地上磨擦,自己爬起來忙說"沒事! 沒事!",再跑了幾步才發現左腳膝蓋有三道約1公分長的擦傷,等再跑幾公里看到醫護站,進去消毒貼了塊OK繃才再上路。

35K左右又鑽進了自行車道,一路無燈,地上擺了螢光棒標示路徑。路面狹窄,大約只容兩三人並行,而要命的是,整段又在 防 - 風 - 林 裡,完全無風。一路黑暗無光,跑友們全都悶著頭趕路,無人交談,人潮眾多卻全都不發一語,只聽得到急促腳步聲,氣氛很是詭異。在想,打敗仗逃命時大約就是這番情景吧! :P 不過,我看到了一隻螢火蟲,很幸運!!

終於,花了6小時29分,兩隻腳領回四個傷口(左膝蓋擦傷、左前腳掌出現3公分長原子筆心粗細的狹長水泡、兩腳踝突骨下方被短統襪磨出小傷口),總算跑完了。預先訂好00:18回台北的客運,沒時間多停留,領了礦泉水獎牌毛巾完賽證明餐盒寄物,找到傳聞中的淋浴區,居然有蓮蓬頭可沖澡,水壓還很強,真感動。另外,每個人還能領到一個700cc珍奶杯,會場有一整排冷飲自由暢飲。只能說,大會完全懂得跑者的需要呀!

沖完澡就馬不停蹄地趕接駁車,眼看時間剩不到一小時,前方還排了幾十人,所幸工作人員讓趕時間的跑友先上(站票),就這樣順利地接上客運發車。而接駁車下車時,目睹一位大哥下車時面目猙獰,接著雙腿微張以僵屍的姿勢搖晃緩行(腳底起水泡的我走起路來也沒多好看,實在沒資格笑別人),另外有位少年一下車就緊扶著媽媽肩膀,彷彿一鬆手就要癱軟倒地。一眼望去,半夜的台南街頭忽然冒出成群僵屍蹣跚而行,蔚為奇觀。(奇怪的是,許多僵屍不約而同背了同款紅色包包... XD)

搭上紅眼班車,抵達台北剛好清晨。回想這趟旅程,所有時間都完美銜接,老天保佑沒出任何差錯,但也緊湊到連發呆閒晃的片刻都擠不出來,沒機會好好品味台南的風土景色,下回再來一趙慢慢遊覽好了!

星光馬一如往例,?牌設計極具巧思,本屆的金屬獎牌是個熱蘭遮城的立體雕塑,城堡經磁吸固定,掀開下方有印章,這是目前我看過最精緻的完賽獎牌了。

     

清晨六點不到,抵達市府轉運站,完賽餐盒的麵包便當了早餐,為這趟閃電遠征畫下句點。

TypeScript的this陷阱

$
0
0

JavaScript的this是個奇妙的東西,我歷經了一段學習與摸索才有較清楚的觀念。最近在寫TypeScript,踩到一個關於this的小陷阱,特分享兼備忘。

用以下的class舉例:

在類別中宣告了parseResult()函式,另外有個getResultByAjax()函式,透過jQuery.post()呼叫取得資料後打算以parseResult()解析。在Boo class內部要呼叫自身函式,直覺會寫成this.parseResult(),TypeScript為強型別,編輯器沒傳出錯誤,也能順利編譯成功,代表程式沒有問題?

仔細想想,錯得離譜!

在$.post()的結果處理函式裡this不會指向Boo的Instance,需使用Closure傳進去。平時在撰寫JavaScript時,已經養成習慣會注意這些細節;到了TypeScript,以為有強型別保護就高枕無憂,精神一鬆懈,搞出這種低級錯誤!

其實,若更仔細一點也能發現玄機:

被包在function() { }中的this,一律被視為any,加上任何方法、屬性都可成功編譯,而且在輸入parseResult時不會有Intellisense提示,出現這些徵兆時就該有所警覺。

解決方法一如在JavaScript的解法,宣告self變數指向this,在結果函式裡改用self就成了。如此,編輯時會看到Intellisense帶出提示,就代表寫法正確囉! (有更簡潔的做法,請再看下去)

【2014-07-18更新】

文章登出後馬上接到多方回饋,只需改用Lambda寫函式,TypeScript就會自動處理this問題,不需要苦情自幹。例如以下範例:

感謝網友 Kevin、Kevin YangHihi Huang熱心回饋!

【茶包射手日記】RadAsyncUpload上傳檔案跳出網頁登入對話框

$
0
0

接獲報案,某專案使用Telerik RadControl的RadAsyncUpload元件,在選取檔案後隨即出現網頁登入對話框,輸入正確密碼後,元件的上傳進度Icon持續閃黃燈,若按上傳鈕甚至在某些機器上導致IE Crash。

據說問題只發生在某些User的機器上,但數量不少,偏偏資訊部門的機器都無法重現問題,最後總算追查到一位行政同仁的PC曾有相同問題,如獲至寶! 重演一次操作,幸運地狀況還在,射茶包時,重現問題是成功的一半!

實際觀察問題,推斷是選取檔案後網頁抛出某個需求,導致IIS再次要求認證身分。想開Dev Tools檢測,卻發現是IE8,好樣的... 懷疑此問題與IE8有關,或可解釋為何資訊部門大部分的機器都沒遇到類似問題。

用我自己的IE8 VM測試,也成功重現問題! 下一步便是追出是哪個Request收到HTTP 401,呼叫Fiddler登場:

而在IE9+,即便啟動IE8相容模式,由User-Agent字串及Referrer判斷是透過傳統網頁POST方式上傳。

使用關鍵字爬文找到相關討論提及在SharePoint網站上有類似狀況,而SharePoint網頁一定會啟用Windows認證,與我的情境相似。依循Telerik Support的建議,修改web.config開放Telerik.Web.UI.WebResource.axd匿名存取:

<locationpath="Telerik.Web.UI.WebResource.axd">
<system.web>
<authorization>
<allowusers="*"/>
</authorization>
</system.web>
</location>

我的IE8 VM與行政同事的IE8的狀況解決! 以為茶包順利成擒準備收拾書包回家去,使用者端卻傳來不幸消息: 有部分User的狀況解除,有部分User問題仍在,有些User遇到Silverlight的安裝提示,而有部分User還在用IE7也有問題。(啊,冰斗啦!!)

先前的討論文章提到RadAsyncUpload提供了三種上傳模組,啟用順序為 Silverlight > Flash > 純HTML。因IE版本及使用者環境複雜,決定停用Silverlight/Flash上傳回歸最保險的純HTML路線,在網頁中加入:

Telerik.Web.UI.RadAsyncUpload.Modules.Flash.isAvailable = 
function () { returnfalse; };
Telerik.Web.UI.RadAsyncUpload.Modules.Silverlight.isAvailable =
function() { returnfalse; };

問題終告解除~

當心SqlParameter(LINQ to SQL及EF)的小數無條件捨去行為

$
0
0

踩到一顆地雷!! 某支 LINQ to SQL 程式遭投訴: 使用者輸入4位小數,寫入資料庫 DECIMAL(3,2) 欄位時,最後兩位小數被無條件捨去而非四捨五入。

使用以下範例重現問題。在SQL建立資料表MathRound,分別有欄位PK VARCHAR(8)及N DECIMAL(3,2)兩個欄位,分別採用SqlCommand、LINQ to SQL及Entity Framework三種方法寫入三筆資料,PK分別為'ADO.NET'、'LINQ2SQL'以及'EF',而數值則一律為2.2459(decimal)。SqlCommand執行時,以SqlParameter傳入2.2459,但執行兩次,第一次不指定Scale,第二次指定Scale=2。

staticvoid TestCommand()
        {
var cnStr = "Data Source=(local);Initial Catalog=Lab;Integrated Security=SSPI";
using (var cn = new SqlConnection(cnStr))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = "INSERT INTO MathRound VALUES(@pk,@n)";
                var pPK = new SqlParameter("@pk", SqlDbType.VarChar);
                pPK.Value = "ADO.NET";
                cmd.Parameters.Add(pPK);
                var pN = new SqlParameter("@n", SqlDbType.Decimal);
                pN.Value = 2.2459M;
                cmd.Parameters.Add(pN);
                cmd.ExecuteNonQuery();
                pPK.Value = "ADO.NET2";
                pN.Scale = 2;
                cmd.ExecuteNonQuery();
            }
        }
 
staticvoid TestEF()
        {
using (var ctx = new MyEntities())
            {
                var ent = new MathRound();
                ent.PK = "EF";
                ent.N = 2.2459M;
                ctx.MathRounds.Add(ent);
                ctx.SaveChanges();
            }
        }
 
staticvoid TestLinqToSql()
        {
using (var ctx = new MyDataClassesDataContext())
            {
                var ent = new MathRound();
                ent.PK = "LINQ2SQL";
                ent.N = 2.2459M;
                ctx.MathRounds.InsertOnSubmit(ent);
                ctx.SubmitChanges();
            }
        }

由執行結果發現一項事實: 當SqlParameter未指Sacle時,寫入數值為2.25,即2.2459四捨五入的結果;但若指定Scale=2,後兩位小數則被捨去而寫入2.24。而LINQ to SQL及EF等同指定Scale的SqlParameter,也會寫入2.24。

在stackoverflow找到相關討論,指出這是SqlParamter特性始然,而在SqlParamter.Scale文件可找到進一步說明:

Use of this property to coerce data passed to the database is not supported. To round, truncate, or otherwise coerce data before passing it to the database, use the Math class that is part of the System namespace prior to assigning a value to the parameter's Value property. (Scale屬性不支援資料精準度轉換,開發者應自行使用Math.Round或Math.Truncate方式處理後再指定給Value屬性)

【結論】

使用指定Scale的SqlParameter、LINQ to SQL或Entity Framework更新數字到DB DECIMAL欄位,若來源數值之小數位數高於DECIMAL欄位規格,多出的小數部分將會被"無條件捨去"而非一般預期的"四捨五入"! 建議做法是自行使用Math.Round(另外需小心銀行家四捨五入法陷阱)或Math.Truncate將數字轉換成要求的小數精準度,以避免誤差。


2014花蓮遊記~

$
0
0

去年底跟團買了住宿券,於是有了今年的花蓮遊。

每回提及暑假旅遊,心中總會浮起淡淡哀傷。大概是命格異常,我的旅行總與颱風犯沖,至今已有兩次記錄,預先請了假,訂好民宿或遊輪,卻硬生生被颱風沒收!orz 更不要提去年公差出國三天遇上潭美颱風,台灣停止上班兩天,吾等一行人卻在國外爆肝開會,好不悲壯~

不過今年不同,姪女小花,旅運超強,甫滿三歲已旅遊經驗豐富,更重要的是老天爺一路相挺,每次總有好天氣,甚至有多次前腳剛走颱風即至的神奇事蹟。這回要與小花同行,吞下定心丸,心想不會再帶賽了吧?萬萬沒想到,出發前夕還是傳來颱風消息,即便有小花同行,最後仍上演 悲情颱風阿伯 vs 無敵太陽小花 對決的戲碼。

出發前的颱風路徑預測是由台灣南端掃過,影響期間在週三之後,看來太陽小花取得優勢。不料出發後劇情急轉直下,預測路徑一路北修,預估會從台灣中部穿過,儼然是悲情颱風伯的逆襲。所幸,除了第三天天氣轉陰,進雪隧前遇上大雨,我們享受兩天極佳的晴朗天氣,總結本回對決,小花勝出!

早早出發,在蘇花遇上兩台大貨車在隧道內發生事故,雙向交通全斷,足足阻塞兩個小時(這也是悲情颱風阿伯所受的詛咒之一嗎?)但也因此我發現神奇的蘇花公路即時路況臉書粉絲團。以路況只能靠警廣彙整用路人回報並循環播送,在這個人人拿手機、永遠有3G的時代,透過社群平台即時彙整網友提供的文字甚至照片,資料詳細程度及查詢方便性已非傳統廣播可以比擬,Internet已經改變這個世界!

小花的乾爺爺,熊爺爺,長住花蓮,熱情地自願擔任我們的響導,醉心山野的他熟知不少值得一遊的好景點,沾小花的光,這回我們體驗到花蓮屬於原野屬於自然的一面。

跟著熊爺爺的車,開始探險。

放棄去擠摩肩擦踵的砂卡礑步道,改去布洛彎。遊客中心前的一大片草原幾乎被我們獨享,活蹦亂跳的蚱蜢很令小人們興奮。

路過長春祠及從中橫牌樓下開車通過,則讓某位年過四十看了幾十年照片如今首次親臨的中年人興奮不已。

長春祠排隊的遊覽車也太多了吧?

趁著黃昏去了七星潭,東海岸無夕陽可賞,但颱風前夕的藍天碧海依然壯麗。

我喜歡這張,小木頭與小閃光望著大海冥想,要立志成為海賊王?

第二天一早就到鯉魚潭報到,來得夠早不用人擠人,我們一行人霸佔十六隻紅面番鴨。

鯉魚潭更美的風景在另外一頭,千里迢迢只跟番鴨拍完和照就走有點可惜,環湖步道散步騎單車兩相宜。

之後去了林田山林業文化園區,木造房舍、老火車廂充滿古樸風味及森林氣息。

      
     

到了光復沒去糖廠吃冰棒成何體統?順道在糖廠的餐廳吃飯。

糖廠的蒼蠅 好~多~啊~ 一頓飯下來,右手拿筷子左手趕虎神,忙到不亦樂乎。待食畢騰出雙手,熊爺爺特別露一手「徒手擒"虎"」的好功夫,沒兩下子活捉三隻,讓一干後生晚輩折服不已。(PS: 熊爺爺的手機竟是Nokia Lumia 1020,身為Windows Phone User少數族群,920在外見到同類,頗有他鄉遇故知的喜悅感。而這回抱著輕鬆的心情遊花蓮,連隨身小相機都沒帶,全程都靠手機打發,920表現不俗沒辜負這些美景。)

接著來到過去沒聽過的大農大富平地森林園區,景色出奇壯觀。長達數公里完全筆直的公路,一眼就能望到底,盡頭是海岸山脈配上藍天白雲,再加上路旁的孤獨大樹,這根本是伯朗大道金城武樹的花蓮版~

此情此景,身為一名台灣好男兒,我必須做我該做的事…

呃… 感覺怪怪的,在樹下喝茶的不一定是金城武,也有可能是歐吉桑。罷了罷了~

變身金城武失敗沒關係,平地森林公園的壯闊仍然令人心醉。

園區內有四片人工裁植的茂密森林,規劃了長長的自行車道,盛夏有綠葉遮陰,秋天則有楓紅為毯,美哉。

回程改走193縣道,接近日落時分,河道上西瓜田的塑膠布反光交織出奇異的幾何斜紋,別富趣味。

這個角度可以遠眺花蓮溪出海口。

東海岸看不到橘子夕陽或火燒雲,但颱風前夕的天色依然可觀,天頂來了特大塊圓形雲餅。

大陽越過中央山脈映射到東方天際,在地平線上方形成一顆彩虹球,猜想是山尖上某團水氣的傑作。

晚上去了自強夜市,一堆攤子要抽號碼牌掛LED燈叫號,點完要等兩小時取餐令人印象深刻。不過,又不是蟠桃或人參果,為了吃花上這麼久時間實在扯了點,該不會也是一種饑餓行銷?

第三天由氣象報導得知颱風來勢洶洶,路徑北偏且提前來襲,決定一早就先通過路況變數多的蘇花。途中趁著天氣尚未轉壞在東岳湧泉小憇,讓小人們戲水,這裡有三個45到50公分深的戲水池,終年水溫維持17-18度間,水質冰涼清徹(還看到許多小魚),好一個夏日消署的好去處。

之後到南方澳餐廳吃午飯,港口邊吃海鮮,鮮美程度沒話說。吃完要踏上歸程之際,突然大雨傾盆,算是悲情颱風阿伯的最後反擊,但吃飽喝足也玩夠了,裁判宣布:大陽小花大勝!

【茶包射手日記】只聽得到音樂沒有歌聲的鬼耳機

$
0
0

平常很少用電腦聽音樂,前陣子心血來潮想聽音樂專心Coding,不料遇上靈異現象: 歌曲音樂聲還在,但人聲部分極小,聽起來像從遠方傳來,有種空靈感~ 連換了好幾首MP3都是如此,全變成卡拉OK伴唱帶,只播音欒跟和聲,歌得自己唱,但Copy至手機播放則正常。更恐怖的來了,決定播個影片做對照,開了Anders Heljsberg的TypeScript介紹影片測試,大神的演講也被消音,鍵盤聲比說話清楚。但是Windows播放WMV音效正常,控制台的左右聲道測試也很OK。要不是事情發生在鬼月之前,還真得朝「某位抑鬱而終女歌手的怨念發作,偷走了歌聲」偵辦。

起初認定是驅動程式問題,試過升級及重裝音效晶片驅動無效。接著懷疑MP3/MP4的Codec(編碼解碼器)有問題,重裝Codec Pack亦未見改善。

無意間發現,調整左右聲道平衡,若將左聲道或右聲道音量歸零,人聲就會出現,而且左右都有聲音(咦?),感覺人聲因左右聲道抵消才消失。用Vocal、Missing、Blanace等關鍵字查詢,再由查詢結果抓出新關鍵字,終於查出原因:

如上圖,為圖方便,我拿Lumia 920的耳機接電腦,插頭有三環分為四節,由尖端到尾端分別是左聲道、右聲道、接地、麥克風端子(AHJ TRRS標準,另一種規格則是OMTP,接地與麥克風順序相反,Nokia WP8系列的Lumia插孔同時支援AHJ及OMTP);而PC耳機插座規格為兩環三段式,分別為左聲道、左聲道及接地。兩種規格間有微小差距,當端子接觸不夠精準,便可能出問題。若左聲道或右聲道接觸不佳,導致左耳或右耳無聲不讓人意外;但如果是接地接觸不良呢? 是的,下場是音樂裡的人聲會消失! 很神奇吧?

背後的原理是這樣的: 立體聲的立體感來自左右耳聽到的音量有差異,當兩耳音量相同,人類會產生聲音在正前方的感覺。錄製歌曲時會透過混音讓左右聲道提供同相音量的歌聲,樂器聲則安排成左右有別,讓聽者產生「歌手在正前方,伴奏樂團位於歌手身旁」的層次感。換句話說,在音樂MP3裡多半會採歌聲部分左右音量相同,樂器聲左右不同的配置。

當接地良好,左右聲道都能完美呈現。當訊號未確實接地,猜猜會發生什麼事? 左右聲道訊號並未因此消失,而是變成以另一聲道訊號為參考基礎的電壓變化訊號。有趣的部分來了,因為左右聲道具有相同音量的歌聲訊息,互為參考基準後剛好完全抵消,於是便得到「歌聲消失、伴奏保留」的卡拉OK伴唱效果。

那為什麼將左右聲道一方調為零,人聲就會恢復,而且兩耳都有聲音?

將左聲道音量調為0,對右聲道而言參考基準歸零,形同接地,故右聲道恢復正常。而左耳則變成零減去右聲道,待到正負相反但波形相同的訊號,故左耳也聽得到聲音。

另外還有一個現象,按下耳麥通話鈕,人聲也能恢復正常。這應該是因為插孔的接地端搭在插頭的麥克風端子上,按鈕後麥克風端子與接地端子形成通路,讓插孔順利接地,訊號便正常。

奇妙現象全都有了合理解釋,沒有鬧鬼,一切回歸科學。

但聽不到人聲要怎麼解決? 專業解法是買個TRRS插頭轉立體聲的轉接頭,或者有個懶人解法: 插頭別插到底,拉出來一點點讓接地正常,一秒搞定~ XD

【參考資料】

HTML5 Canvas與幾何練習題-矩形邊框交點與線條箭頭

$
0
0

程式寫多了,什麼死人骨頭都可能遇到題材都有機會玩到。最近在寫電子表單流程圖模組,根本是在複習國中數學: sin、cos、兩點距離...  被幾何邏輯搞到昏頭,在草稿紙畫了一堆三角形示意圖還是似懂非懂 orz(數學老師站在我背後,他非常火)

其中有個需求: 用線條連接矩形中心與外部點,但線條需由矩形邊框開始畫起。換句話說,線條在矩形內部的部分隱形,只有與邊框交點到外部點間要畫線。原本已用一堆if else硬幹搞定,但想想還是該用幾何函數解決才會優雅。無奈數學天分不佳,只靠大腦模擬搞不出名堂,心一橫乾脆來寫個繪圖小程式即時顯示計算結果,再依此調整演算法,比全憑抽象思考簡單,適合我這種大腦不夠力的阿呆。測試程式決定用JavaScript寫,順便複習HTML5 Canvas,好個一石二鳥之計。

測試程式成品如下: Online Demo (果然,測試程式剛寫完,演算法也試出來了)

完整程式碼如下:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>計算矩形中心點連線與邊框交點</title>
</head>
<body>
<canvasid="cSketchPad"width="640"height="480"style="border: 2px solid gray"/>
<scriptsrc="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script>
var $canvas = $("#cSketchPad");
var ctx = $canvas[0].getContext("2d");
var box = { x: 100, y: 150, w: 140, h: 90 };
var st = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
function init() {
//清空背景
      ctx.fillStyle = "white";
      ctx.fillRect(0, 0, $canvas.width(), $canvas.height());
//繪製矩形
      ctx.beginPath(); 
      ctx.rect(box.x, box.y, box.w, box.h);
      ctx.strokeStyle = "red"; 
      ctx.stroke();
//繪製中心點
      ctx.fillStyle = "black";
      ctx.fillRect(st.x - 1, st.y - 1, 3, 3);
      ctx.save();
    }
    init();
var ed, pos = $canvas.position(), mx = pos.left, my = pos.top;
    $canvas.mousedown(function(e) {
      init();
      ed = { x: e.pageX - mx, y: e.pageY - my };
//繪製目標點
      ctx.fillStyle="black";
      ctx.fillRect(ed.x - 1, ed.y - 1, 3, 3);
//計算與邊框相交點
var pnt = findEdgePoint(st, ed, box.w, box.h);
      drawLine(st, pnt, "#ccc");
      drawCross(pnt, "black");
      drawLine(pnt, ed, "blue", true);
    });
 
function drawCross(p, c) {
var d = 3;
      drawLine({ x: p.x - d, y: p.y - d }, { x: p.x + d, y: p.y + d }, c);
      drawLine({ x: p.x + d, y: p.y - d }, { x: p.x - d, y: p.y + d }, c);
    }
function drawLine(s, e, c, arrow) {
      ctx.beginPath();
      ctx.moveTo(s.x, s.y);
      ctx.lineTo(e.x, e.y);
      ctx.strokeStyle = c;
      ctx.stroke();
//畫箭頭
if (arrow) {
        ctx.save();
        ctx.fillStyle = c;
        ctx.translate(e.x, e.y);
var ang = Math.atan2(e.y - s.y, e.x - s.x) + Math.PI / 2;
        ctx.rotate(ang);
        ctx.moveTo(0, 0);
        ctx.lineTo(-5, 10);
        ctx.lineTo(5, 10);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
      }
    }
    Math.sign = function(n) { return n == 0 ? 0 : n / Math.abs(n); }
function findEdgePoint(src, dst, w, h)
    {
var dy = dst.y - src.y;
var dx = dst.x - src.x;
//計算斜率
var ang = Math.atan2(dy, dx);
//對角線斜率
var a1 = Math.atan2(h, w), a2 = Math.PI - a1;
//計算交點到中心的長度
var l =
        (ang >= -a1 && ang <= a1 || ang >= a2 || ang <= -a2 ) ?
         Math.sign(dx) * w / Math.cos(ang): //交點在左右側時X軸長度要等於正負w
         Math.sign(dy) * h / Math.sin(ang); //交點在上下側時Y軸長度要等於正負h
//顯示角度(Debug用)     
      ctx.fillText(ang * 180 / Math.PI, 12, 12);
//計算交點座標
var tx = src.x + l * Math.cos(ang) / 2;
var ty = src.y + l * Math.sin(ang) / 2;
return { x: tx, y: ty };
    }
</script>
</body>
</html>

簡單補充:

  1. 在使用rect(), moveTo(), lineTo()時,記得一開始先beginPath()宣告為一段新Path,最後用stroke()繪線時,beginPath()之後的整段路徑會塗成同一顏色。
  2. 箭頭的畫法:
    先store()保留沒有位移、旋轉的狀態,用translate()將位置移至線條的末端,之後以(0, 0)為起點用moveTo()、lineTo()、closePath()圍出三角形,並以rotate()旋轉至與線條水平,最後以fill()塗色。完成後記得要restore()取消旋轉及位移,否則再畫其他元素時座標與角度會跑掉。
  3. 在計算與邊框交點時,需判斷線條觸及上、下、左、右的哪一邊而運算參數不同。我用的方法是找出矩形對角線的四個角度,如下圖中的-32.7、147.3、-147.3、-32.7。當中心與外部點連線的角度介於-32.7與32.7間、小於-147.3,或大於147.3時代表交點在左右側,此時要使 灰線長度 * cos(線條角度) == 矩形寬度的一半,所以灰線長度等於矩形半寬除以cos(線條角度);反之,若交點在上下側,灰線長度要等於矩形半高除以sin(線條角度)。透過這個演算法找出灰線長度,乘上cos及sin就能找出線條與邊框的交點。

在IE8顯示SVG

$
0
0

除了Canvas,HTML5還提供另一種向量繪圖技術--SVG(Scalable Vector Graphics),透過XML標籤定義矩形、圓弧、路徑(Path)、多邊形(Polygon)等向量模型,並可加上濾鏡、變形等特效,就能在網頁顯示可縮放的向量圖案。而SVG的XML元素如同HTML元素能透過JavaScript動態改變屬性、樣式,甚至用CSS :hover選取器就能做出滑鼠移過變色效果,例如:

SVG與Canvas各有所長,適用場合不同,MSDN有篇詳細的分析可做為選擇評估的依據。

手上的專案決定採用SVG,卻遇到一個大問題: 公司還有一堆IE8甚至IE7餘孽未消,但IE9+才支援SVG啊!

Can I Use網站推薦SVG Polyfill -- SVG Web,遇到HTML5瀏覽器就直接顯示SVG,遇到"舊瀏覽器"(IE6/7/8,沒錯,就是在說你們! [氣]),則改用Flash解析SVG顯示圖案。

SVG Web用起來挺簡單。一開始先載入svg.js,在網頁裡用<script type="image/svg+xml">把<svg>文件包起來,svg.js就會視瀏覽器支援程度,直接顯示或是透過Flash呈現SVG圖案:

<!DOCTYPEhtml>
<html>
<head>
<scriptsrc="js/svg.js"data-path="js"data-debug="true"></script>
</head>
<body>
<scripttype="image/svg+xml">
<svgxmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
height="800"width="800">
<gtransform="translate(200,200)"style="fill-opacity:1; fill:none;">
<gstyle="fill: #ffffff; stroke:#000000; stroke-width:0.172">
<pathd="M-12 ... 省略 ... ">
</g>
</svg>
</body>
</html>

薑! 薑! 薑! 薑! 經典的SVG老虎在IE8現身了。按滑鼠右鍵可發現圖案部分其實是Flash物件:

經測試,SVG Web並沒有支援全部的SVG規格,例如漸層背景的顯示就有點問題。我們修改老虎SVG,用<defs><linearGradient>及<rect fille="url(...)">加上漸層背景矩形:

<svgxmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
height="800"width="800">
<defs>
<linearGradientid="linearGradient_0017">
<stopid="stop_0018"stop-color="#003059"offset="0"/>
<stopid="stop_0019"stop-color="#0088ff"offset="0.95"/>
<stopid="stop_0020"stop-color="#ffffff"offset="1"/>
</linearGradient>
</defs>
<rectid="rect_0002"x="0"y="0"width="600"height="600"
stroke="#aaa"stroke-width="2"fill="url(&quot;#linearGradient_0017&quot;)"/>
<gtransform="translate(200,200)"style="fill-opacity:1; fill:none;">
<gstyle="fill: #ffffff; stroke:#000000; stroke-width:0.172">

IE10/IE9及其他瀏覽器顯示正常,在IE8用Flash模擬時背景變成黑色。(左邊為模擬IE10模式的效果,右邊為IE8) 另外,遇到較複雜的SVG,Flash需花2-3秒才顯示結果,效能不如瀏覽器內建引擎,但SVG Web仍不失為可用的SVG跨"舊"瀏覽器解決方案。

【笨問題】在Excel如何只加總篩選後的結果

$
0
0

剛學會一個非常基本的Excel技巧:

Excel篩選功能很好用,但使用SUM()加總永遠都是全部資料的總和,如何只加總篩選後的結果呢?

答案是 -- 使用SUBTOTAL()函數!廢話不多說,請看VCR~

【笨問題】Excel「數值儲存為文字」小筆記

$
0
0

拿到一份資料庫查詢匯出的Excel檔,想加工卻碰了釘子。如下圖,明明是數字,用SUM(A1:A4)加總卻得到0。

全是幻覺,嚇不倒我滴!一切皆因儲存格格式被設成文字。

已知原因,但經驗不足,後續處理不怎麼順利。先試著將儲存格格式改為數值,加上千位號並調小數位數,但因欄位已被認定成文字,不動如山。換一招,複製整欄再用選擇性貼上,試了不同選項,文字就是文字,怎麼都無法變回數字。

【小筆記】一般手動輸入時若想將數字強迫存為文字(最常見情境是電話號碼,遇到通用格式前方的零會被移除),做法是在數字前方加上單引號(')。如果文字內容來自複製,要避免被解讀成數字走味,則可先將儲存格格式指定成文字再貼上即可。

所幸,Excel夠貼心!當數字被視為文字儲存,儲存格左上角會出現綠色三角,同時有選單提供處理選項,其中有項「轉換成數字」,執行後Excel就會將欄位解讀成數字。另外,「轉換成數字」也支援批次處理,可以選取範圍後一次轉換。

同事分享了另一種做法,使用「資料剖析」也能快速將整批文字轉成數字,雖然步驟較多,但能對不同欄位指定不同轉換原則,還有很多花式應用,威力強大,特別適合複雜情境。

轉換為數字後,就能順利加總了。

爬文還找到其他解法,但都不如「轉換成數字」及「資料剖析」簡便,補充於後:

  1. 使用選擇性貼上配合乘法運算(跟JavaScript String轉Number有異曲同工之妙 XD)
    EXCEL 移除 ' 符號 並讓「格式為文字」的數字,一次轉換成數字格式的方法 @ Ted's Blog -- 痞客邦 PIXNET --
  2. 另外建一欄位用=VALUE(A1)做轉換

【茶包射手日記】TypeScript錯誤: missing property 'concat'

$
0
0

一段運作正常的Kendo UI JavaScript程式,搬到TypeScript後發生編譯錯誤,起火點在DataSourceOptions物件的group屬性:

var x = new kendo.data.DataSource({
                data: [],
                group: {
                    field: "Boo", aggregates: [
                        { field: "Foo", aggregate: "sum" }
                    ]
                }
            });

錯誤訊息如下

Supplied parameters do not match any signature of call target:
    Types of property 'group' of types '{ data: any[]; group: { field: string; aggregates: { field: string; aggregate: string; }[]; [n: number]: kendo.data.DataSourceGroupItem; }; }' and 'kendo.data.DataSourceOptions' are incompatible:
        Type '{ field: string; aggregates: { field: string; aggregate: string; }[]; [n: number]: kendo.data.DataSourceGroupItem; }' is missing property 'concat' from type 'kendo.data.DataSourceGroupItem[]'.   

江湖經驗不足,沒能一眼看出錯誤原因,到官方論壇提問後才自己找出問題來源。is missing property 'concat'是關健字,通常發生於該傳陣列的地方卻給了一般物件。

查看kendo.all.d.ts(官方論壇可下載最新版),group的定義為DataSourceGroupItem[];

interface DataSourceOptions<T extends kendo.data.Model> {
        aggregate?: DataSourceAggregateItem[];
        autoSync?: bool;
        batch?: bool;
        data?: any;
        filter?: any;
        group?: DataSourceGroupItem[];
        page?: number;
        pageSize?: number;

但依據API文件,group可為陣列或物件(如果只有一筆,可直接給設定物件),與TypeScript指定必須為DataSourceGroupItem[]有出入,如此可解釋為何JavaScript程式能跑,搬到TypeScript卻無法編譯。依我的理解,屬性型別不像函式可以多載,設定any的話就無法規範DataSourceGroupItem的設定屬性,或許這是Kendo UI RD的取捨吧!

既知原因,解決就是小事一椿,將物件改寫成單一元素陣列 group: [ { field: "boo", aggregates: [ { field: "foo", aggregate: "sum" } ] } ] 便一切OK囉~


【水電工日記】馬桶漏水維修記

$
0
0

前幾天提水桶沖完馬桶,水面低於下方出水口,發現馬桶在關閉狀態也會微微滲水,水量不大,但平時出水口在水面之下,這種漏水量很難察覺,莫非這水已經漏了一整年?雖然理性地算一下,這個速度一天下來也差不多是一次沖馬桶的量,但對天性摳門的我來說,點點滴滴,跟被人一直用小石子K頭沒什麼兩樣。更重要的是,最近全球暖化的議題喊得震天價響、天然資源將耗盡的問題正酣,若坐視我們家的馬桶一直漏水下去,將有可能導致全球水資源耗盡、人類滅絕。於是,我決定即刻展開拯救全人類的偉大行動---把漏水的馬桶修好。(謎之聲:同一個梗是拿來要玩幾次啦?)

查了網路,推測是水箱內的落水皮老化造成,這是極常見的馬桶漏水原因。抱著興奮心情衝到水電材料行(沒辦法,太久沒變身黑暗水電工),經店員指示找到水材零件區,如網路所說,落水皮有好有壞,價差頗大,如下圖1到3,從40元到近200塊都有。大概是路邊攤到LV的區別吧!40元的摸起來像塑膠,讓人懷疑密合性及耐久度,最後我買了中等價位橡皮材質的MIT產品(99塊大洋)。

落水皮(或稱止水皮或止水塞)的構造為上方呈半球體蓋狀,下方突出有孔,沖水時靠球體內的空氣保持懸浮,維持出水口開啟,待水洩完失去浮力再因重力落下止水。這款落水皮的半球體厚實,又是橡皮材質,感覺密合度應該不錯。下方突出部分則為分離式,需簡單組裝。

落水皮後方有兩根帶孔短桿,要掛在注水管根部的小鉤上當成轉軸,桿子材質是軟的可彎曲,輕拉轉動便能卸下或套上,難度不高。網路上的安裝教學蠻多的,甚至有示範影片可看,這裡就不多贅述。

觀察換下的落水皮,經過深入分析、測量與計算,茶包射手大膽推測落水皮老化變形是本次漏水的主因。(廢話!邊緣都扭成波浪狀了,用膝蓋想都知道。)

就這樣,黑暗水電工再次止住漏水,拯救了地球。但這只是小事一椿,大家將感謝默默放在心裡就好,不必發賀電寫卡片送花籃發獎狀頒匾額打金牌給我哦~ (謎之聲:黑先生,該吃藥囉!)

從IE11的可笑UserAgent聊起

$
0
0

微軟在Windows Phone 8.1 Update裡幹了一件看似好笑的事,把Windows Phone的IE11 User Agent改成:

Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 930) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537
(註:User Agent是瀏覽器向網站伺服器表明自己種類、版本的資訊字串,每次發出HTTP請求時夾帶送出,網站可依此對不同版本瀏覽器提供不同內容)

明明是Windows Phone的IE,User Agent裡Android、iPhone、Mac OS X AppleWebKit、Safari 什麼都來一下是怎樣?是想冒充Android或iPhone嗎?沒錯!WP8 IE11這回騙很大,但效果也十分驚人!(話說,Safari也幹過同樣勾當

右圖是舊版IE11開啟Google Calendar的畫面,左圖則是新版換上「迷彩User Agent」IE11開啟同一網頁的效果:


圖片來源:Neowin

再來看看Twitter行動版,右圖是更新前IE11的檢視畫面,左圖是更新User Agent後的結果:


圖片來源:IE Blog

為此IE Team發了篇Blog文章(The Mobile Web should just work for everyone,文中還舉了百度、約紐時報的實例),指出部分知名網站對行動裝置上的IE、Firefox不友善,為手機開發專屬網頁,支援HTML5的瀏覽器就能顯示,IE/Firefox卻因User Agent判斷邏輯被導向桌面版,美觀度及操作便利性大打折扣。我不認為這需要搬出陰謀論,直覺認定純粹是市佔率現實再加上網站規劃者的決策(我自己寫網站也常把Opera當空氣呀 :P )或開發者疏忽造成的結果。前兩點因素非關技術,沒什麼好討論,就來看看開發者如何精準判別行動瀏覽器種類,讓使用者在其裝置得到最佳使用體驗。客人明明一口好牙,你放著牛排不上卻只敢端豆腐上場,很荒謬啊!

IE Blog舉了一個真實案例,有個網站判斷行動裝置的JavaScript函式如下:

window.mobileCheck = function() {
var check = false;
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
return check;
}

這段程式當初肯定寫得很辛苦,卻害某些手機使用者瀏覽得很痛苦,也讓接手維護的人很想哭。(腦中浮現畫面:PM跟工程師說要再多支援一款新手機,請修改以上程式!某人就跳樓了…)

依IE Team建議,「偵測User Agent有沒有包含"mobile"」就夠了!

function isMobile() { return navigator.userAgent.toLowerCase().indexOf("mobile")>=0; }

除了User Agent偵測,另一個常見的相容議題是網頁使用-webkit-*、-moz-*等瀏覽器專屬CSS屬性(Vender-Specific Property),正統做法應為每個屬性寫三份CSS:標準版、webkit版、moz版,已有很多前端工具可以幫忙(例如:Visual Studio WebEssentials的Generate vendor specifics、jQuery cssHooks、Sass/LESS CSS3 Mixin…)而新版IE11加入將webkit CSS對應成標準CSS的功態,遇到只寫-webkit-*沒寫標準版CSS的網頁也能正確顯示。雖然有一堆Workaround,我想「瀏覽器規格早日統一」還是所有前端工程師心中最大的願望。

說穿了,即使已有一堆工具、程式庫、Framework幫忙,「跨瀏覽器」這個偉大的目標依千絲萬縷,鳥事如麻,革命尚未成功,同志們請繼續努力~  orz

XML Documentation常見陷阱一則

$
0
0

小測驗,以下程式碼有什麼問題?(請忽略程式實用價值)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ClassLibrary
{
/// <summary>
/// 測試類別
/// </summary>
publicclass Boo
    {
/// <summary>
/// 數字
/// </summary>
publicint Value { get; set; }
/// <summary>
/// 若 Value > 100,傳回true
/// </summary>
publicbool IsBig
        {
            get
            {
return Value > 100;
            }
        }
/// <summary>
/// 若 Value < 100,傳回true
/// </summary>
publicbool IsSmall
        {
            get
            {
return Value < 100;
            }
        }
/// <summary>
/// First Name & Last Name
/// </summary>
publicstring FullName { get; set; }
    }
}

答案藏在XML Documentation裡:註解用到特殊字元<、>及&卻未額外處理,轉成XML程式文件會爆炸:

「>」未與「<」成對時不易誤判,尚能正常顯示;但「<」及「&」會跟後方內容一起被解讀成HTML標籤或特殊符號,就會出現上圖中的<!—Badly formed XML comment ignored form member "…" –>。在專案使用DocsByReflection讀取XML文件,也跟著爆炸:(原本預期出現<member />的地方出現<!—... –>導致節點讀取失敗)

An unhandled exception of type 'System.InvalidCastException' occurred in DocsByReflection.dll
Additional information: Unable to cast object of type 'System.Xml.XmlComment' to type 'System.Xml.XmlElement'.

註解問題被視為警告不影響編譯,卻也常被忽略,直到轉為Help文件或用於整合時才發現。在要求「0 Warning」的專案裡註解問題很難被無視(如下),但你知道的,理想與現實總是有差距的… (掩面)

結論:當XML註解文字涉及<, >, &等Syntax Charactors,記得改用&lt; &gt;及&amp;。若出現XML Documentation相關錯誤,也請優先排除是否與特殊字元有關。

PS:在web.config/app.config中也要小心類似狀況

【茶包射手日記】SharePoint導致ASP.NET PageMethod失效

$
0
0

接獲報案,某ASP.NET PageMethod程式部署至正式主機失效,以jQuery送出POST Request,未傳回JSON結果而是傳回完整網頁,如同未設[WebMethod]屬性一般。

經過對照測試,鎖定問題只有在ASP.NET網站跟SharePoint 2007並存於同一IIS站台時發生,只要脫離Sharepoint魔掌就一切如常。

爬文找到有人提及需確認web.config已加入ScriptModule,PageMethod才能正常運作:

<httpModules>
  <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</httpModules>

但問題專案為.NET 4.0,預設已載入ScriptModule,不像3.5需額外加掛,何況同一web.config設定在沒遇到SPS時是正常的!推測SPS涉有重嫌,加上有前科,即刻發出拘票抓回審問!

打開SPS的web.config檔找到<httpModules>的一瞬間,我就明白了!

問題出在第一列的<clear />,SPS把預設的HttpModule清光光,再依序重新掛上所需模組。這樣子就算.NET 4預設已載入ScriptModule也被清掉,少了ScriptModule,造成PageMethod失效。

找出原因,要解決只是一塊蛋糕。在使用PageMethod ASP.NET web.config補上ScriptModule:

<httpModules>
  <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</httpModules>

搞定收工!

TypeScript列舉型別

$
0
0

列舉(Enumerate)是我愛用的TypeScript特性之一,它能嚴格限制數值範圍,較數字或字串安全,不慎打錯字在編譯時就會被揪出來,對於錯字成習甚至已發展成個人特色的我來說,節省了可觀的Debug時間,並大幅降低氣到想刴手指的風險,功德無量。(手指頭:謝謝你,TypeScript!)

TypeScript列舉轉換到JavaScript端的設計得挺巧妙,花了點時間才熟悉,順手整理筆記供大家參考。

TypeScript列舉宣告寫起來跟C#幾乎相同,但實際轉換成的JavaScript物件很有趣,例如以下的enum:

enum catgs {
    Desktop, Pad, Phone
}

會產生一個名為catgs的物件,且包含六個屬性:

var catgs;
(function (catgs) {
    catgs[catgs["Desktop"] = 0] = "Desktop";
    catgs[catgs["Pad"] = 1] = "Pad";
    catgs[catgs["Phone"] = 2] = "Phone";
})(catgs || (catgs = {}));

透過這六個屬性,就可以將0、1、2轉換成"Desktop"、"Pad"、"Phone",也能將"Desktop"、"Pad"、"Phone"轉換成0、1、2。

預設TypeScript會由0開始逐一為列舉項目編號,但我們也可以自訂對應的數值:

enum catgs {
    Desktop = 10, Pad, Phone
} //結果:Desktop=10, Pad=11, Phone=12
enum flags {
    Public = 1, Static = 2, Inherited = 4
} //也可設計成Flag應用

列舉宣告後,輸入程式時就能享受Intellisense:

打錯字會出現警告且無法編譯:

最後,實務上一定會遇到數字、字串與列舉相互轉換的情境,說明如下:

  1. 列舉與數字互換
    TypeScript會將 var c1 = catgs.Desktop; 編譯成 var c1 = 0 /* Desktop */;故以JavaScript觀點,列舉型別本身就是數值,遇到TypeScript只接受數值型別的場合,直接把列舉當數字用就好,例如:呼叫外部函式時當數字參數用、做加減運算(可能會超出範圍破壞嚴謹性,不鼓勵)。
    反之亦然,若數字要轉成列舉,不需任何轉換,直接指定即可。
    var c1 = catgs.Desktop;
    //列舉型別可以直接當成數值數使用
    var n:number = c1;
    function x(v:number) {}
    x(c1);
    //列舉型別可以指定為對應數字
    //但若給錯值編譯時不會發現,不建議使用
    var c2:catgs = 1;
  2. 列舉與字串互換
    前面提到catgs有六個屬性,在做字串轉換時就派上用場囉!不多說,直接看示範
    //列舉型別可以指定為對應數字
    var c2:catgs = 1;
    //傳入列舉對應數字可以取得文字
    alert("c2 = " + catgs[c2]); //顯示c2 = Pad 
    //列舉型別不可以直接指定為列舉文字
    //var c3:catgs = "Phone";//<--無法編譯
    //傳入列舉文字可以取得對應數字,轉成列舉型別
    var c4:catgs = catgs["Phone"];
    alert("c4 = " + c4);  // 顯示 c4 = Phone
Viewing all 2429 articles
Browse latest View live


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