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

CODE-自動產生程式碼時將System.Int32轉為int

$
0
0

這是我在寫程式碼產生器常遇到的小困擾。例如: 當透過Reflection偵測型別轉成屬性型別宣告時,typeof(T).ToString()會產生如"System.String"的完整型別名稱,故不時會出現如下的產出結果:

public class Player
{
    public System.String Name { get; set; }
    public System.Int32 Score { get; set; }
....略....

有沒有覺得程式碼裡充滿System.String、System.Int32 十分"矯情",一點都不像正常人會寫的程式(雖然它執行起來100%正確),充滿了賤人機械味。一時手癢想改善這點小瑕疵,便參考C#文件關於Predefined Types(預先定義的型別)的項目建成轉換字典,如此便可將產出程式碼的System.Int32換成親切的int,看起來賞心悅目多了!

//REF: http://msdn.microsoft.com/en-us/library/aa664635(v=vs.71).aspx
staticstring[] typeMap = newstring[] {
"object,System.Object",
"string,System.String",
"sbyte,System.SByte",
"short,System.Int16",
"int,System.Int32",
"long,System.Int64",
"byte,System.Byte",
"ushort,System.UInt16",
"uint,System.UInt32",
"ulong,System.UInt64",
"float,System.Single",
"double,System.Double",
"bool,System.Boolean",
"char,System.Char",
"decimal,System.Decimal"
        };
static Dictionary<string, string> PT2Name =
            typeMap.ToDictionary(o => o.Split(',')[0], o => o.Split(',')[1]);
static Dictionary<string, string> Name2PT =
            typeMap.ToDictionary(o => o.Split(',')[1], o => o.Split(',')[0]);
 
staticvoid Test()
        {
string s = typeof(string).FullName;
            Console.WriteLine("{0}->{1}", s, Name2PT[s]);
            s = "uint";
            Console.WriteLine("{0}->{1}", s, PT2Name[s]);
        }

【同場加映】

透過Reflection取得泛型及巢狀類別相關型別時,會出現如"`"、"+"等符號。例如: typeof(Dictionary<string, Dictionary<DateTime, Boo.Foo>>).ToString(),會得到奇怪的字串結果: (Boo.Foo為定義在Boo裡的巢狀型別)

System.Collections.Generic.Dictionary`2[System.String,System.Collections.Generic.Dictionary`2[System.DateTime,Boo+Foo]]

如果直接在程式碼使用它當作類別宣告,包你錯到鬼哭神號。針對這個問題,我在Stackoverflow上找到一段高手寫的簡短函數,自己補上巢狀型別的"+"轉換,再接上前述的Predefined Type名稱置換、省略System.*命名空間(註: 需配合對應的using namespace宣告)等簡化,最後終於導回正軌,得到:

Dictionary<string,Dictionary<DateTime,Boo.Foo>>

範例程式碼如下,供有興趣的朋友參考:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
 
publicclass Boo
{
publicclass Foo { }
}
 
class Program
{
staticvoid Main(string[] args)
    {
        Type t = typeof(Dictionary<string, Dictionary<DateTime, Boo.Foo>>);
        Console.WriteLine(t.ToString());
        Console.WriteLine(GetFriendlyTypeName(t));
        Console.Read();
    }
 
//REF: http://msdn.microsoft.com/en-us/library/aa664635(v=vs.71).aspx
staticstring[] typeMap = newstring[] {
"object,System.Object",
"string,System.String",
"sbyte,System.SByte",
"short,System.Int16",
"int,System.Int32",
"long,System.Int64",
"byte,System.Byte",
"ushort,System.UInt16",
"uint,System.UInt32",
"ulong,System.UInt64",
"float,System.Single",
"double,System.Double",
"bool,System.Boolean",
"char,System.Char",
"decimal,System.Decimal"
        };
static Dictionary<string, string> PT2Name =
        typeMap.ToDictionary(o => o.Split(',')[0], o => o.Split(',')[1]);
static Dictionary<string, string> Name2PT =
        typeMap.ToDictionary(o => o.Split(',')[1], o => o.Split(',')[0]);
 
 
 
//REF: http://stackoverflow.com/questions/401681
//Add nested type name, predefined types, System.* namespace trimming support
publicstaticstring GetFriendlyTypeName(Type type)
    {
        Func<string, string> fixTypeName =
            (s) =>
            {
                s = s.Replace("+", "."); //Fix nested type name
//Replace predefined types
foreach (string n in Name2PT.Keys)
                    s = s.Replace(n, Name2PT[n]);
//Trim System.* namespace
                s = Regex.Replace(s, @"System\.(\w+\.)*(?<n>\w+)", 
                    m => m.Groups["n"].Value);
return s;
            };
 
if (type.IsGenericParameter)
        {
return fixTypeName(type.Name);
        }
 
if (!type.IsGenericType)
        {
return fixTypeName(type.FullName);
        }
 
        var builder = new System.Text.StringBuilder();
        var name = type.Name;
        var index = name.IndexOf("`");
        builder.AppendFormat(
            fixTypeName(type.Namespace + "." + name.Substring(0, index)));
        builder.Append('<');
        var first = true;
foreach (var arg in type.GetGenericArguments())
        {
if (!first)
            {
                builder.Append(',');
            }
            builder.Append(GetFriendlyTypeName(arg));
            first = false;
        }
        builder.Append('>');
return builder.ToString();
    }
}

【茶包射手日記】怪異的IE DNS Error

$
0
0

遇到一起奇怪的IE問題。

User回報使用某ASP.NET網頁出錯,問題發生在使用者按下送出鈕後,ASP.NET網頁進行PostBack,但沒多久IE 8彈出DNS Error:

無法顯示網頁。找不到伺服器或 DNS 錯誤。
The page cannot be displayed. Cannot find server or DNS Error.

這問題最吊詭的地方在於: 前後都是同一支ASP.NET程式,若真因DNS有誤找不到網站,怎麼會先GET正常顯示網站,卻在POST時回報找不到該網站?

使用Fiddler追蹤後,發現該網頁PostBack耗時較久,1分多鐘才會傳回結果,而使用馬錶計時,IE只等了約10秒就彈出找不到伺服器錯誤! 為了驗證此問題會發生在每個耗時較久的網頁,我做了一個簡單的測試網頁,PostBack時Thread.Sleep(30*1000),等待30秒再Response.Write(),使用問題IE測試,得到同樣結果 — IE等了10秒,接著彈回找不到伺服器錯誤!

同事找到一則微軟KB,提供重要線索 -- 當網站傳回資訊耗時過久,IE有可能出現"無法顯示網頁。找不到伺服器或 DNS 錯誤。"。有趣的是: 1) 為何錯誤訊息不是連線逾時而是DNS錯誤? 2) IE8的預設逾時設定為60分鐘,10秒彈出錯誤是哪招?

依KB所說,找出ReceiveTimeout Registry設定,發現問題機器的設定值為10,000,相當於10,000ms,跟先前觀察到的10秒完全吻合,至此真相大白!!

但是,為什麼它會被修改成10秒??

嗯,跟大家想的一樣,使用者指出對於此事完全不知情! 擔心刑求逼供會傷了皇城之內的和氣,本案到此終結。

如何切換TFS伺服器的連線帳號

$
0
0

被問到如何在Visual Studio 2012切換企業內部TFS伺服器的連線帳號?

例如: 在VS2012中以Domain\UserA連上某台TFS伺服器,之後想改用Domain\UserB登入以取得不同授權。發現Visual Studio的連線TFS伺服器設定介面不支援此種一人分飾多角的情境。如下圖右,首次登入後,再選取該TFS伺服器,就會自動登入並顯示當初登入帳號,沒有切換身分的機會。

先前保哥有篇雲端 TFS (Team Foundation Service) 如何切換登入身分文章介紹了如何切換雲端TFS的登入身分,但測試後發現登出鈕只有連線到雲端TFS時才有,連線到TFS伺服器時不提供登出功能。

  連線雲端TFS時可登出  TFS Server時無登出鈕

猜想,在大部分企業內部應用情境,每個人只會使用自己專屬的AD帳號存取網路資源,不同使用者會使用不同AD帳號登入Windows,而TFS連線設定包含認證身分也會保存在使用者Profile不致混淆,一人分飾多角並不常見,或許這是連線企業TFS Server時沒有提供登出功能的原因吧!

已知TFS連線保存在使用者Profile,而TFS本身為HTTP 8080 Port Web Service(or API),由此推敲,連線認證應會採用Windows內建的網站認證保存機制。果不其然,在 控制台 / 使用者帳戶 / 管理您的認證 中發現了Visual Studio每次連線TFS自動登入的依據。

將上圖中紅框所指的兩個認證都移除掉(tfs為本案例TFS Server的機器名稱),重新啟動Visual Studio,下次連線時將會重新彈出登入對話框,便可使用不同AD帳號登入囉~

【同場加映】

如果你常常在切換身分,希望每次連線時都重新選擇登入帳號(是有沒有這麼忙?!),每次都要刪認證太麻煩,在登入時可以不要勾選"記住我的認證",當想要切換身分時,請關閉所有IE(清除認證暫存區),重新啟動VS2012,就可以重新選擇登入TFS的帳號。

【茶包射手日記】呼叫showModalDialog時發生"物件不支援此屬性或方法"錯誤

$
0
0

接獲報案,某個在IE7運作已久的網頁改用IE8執行,onblur事件呼叫windows.showModalDialog()開啟新視窗的功能傳回"物件不支援此屬性或方法"(Object doesn't support this property or method)錯誤。showModalDialog確定是window物件的內建函數,同一網頁已在IE7使用多時,window忽然翻臉不認showModalDialog是哪招?

用錯誤訊息爬文後,才想起這是個老問題 -- 快顯封鎖會封鎖"由程式觸發而非使用者點擊觸發的開啟新視窗行為",onblur呼叫showModalDialog被歸在非使用者點擊觸發的範圍,故被封鎖! 但坑爹之處在於: 快顯被封鎖,傳回的錯誤訊息不是預期的Access Denied(存取被拒),而是詭異的"物件不支援此屬性或方法"。

將該網頁網址加在前文文末圖示的Allowed sites清單,問題瞬間消失,結案收工。

TIPS-快速輸入INotifyPropertyChanged屬性

$
0
0

為了讓物件支援Data Binding,資料物件必須實作INotifyPropertyChanged介面,提供PropertyChanged事件,並在屬性值變動時,以便即時通知UI更新繫結對象的顯示內容。而實作INotifyPropertyChanged的類別,在宣告每個屬性時都要寫成如下格式:

private bool connected;
public bool Connected
{
    get { return connected; }
    set
    {
        connected = value;
        OnPropertyChanged("Connected");
    }
}

不能只用public bool Connected { get; set; }帶過。

為了簡化撰寫程序,我會用propfull Snippet(Snippet是什麼? 能吃嗎?)先建出私有變數、get、set區塊,再為set區塊插入OnPropertyChanged。問題出在,我是一名耐心比0.5自動鉛筆筆芯還細的莽夫,同樣的步驟重複超過三次就會肝火上升,於是興起製作內含OnPropertyChanged程式碼propfull的念頭。動手前找了一下,網路上已有先進寫好了Snippet,這下就樂得不用自己造輪子囉~

將該文章的兩段程式存成notifyo.snippet及notifyp.snippet,開啟VS2012的Code Snippet Manager,即可將這兩個Snippet匯入Visual C#自訂程式片段區。(X:\Users\username\Documents\Visual Studio 2012\Code Snippets\Visual C#\My Code Snippets)

匯入後,現在只要輸入notifyp,Visual Studio會自動帶出程式片段,我們只需填入型別及屬性名稱(下圖紅框處,一個包含OnPropertyChanged的屬性宣告就完成了。

網路上還有其他透過更複雜機制簡化程式撰寫的方法(例如使用Attribute標示,或是像這篇,甚至用上了修改MSIL的大絕... 佷可怕,不要問!),個人偏愛KISS原則(Keep It Simple and Stupid,或者更有學問一點,要說我是奧卡姆剃刀的愛用者),使用Snippet已讓宣告屬性的撰寫步驟簡化到合理可接受的程度,我選擇不再追求更省事的做法,以保有系統架構的單純性。

CODE-利用T4依XML產生多個資料物件

$
0
0

遇到的需求是有現成的XML資料定義檔如下

<?xmlversion="1.0"encoding="utf-8" ?>
<datatable>
<tableid="Running"version="1.0">
<colname="RunDateTime"datatype="System.DateTime"pkey="Y"/>
<colname="RunnerId"datatype="System.String"pkey="Y"/>
<colname="Record"datatype="System.String"/>
</table>
<tableid="Runner"version="1.0">
<colname="RunnerId"datatype="System.String"pkey="Y"/>
<colname="Name"datatype="System.String"/>
<colname="Birthday"datatype="System.DateTime"/>
</table>
</datatable>

希望為其中每個Table各產生一個C#類別轡下:

using System.ComponentModel.DataAnnotations;
 
publicclass Runner
{
    [Key]
public System.String RunnerId { get; set; }
public System.String Name { get; set; }
public System.DateTime Birthday { get; set; }
}

在過去,我會自己寫Code,解析XML文件再用StringBuilder組裝C#類別的內容。但既然已經學會T4範本程式產生器這等好物,自然沒有再走回頭路的道理。

由XML產生C#類別的邏輯很簡單,唯一的小挑戰是一般T4範例多是一個.tt檔產生一個.cs檔,而這個需求得依照XML定義一次產生多個.cs檔案。爬文找到範例,單一.tt要輸出多個檔案的原理,是在執行過程由GenerationEnvironment.ToString()取得當時累積的輸出結果,以File.WriteAllText()寫入檔案後,執行GenerationEnvironment.Clear()清掉已寫到檔案的內容,再執行下個類別的程式碼產生邏輯。

T4程式碼如下:

<#@ template  debug="true" hostSpecific="true" #>
<#@ output extension=".log" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ Assembly Name="System.Windows.Forms.dll" #>
<#@ Assembly Name="System.Xml.dll" #>
<#@ Assembly Name="System.Xml.Linq.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #> 
<#
string currPath = Path.GetDirectoryName(Host.TemplateFile);
    XDocument xd = XDocument.Load(Path.Combine(currPath, "marathon.xml"));
foreach (var t in xd.Root.Elements()) {
string tableName = t.Attribute("id").Value;
#>
using System.ComponentModel.DataAnnotations;
 
publicclass<#= tableName #>
{
<#
foreach (var c in t.Elements()) {
string name = c.Attribute("name").Value;
string type = c.Attribute("datatype").Value;
bool isPK = c.Attribute("pkey") != null;
if (isPK) {
#>
    [Key]
<#
            }
#>
public<#= type #> <#= name #> { get; set; }
<#
        }
#>
}
<#
string relativeOutputFilePath = "Code\\" + tableName + ".cs";
string outputFilePath = Path.Combine(currPath, relativeOutputFilePath);
        TemplateHelper.WriteTemplateOutputToFile(outputFilePath, GenerationEnvironment);
    }
#>
<#+
//REF: http://bit.ly/X9wTu0
publicclass TemplateHelper
{
publicstaticvoid WriteTemplateOutputToFile(
string outputFilePath,
        System.Text.StringBuilder genEnvironment)
    {
        System.IO.File.WriteAllText(outputFilePath, genEnvironment.ToString());
        genEnvironment.Clear();
 
    }
}
#>

補充幾點:

  1. 預設T4並未參照System.Xml.dll及System.Xml.Linq.dll,記得要在最前方透過<#@ Assembly #>及<#@ import #>加入參照。
  2. 透過Host.TemplateFile可取得T4檔案所在實體路徑,可用來決定輸出檔案路徑。
  3. T4檔末端透過<#+ #>宣告了共用函數TemplateHelper.WriteTemplateOutputToFile(),透過其將產生結果寫入檔案。

2013 櫻花馬拉松~

$
0
0

2013 三月三連馬二部曲,重回引領我進入馬拉松殿堂的初馬聖地 -- 雙溪櫻花大道。

還是菜菜的馬拉松二年級生,參加心境卻已大不相同,天氣也大不相同,去年在8度寒流淒風苦雨中完賽,今年則是氣溫直逼28度的豔陽天。山路依舊,心境、氣候不同,便有不同風景,這是一場賽事值得年年參加不倦的理由。

今年的櫻花馬創下可怕的記錄,報名當天網站瞬間被秒殺,幾經波折,最後全馬人數直衝五千人,而其中有近2500名初馬,除了櫻花馬的好口碑,得歸功於琉璃馬的驚人魔力吧!

清晨不到六點,驅車前往雙溪,途中看到仍沈浸雲霧間的山巒,美得讓人摒息。只是,依過往爬山經驗,這層霧氣已在預告(或警告)今天的太陽會很熱情~ (抖)

六點半抵達會場,時間尚早,大會志工已開始集結準備,如臨大敵。這場五千人賽事動員了六百名志工,跑者最多六個小時跑完就解脫,志工們卻要服務超過十小時,辛苦了!! 聽了飛小魚的廣播節目,對於馬場志工的世界才多了一層認識,在此向每場賽事熱心投入的志工們致上敬意與謝意。

今年因人數激增,會場改在雙溪高中,場地較大,沒想到連我這種無社團散客也有休息帳篷可用。時間尚早,便好整以暇地在帳篷下躲太陽(是的,七點出頭就開始出大太陽了),簡單伸展,順便擦防曬油。會場有不少體育用品攤位,價格頗優惠,本想順便物色雙新鞋,好讓腳上伴我南征北討里程破千的Ware Rider 14早日退役,這才驚覺,擔心身懷貴重物品有風險,出門時身上只帶了平日跑LSD買涼水用的悠遊卡跟幾百元,鞋子大概只能買左腳的前半截,這下只好作罷。

八點左右由會場往起跑點移動,步行約500公尺穿過一個短隧道,起跑點才出現眼前。照慣例會依實力分區,給自己SUB 5的期許,便站在4:31-5:00這區,而5:00之後剛好由隧道口排起,跑友佔滿隧道,五千人的場面果然浩大! 照慣例: 有沒有每次起跑,就算沒看到獅子頭大哥也會聽到他聲音的八卦? (他是大會主持人)

 

8:30鳴槍起跑,今年為因應人數爆增路線稍有調整,進入山區產業道路前先來10K寬敞平地路段消化人潮,我也才有機會拍下綿延數公里的壯觀人龍。

身為二年級生已無初次挑戰時的忐忑,但連續9K爬坡的疲憊依然原汁原味重現,很沒志氣地一上坡就切換成步兵,保留實力給回程,也給下一場。這場比賽,初馬跑者的比例驚人,其中也不乏實力堅強者,被超車時難免心頭一驚,但想想本當如此,成績源自訓練、天賦與態度,無關資歷。這場也算驗收自己一年以來的訓練成績,速度與爆發力沒啥長進,但肌耐力明顯提升了,去年雙膝硬化(與雙溪櫻花相映成趣 XD)、每一步都在抽筋邊緣的慘烈場面未再上演,只是天氣真的熱爆了,全身汗濕不說,帽簷開始滴汗不止,口渴難耐,進水站時趕緊補水補鹽,裝完一肚子水像個水壺搖搖晃晃上路,是另一種折磨,如果有選擇,還是冷一點比較好。

大約21K左右遇到第一名折返,狂奔的速度彷彿剛才沒爬過坡是坐小黃上山的,令人折服。前半段未盡全力,後半程路跑來還算輕鬆,只有右小腿近腳跟處有些緊繃,把速度放慢,在水站噴了點肌樂,一路倒也相安無事沒爆發。大會這回辦得挺用心,賽前還為初馬們辦了兩場LSD團練,對一些參加過團練的初馬跑友,今天根本是"原車原地考照",真是佛心來著。水站的桌子排得夠長,不致擠成一堆,過終點不到兩分鐘就領到成績單,加上先前提過散客專用休息帳,以及賽後淋浴區等等,不少地方都看得到大會的用心。但賽後在網路上看到跑友抱怨後期水站大缺水(似乎是交管臨時變更導致補給不及)、領成績等候及奬座排隊問題... 要辦一場七千人的賽事,真的不是件簡單事。

回程繼續執行上坡走下坡跑的戰術,但後繼無力,下坡段加速有限,難以彌補早先上坡散步損耗的龐大缺口,最後SUB 5不保,只以5:08:18完成第七馬。準備迎接三連馬的最後一役,嚐嚐在高速公路上狂奔的滋味!

【Pace統計】
05:57 / 05:34 / 05:22 / 05:29 / 06:19 / 05:14 / 05:35 / 05:15 / 05:42 / 05:21 / 05:34 / 08:23 / 09:35 / 08:22 / 08:52 / 08:52 / 08:47 / 07:29 / 09:17 / 05:22 / 07:43 / 06:46 / 08:25 / 07:48 / 09:53 / 10:43 / 07:04 / 06:45 / 06:00 / 08:14 / 10:59 / 08:45 / 07:54 / 09:36 / 07:20 / 06:34 / 05:17 / 06:21 / 07:45 / 05:48 / 06:26 / 06:19 / 06:43

前10K平地還算好看,之後的配速只能用"荒唐"二字形容了,哈!

Skype MSN連絡人群組匯入工具Ver1.3 - 支援別名設定功能

$
0
0

不少網友反應,Skype整合了MSN連絡人,卻未帶入在MSN為連絡人設定的別名,造成在Skype裡只看到一堆英文名或Email,搞不清楚誰是張三,誰是王五? 故建議工具能在匯入群組時一併連別名也還原。

原本透過實測,判定Live網站上不提供別名資料,後來經網友提醒,MSN設好別名後,必須要重新登入登出幾次,別名資料才會出現在Live網站連絡人的匯出檔。有了此一情資,加上期待別名設定功能的朋友為數不少,基於民之所欲,常在我心程式不難寫,Coding樂無窮的心態,便為這支衛生紙型工具再做一次改版,推出1.3版,加入別名設定功能。

原本別名設定是跟匯入類別時一起執行,但在FB群組釋出測試後有網友反應,Skype的別名資料看來未保存在伺服器,不能跨機器,所以可能得每台機器都跑一次別名設定,於是再做了一次調整,將匯群組跟設別名兩個功能拆成獨立按鈕,方便在多台機器上重複執行別名設定。

1.3版在抓取連絡人資料時,會一併試著帶入連絡人的別名,但發現Live網站上的別名欄位,有時會摻雜MSN標題,未必是別名,這屬已知問題,目前也沒有解決方案,只能請大家在設定別名前再人工核對一次。要修改別名時,可直接在介面上修改(如下圖橘框),亦可匯出成CSV再一次調整,另外,建議大家手動設定完匯出成CSV檔備分,以便未來能重複執行。

另外,1.3版還有一些小改良,包含:

  1. 支援直接由CSV匯入資料就設定,不一定要登入Live網站
  2. 中文模式下CSV檔編碼由UTF-8改為ANSI(BIG-5),說白話文講一次: 用Excel開CSV不會出亂碼了!
  3. 改掉一個小Bug,防止程式開太久會一直吃RAM的情況。

完整操作說明及檔案下載連結已新增至工具首頁,有需要的朋友請自取。


【茶包射手日記】離奇的Chrome HTTP 502 Bad Gateway Error

$
0
0

91做了一個有趣的Web API Help Page,可以自動產生Web API的方法清單並加註說明,還提供直接使用瀏覽器測試的功能。不過測著測著,遇上鬼打牆:

  • 同一台機器用IE/Firefox測試OK,Chrome測試傳回HTTP 502 Bad Gateway
  • 使用公司的Chrome測試OK
  • 家裡的Chrome測試Azure上的版本出現502,測試本機Server則沒問題
  • Chrome出錯只限於透過Test API發出的GET Request,直接在網址輸入同樣URL則完全沒問題

在線上遇到91,聊到這枚離奇茶包,格外引發我的好奇,骨子裡駭客魂蠢蠢欲動... 等到意會到自己在幹什麼時,眼前是兩個Chrome視窗,一個使用Test API功能傳回HTTP 502,一個則是在網址輸入Test API所GET的URL,回應正常... 使用Chrome Developer Tool,分別截錄下兩個Request的內容

以下是HTTP 502錯誤時的Request

GET /api/Values HTTP/1.1
Host: 91-webapi-sample.azurewebsites.net
Connection: keep-alive
Cache-Control: max-age=0
If-Modified-Since: Thu Jan 01 1970 08:00:00 GMT+0800 (台北標準時間)
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.152 Safari/537.22
Accept: */*
Referer: http://91-webapi-sample.azurewebsites.net/Help/Api/GET-api-Values
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4
Accept-Charset: Big5,utf-8;q=0.7,*;q=0.3
Cookie: ARRAffinity=f5105030b49455c354a66ad349ab75b57b342f0a8b721d229021b4f0df2e5a18; WAWebSiteSID=99670baf00114d618cff2295d00d248a

以下是HTTP 200正常回應時的Request

GET /api/Values HTTP/1.1
Host: 91-webapi-sample.azurewebsites.net
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.152 Safari/537.22
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4
Accept-Charset: Big5,utf-8;q=0.7,*;q=0.3
Cookie: ARRAffinity=f5105030b49455c354a66ad349ab75b57b342f0a8b721d229021b4f0df2e5a18; WAWebSiteSID=99670baf00114d618cff2295d00d248a

二者差異有限,結果迥異。至此已破案在望,只需比對二者,以消去法抓出關鍵差異即告真相大白。

雖然或許能找到可編輯Request Header的Chrome Addon進行實驗,但我選擇自己用Visual Studio寫支小程式來玩,一方面當作練功,二方面想確保100%擁有Request內容的控制權(其實,以上全是程式魔人為了想寫Code,千方百計編出來的藉口... orz),程式範例如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
 
namespace ChromeIssue
{
class Program
    {
staticvoid Main(string[] args)
        {
            TcpClient client = new TcpClient();
            client.Connect("91-webapi-sample.azurewebsites.net", 80);
string fixedChromeReqData = @"GET /api/Values HTTP/1.1
Host: 91-webapi-sample.azurewebsites.net
Connection: keep-alive
Cache-Control: max-age=0
If-Modified-Since: Thu Jan 01 1970 08:00:00 GMT+0800 (台北標準時間)
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.22...省略...
...省略...
Cookie: ARRAffinity=f5105030b4...省略...
 
";
            var stm = client.GetStream();
byte[] buff = Encoding.UTF8.GetBytes(fixedChromeReqData);
            stm.Write(buff, 0, buff.Length);
//為了簡化起見,不管Stream資料傳回時機,直接等兩秒後讀結果
            Thread.Sleep(2000);
using (StreamReader sr = new StreamReader(stm))
            {
byte[] result = newbyte[65535];
                var len = stm.Read(result, 0, result.Length);
                Console.WriteLine(Encoding.UTF8.GetString(result, 0, len));
            }
            Console.Read();
        }
    }
}

為了100%控制HTTP Request內容,我選擇不用高階的WebClient物件,而是自己建TCP連線,對Stream進行讀寫。使用以上程式碼送出HTTP 502情境的Request Header,果真也得到HTTP 502錯誤,Bingo! 接下來的重點是一一拿掉可疑Request Header,若移掉某條Header就正常,人肯定是他殺的! 沒多久,我找出If-Modified-Since: Thu Jan 01 1970 08:00:00 GMT+0800 (台北標準時間)是造成HTTP 502的嫌犯。將Request簡化到只剩
GET /api/Values HTTP/1.1
Host: 91-webapi-sample.azurewebsites.net
If-Modified-Since: Thu Jan 01 1970 08:00:00 GMT+0800 (台北標準時間)

仍然會產生HTTP 502,一旦移掉字尾的"(台北標準時間)",HTTP 502錯誤就消失,罪證確鑿,凶手現身!

追蹤Test API程式碼,發現該Request Header來自WebApiTestClient.js(Web API Test Client package)
Line 168    httpRequest.setRequestHeader("If-Modified-Since", new Date(0));
進一步測試,new Date(0).toString()在中文版Windows的IE/Firefox會傳回"Thu Jan 1 08:00:00 UTC+0800 1970" ,而Chrome則傳回"Thu Jan 01 1970 08:00:00 GMT+0800 (台北標準時間)"。是的,Chrome雞婆地在後方加註(台北標準時間),讓Request Header裡多出了這段中文,導致美國端的Azure主機(應為英文版Windows)解讀失敗發生錯誤。一瞬間,所有離奇的鬼打牆現象都有了解答:

  • 同一台機器用IE/Firefox測試OK,Chrome測試傳回HTTP 502 Bad Gateway
    只有Chrome會在Date().toString()加"(台北標準時間)"
  • 使用公司的Chrome測試OK
    公司的Windows為英文版
  • 家裡的Chrome測試Azure上的版本出現502,測試本機網站則沒問題
    本機網站為中文版(也可能是本機Web Server版本與Azure不同),能順利解讀Header裡的中文
  • Chrome出錯只限於透過Test API發出的GET Request,直接在網址輸入同樣URL則完全沒問題
    問題Request Header是JavaScript額外加入的,直接輸入URL時不會出現 

前一刻滿天疑雲,下一秒忽然雲開見日,很有撞球一桿清台(Clean Table!)的暢快,射茶包迷人之處,莫過於此!

2013 國道馬拉松~

$
0
0

2013 三月三連馬最後一役,挑戰在高速公路奔馳的快感~

之後無賽事在望,終於不必再保留戰力,而高速公路平整寬敞,對於創造佳績是大利多,今天的戰略清楚明瞭,只有兩個字 -- "衝呀!"

5:20抵達會場,速速擦好防曬,寄完物上完廁所,便在交通警察的護駕之下,大大方方走上交流道前進起跑區,享受在高速公路橫行的特權!

氣象預報今天的氣溫18-28,晴到多雲,6:00出發時太陽還躲在雲後,氣溫估計不到20度,很適合跑步,但心裡有數,一切會在太陽出現後改觀。

高速公路的平整寬敞不在話下,就算幾千人同時開跑也不太有擁擠感,我也樂得無拘無束恣意狂奔,看到"前有測速照相"標誌時還刻意減速以免受罰(大誤! 謎之聲: 你的號碼布別在前面,被拍到也沒關係啦! [特誤])。只是國道雖然好跑,景緻卻十分單調,原以為將是場無聊旅程,沒想到澎湃洶湧、高潮迭起的內心戲卻讓這場成為難忘回憶!

  • 5K 25:31
    最前面5K,只花掉26:31,平均Pace 5:18。嗯,狀況真好!
  • 10K 52:27
    只花了52分鐘就跑完10K,平均Pace 5:11,比前5K還快,Wow,狀況極佳! 在此段一舉超過4H配速免子,難道,今天我會拿下夢想中的SUB 4(四小時內完賽)嗎?
  • 15K 1:20:11
    這5K花了27:44,中間還包含上了一次廁所,但仍在6分速之上。SUB 4成為心中目標後,開始緊扣水站的停留時間,幾乎只拿幾塊餅乾配一杯飲料就邊跑邊塞。
  • 20K 1:28:14
    再花了28:02跑完5K,都快跑完半馬了,速度仍佳,疲累不多無傷無痛,今天跑得好順,感覺SUB 4已不再遙不可及。
  • 21K 半馬破PB 1:53:10
    雖然是場全馬,順便破一下半馬PB,加油!
  • 25K 2:48:03
    20K後的5K花29:49,半馬後仍有這個速度,且一路領先4H免子,好感動,SUB 4真的不是夢想耶!
  • 30K 2:48:01
    跑完近3/4了,這5K仍保持在29:57,高於6分速。若以4小時完賽為目標,餘下1h12m,最後12K只要不低於6分速,今天就能拿下人生第一個SUB 4,把MP3切成搖滾樂,準備靠著MDR(Music Driven Run)創造奇蹟,情緒也跟著激動起來~
    淚光迷濛中,彷彿站在雲端,看著SUB 4大門就在面前伸手可及,但怎麼就是摸不到... 忽然,馬拉松大神現身空中,聲如洪鐘: "不得放肆,老雜魚! SUB 4的神聖殿堂豈是你現在這種實力該來的地方,給我滾!"
    一瞬間,SUB 4大門消失無蹤,我被4H免子超車! 雖然仍能勉力跟上,但步伐已顯吃力。
  • 35K 3:18:02回到凡間
    30K後的5K花了30分鐘完成,餘下7K,餘下42分鐘,理論上只要維持6分速,仍有機會在4小時完賽賽。但自己很清楚,這最後7K只能勉強維持6分鐘,但要壓進SUB 4,一定要更積極地拉到5:40以上,嘴巴說可以,身體倒是很誠實地投下反對票。
    當SUB 4幻夢破滅,我瞬時由雲端回到了人間,面對現實。
  • 40K 4:06:48
    黑暗教練團做出關鍵指示,換下熱血歐吉桑,換了閒散老頭子上場,好整以暇地在高速公路上散步看風景,充分享受花800元報名費買來在高速公路散步的特權,唯一的缺點是,太陽好矖哦~ 一點都不愜意。這5K花了48:45外加一路被狂刷卡,媽呀,有夠可恥! 但閒散老頭可不在乎這些,黑暗教練團的策略真高明。
  • 42K 4:25:39
    最後回到終點,終以4:25:39秒完賽,還是破了PB,說起來最後7K幾乎全用走的,算是躺著破了PB,還留足了下回破PB的充裕空間,只能說黑暗教練團真是令人不齒、十足體壇敗類機智過人,慎謀能斷呀!

   

   

   

我想,我會永遠記得這場體驗雲端到人間的精彩比賽,並握拳發誓,總有一天,我一定會踏進SUB 4殿堂!

【Pace統計】
05:18 / 05:17 / 05:05 / 05:16 / 05:05 / 05:12 / 05:09 / 05:23 / 05:21 / 05:20 / 05:42 / 05:37 / 05:12 / 05:24 / 05:26 / 05:41 / 05:25 / 05:40 / 05:44 / 05:33 / 05:36 / 06:13 / 05:50 / 05:57 / 06:17 / 06:30 / 05:54 / 06:02 / 05:56 / 05:49 / 05:56 / 06:05 / 05:55 / 05:45 / 06:05 / 06:25 / 09:12 / 09:47 / 10:21 / 12:36 / 11:13 / 09:15

JavaScript Eevent偵錯利器–Visual Event 2

$
0
0

開發網頁的朋友應該都有過類似經驗,網頁載入一堆JavaScript,一陣兵荒馬亂後,很難搞清楚最後在哪些元素的哪個動作掛了事件,尤其是JavaScript加掛事件的方式五花八門,可以透過jQuery、element.click = function() { }、element.addEventListener()…,很難由單一處找出所有事件。而理不清事件來龍去脈,要追蹤某個點擊動作背後的程式行為就變得有些困難。

發現一個神奇的JavaScript偵察工具 – Visual Event 2

Visual Event 2的神奇之處在於不需要在瀏覽器上加裝任何外掛,只需把一長串以"BLOCKED SCRIPT"為首的字串記為書籤或我的最愛,便可適用於各大瀏覽器。使用時,先開啟待偵察網頁,再按下書籤或我的最愛,網頁便會注入Visual Event程式,以視覺化方式呈現掛載事件的元素,如下圖所示:

Visual Event的運作原理,在於其熟知主要JavaScript程式庫(例如: jQuery、YUI、ExtJS)事件機制,可深入其中擷取事件資訊,並將其標註在對象元素上。目前支援的程式庫包括:

  • DOM 0 events
  • jQuery 1.2+
  • YUI 2
  • MooTools 1.2+
  • Prototype 1.6+
  • Glow
  • ExtJS 4.0.x

如前圖所示,在啟用Visual Event後,有掛事件元素將被標上藍色區塊,滑鼠停留時會顯示事件的細節。以上圖為例,該<a>元素掛載了click及mousedown兩個事件,其中mousedown是透過DOM方法直接掛上,其指向一個匿名函數,其中呼叫了sc_clickstat_call()。如此,追蹤事件就簡單多了。

【茶包射手日記】在Windows x64註冊OCX/DLL元件

$
0
0

接獲報案,在Windows 2008註冊OCX失敗。開啟cmd.exe執行regsvr32 boo.ocx時傳回錯誤訊息:

The module "c:\windows\system32\boo.ocx” failed to load. Make sure the binary is stored at the specified path or debug it to check for problems with the binary or dependent .DLL files. The specified module could not be found.
無法載入模組 c:\windows\system32\boo.ocx"。 請確定二進位檔儲存於指定的路徑,或進行偵測以檢查二進位檔或相依 .DLL 檔是否發生問題。找不到指定的模組。

雖然訊息指出問題出在找不到檔案,但確定OCX檔案是存在的,聯想到問題可能出在Windows平台為x64,但OCX是用32位元開發的關係。爬文後確認此點,在x64平台註冊32位元元件(OCX, DLL)應使用c:\windows\sysWOW64\regsvr32.exe,而非c:\windows\system32\regsvr32.exe! (發現有趣的事: system"32"下的regsvr32.exe是64位元版,sysWOW"64"下的regsvr32.exe才是32位元版。XD)

將boo.ocx複製到c:\windows\sysWOW64,並使用同目錄下的regsvr32.exe執行註冊。以為就此打完收工,但又接到第二則錯誤訊息:

"boo.ocx" was loaded but the call to DllRegisterServer failed with error code 0x80040200
模組boo.ocx已載入, 但是呼叫 DllRegisterServer 失敗, 錯誤碼為 0x80040200

原因: 同事因不熟悉UAC的眉角,在啟動cmd.exe時未Run As Administrator提升為管理者權限,導致0x80040200權限不足錯誤(印象中,權限不足代碼多為0x80004005,本例不然)。重新升為管理者權限執行cmd.exe,註冊成功!

打造更貼心的連動欄位網頁

$
0
0

在網頁設計中輸入欄位連動是很常見的情境,例如有員工編號及員工姓名兩個欄位,當使用者在輸入員工編號後,系統需自動帶出員工姓名。一般最直覺做法是利用<input>的onchange或onblur事件,在使用者輸入完成後送出AJAX呼叫向伺服器查詢後設定姓名欄位。程式範例如下: (展示)

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>傳統OnChange觸發</title>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script>
        $(function () {
            $("#txtEmpNo").change(function () {
                $.get(
"DataSrc.ashx",
                    { key: $(this).val() },
function (res) {
                        $("#txtEmpName").val(res);
                    });
            });
        });
</script>
<style>
        .fld {
            width: 80px;
        }
</style>
</head>
<body>
員編: 
<inputid="txtEmpNo"class="fld"/>
<inputid="txtEmpName"readonlyclass="fld"style="background: #CCC"/>
<inputtype="button"value="查詢"/>
</body>
</html>

預期的操作流程是: 使用者輸入員工編號,右方灰底唯讀欄位帶出員工姓名供確認員編無誤,接著再按下查詢。

但是因為帶出姓名的邏輯放在change事件,使用者輸入124後,必須按Tab或點選其他網頁元素才會觸發AJAX呼叫。不熟操作的使用者,可能會在輸入完124後停止動作,卻苦等不到姓名出現,摸索一段時間才學到"多按一下Tab"的撇步。

為了改善操作流暢度,我們試著將查詢姓名的邏輯由onchange事件移到onkeyup按鍵事件,如此使用者按完鍵,毋需按Tab或點選其他欄位,系統就會直接帶出姓名。這種設計,應用於編號長度固定的情境還算完美,因為在keyup事件中可以檢查已輸入的文字長度,等待編碼完整再送出查詢。但若有效資料長度不一,就會產生困擾。例如: 在我們的範例程式中,1代表Transparent、11代表Blue、114代表Purple,於是在輸入114的過程,左側的欄位會依序冒出Transparent、Blue及Purple(見下方動畫展示),這意味著程式觸發了無效查詢,也干擾了使用者操作。(展示)

開發世界高手如雲,便有人想出巧妙解法 -- 使用者在輸入過程一般會先有連續打字的階段,輸入完畢後則會出現停頓,因此程式便可依按鍵間隔偵測使用者是否已輸入完畢,待全部輸入完畢後再送出查詢。透過這個技巧,電腦瞬間通人性,不需要多按鍵,又知道等輸入完成再查詢,提供更好的使用者體驗。

要使用JavaScript實踐構想並不困難,只需要在每次keyup事件時,不要直接送出AJAX,而是以window.setTimeout預定一段時間再送出(例如: 1秒),而在設定setTimeout時要留下識別,以便下次keyup時,先以clearTimeout清除上次預約的AJAX動作,再設定新的預約AJAX動作。如此,若按鍵後未滿一秒又按鍵,前次的AJAX查詢就會被取消,直到打完字停頓超過一秒沒按任何鍵,AJAX查詢才會真的送出。這段邏輯寫起來就會像下面這樣:

       $(function () {
var hnd;
            $("#txtEmpNo").keyup(
function () {
//取消前一次預定的查詢
                    window.clearTimeout(hnd);
//延遲一秒後才查詢,若這一段內又輸入其他字元
//則此一預約執行會被上一行程式取消
var value = $(this).val();
                    hnd = window.setTimeout(function () {
                        $.get(
"DataSrc.ashx",
                            { key: value },
function (res) {
                                $("#txtEmpName").val(res);
                            });
                    }, 1000);
            });
        });

操作過程如以下動畫所示,不需要額外Tab或點選,在輸入完成後又能自動帶出姓名,此種設計是不是更善解人意呢? (展示)

為了解說原理,我們剛才用setTimeout、clearTimeout自己造了輪子。但實務上不用這麼麻煩,網路已有現成Plugin可以實現前述構想,它有個術語叫Debounce(借用自電子學術語,指在接收訊息時避免發生誤動作的濾波機制),簡單列出幾個jQuery Plugin:

以下是改用doTimeout Plugin後的寫法: (展示)

        $(function () {
            $("#txtEmpNo").keyup(
function () {
                    $(this).doTimeout("findEmpName",
                        1000, function () {
                            $.get(
"DataSrc.ashx",
                                { key: $(this).val() },
function (res) {
                                    $("#txtEmpName").val(res);
                                });
                        });
            });
        });

學會這招,大家不妨檢視自己專案中採用onchange、onblur觸發AJAX連動欄位的場合,看看有無調整的需要,試著讓自己的網頁作品更貼心吧!

如何在Windows 8變更密碼提示?

$
0
0

在Windows 8要修改密碼,依循習慣,佷自然地在桌面模式按下Ctrl-Alt-Del,如預期地在Windows安全性(Windows Security)視窗找到"變更密碼(C)"的選項:

正準備修改密碼,卻發現沒地方設定密碼提示! 在Windows 8安裝過程可以設定,變更時卻無法指定?

過去我敢自信地說"誰需要密碼提示這種東西?",但人屆中年,擔心過度自信不設密碼提示,有天會上演這般場景:

努力地尋找一番,才發現如果要設定密碼提示,得使用【電腦設定】中的【使用者】/【變更您的密碼】

就能在設定密碼時一併指定密碼提示囉!

我的Windows Phone 8體驗

$
0
0

HTC HD7使用一年多,開始偶爾出現喇叭失效的小故障,需要進行一個拍打機身的動作才能排解,萌生換機念頭。手機大廠Nokia加入Windows Phone陣營後,依著過去對N牌手機的良好印象 ( 鄉民也說讚,別忘了神奇的3310 ),加上Nokia在地圖、導航領域投入頗深,相關App支援最完整,讓我對Nokia的WP8手機有頗高期待。

Lumia 920巫術等級的暗處照相效果讓我挺心動,但問市後一直處於缺貨狀態,前陣子貨源漸豐,加上Nokia推了一個開發者方案(購機優惠,送開發帳號及元件),沒有太多猶豫便決定入手。

行動裝置用了這麼多年,這回決定不裝殼不貼保護貼,徹底體驗裸機使用的豪氣。說老實話,3C產品壽命有限又是身外之物,何苦呵護像公主,應當使喚如女僕,才稱得上不為物役呀~ (謎: 屁! 明明就是不想花錢買配件,刮花了別哭蛤~)

由於計劃裸機使用,雖然有點想挑跳TONE的紅色,卻擔心亮面機身惹來一身指紋得日日擦拭變長工,最後決定黑色--霧面的唯一選擇。

與Windows Phone 7使用經驗相比,分享我所感受到的Windows Phone 8亮點:

  1. 倉頡輸入法(終於...)
    坦白說,WP7的注音輸入法的提示、選字做得挺好,但直到倉頡出現,在手機上打字才真的擺脫跛腳感。另外,920螢幕比HD7大,按鍵操作空間更充足,輸入流暢度也大大加分
  2. 支援螢幕擷取(終於...)
    同時輕按Start鍵與電源鍵,手機畫面天會被存成圖檔放入"螢幕擷取畫面"資料夾,要抓圖展示總算容易多了
  3. 連接電腦後可模擬儲存裝置
    如下圖,不解釋!
  4. 備份功能(終於)
    可將相片、影片、App清單、撥話記錄、簡訊、IE我的最愛、Microsoft Account設定、手機設定備份到Skydrive。但不包含App軟體本身、App專屬資料,且存入Skydrive的備份內容會被隱藏,無法透過其他方式檢視或下載。這篇文章對WP8備份有很詳細剖析,而我認同該文的結論: 備份就是要做到能全機還原才算完美啊!
  5. 影音檔案同步
    可設定自動將相片、影片上傳至Skydrive,也算是種備份機制,但差別在於上傳後可透過Skypdrive瀏覽檢視。
  6. 動態磚應用豐富化
    動態磚不再只能多顯示數字,允許App高度客製化,如下圖的天氣預報及電量顯示就是很好的應用案例:
  7. Nokia地圖相關App
    Nokia地圖、導航、城市特蒐(配合GPS、羅盤,即時在相機觀景窗顯示各方位商店/餐廳/旅館的距離資訊)、公共運輸(規劃如何搭乘大眾交通工具前往目的地)... 不確定是否Nokia手機專屬,都絕對是實用的App。
  8. 兒童專區
    只挑選少數程式建立兒童專區,在其中就只能使用指定的音樂、影片、程式及遊戲,把手機交給小孩亂玩也安心,不慎摔壞就又可以再買新的(咦?)
  9. 無線更新
    拿到手機剛設定完就收到更新通知,接著被導入程序,OS就莫名其妙升級了。WP8不再像WP7必須要連上PC執行Zune才能下載更新。而WP8也不再需要Zune,一開始接上USB時Zune找不到920裝置讓我楞了一下,後來才知道WP8的更新及傳檔都能直接來,中介軟體也不再有其必要性。
  10. 內建簡單照片編輯功能
    內建剪裁、旋轉功能,簡單處理不需借助其他App
  11. 動態磚有三種尺寸,排列組合更自由
    但這點已在HD7升級WP 7.8後體驗過
  12. 支援NFC、電子錢包
    感覺很酷,但需要其他設備配合,還沒試過

另外,920推出時間畢竟比HD7晚了兩年,期間手機硬體規格標準已翻兩番,WP8操作起來更流暢,而WP App數量愈來愈多,加上WP8的開發環境比WP7又成熟一點(有了WinJS可用HTML5+JavaScript寫App,而且WP8 App的開發技巧與Win8 App高度互通),Windows Phone手機在使用與開發上都比先前順手,對於我這個不想多學其他語言卻想跨足行動裝置App開發的老人來說,是個好消息。


邀請iPhone加入WP8俱樂部

$
0
0

俱樂部是Windows Phone 8的新功能,使用者可以在連絡人區建立俱樂部(Room),只限受邀者(透過簡訊發送連結)才能進入,在其中可分享私人行事曆、群組聊天、相片或影片與記事。依據文件的說法:

使用 Windows Phone 7 或 iPhone 的人可以加入您的俱樂部,並且在他們的電話上設定分享的行事曆 (其他功能在 Windows Phone 8 上的效果最好,但只有在 Windows Phone 8 上才能夠使用群組即時聊天)。如需詳細資訊,請參閱 Windows Phone 7 和 iPhone 上的俱樂部。(注意:並非所有國家與地區都能使用 Windows Phone 7 與 iPhone 的俱樂部。)

官網還有關於iPhone使用俱樂部的進一步說明:

* 在 iPhone 上設定俱樂部行事曆

移至 iPhone 的設定,並新增您用來加入俱樂部的 Microsoft 帳戶作為電話的電子郵件帳戶 (您看到的可能是 Windows Live 或 Hotmail)。

注意
  • 只有 Windows Phone 8 才有俱樂部的群組即時聊天。使用 Windows Phone 7 或 iPhone 的俱樂部成員將無法參加。
  • 俱樂部的群組即時聊天使用 Messenger,因此當您加入俱樂部時,會成為其他成員的 Messenger 好友。(如果是使用 Windows Phone 7 或 iPhone,不能使用即時聊天,但是如果未來改用 Windows Phone 8,就能夠參加。)稍後若有新人加入俱樂部,您也會成為他們的 Messenger 好友。如果您離開或從俱樂部移除,除非您在 Messenger 上移除好友,否則仍然是其他成員的 Messenger 好友。您可以在電腦上使用 Messenger 應用程式執行此動作。

不過,我找不到Step by Step的圖文iPhone操作說明,那麼就效法神農嚐百草,親自試試吧! 在WP8上選取女王iPhone手機號碼,跪迎邀請女王加入安裝時預設的"家庭俱樂部",不久iPhone收到簡訊,以我的手機號碼發出,內容為: 我已經在稱為"家庭俱樂部"的手機上設定一個俱樂部。請立即加入以分享行事曆、相片、群組即時聊天和記事。另外還有了一個www .windowsphone.com/r/{GUID}的網址,點選開啟網頁,iOS Safari卻爆出以下錯誤:

看到**語言**,我一直想成瀏覽器的**JavaScript**支援有問題,換了Chrome結果依舊,無計可施之餘在WP7 Asia Developer FB社群提問,熱心的社群朋友回應將iPhone改為英語即可解決,這下我才恍然大悟,提示早在訊息中:

  • 注意:並非所有國家與地區都能使用 Windows Phone 7 與 iPhone 的俱樂部
  • 目前不支援您的網頁瀏覽器語言

此語言非彼語言,不支援的語言指的是中文,不是JavaScript啊!! 一切與語系有關,應是俱樂部網頁還不支援中文之故,將iPhone暫時切換成英語,果然就能順利在網頁用Microsoft帳號登入,加入俱樂部共享行事曆。

至此,大致能理解iPhone參與俱樂部的做法,是透過指定的Microsoft帳號存取俱樂部的行事曆,要進一步把行事曆整進iPhone,則要透過iPhone的Mail整合功能。在iPhone【設定】的【郵件、聯絡資訊、行事曆】選單,可以新增各大Mail平台的郵件帳號,包含Hotmail!

 

加入Hotmail帳號後,再選擇將Hotmail中的郵件、連絡人、行事曆及提醒事項同步到iPhone,之後WP8在俱樂部新增的行事曆事件便可自動同步到iPhone;iPhone也可新增事件到俱樂部再同步到WP8,只需留意事件的行事曆要選取俱樂部(下圖紅框所指)。

 

測試完畢~

小試SmartAssembly .NET混淆器

$
0
0

可輕易反組譯是採用中介語言(.NET, Java)平台的共有特性,也是實務應用的資安隱憂,面對這個問題,最有效的解決方案是 -- 混淆器(Obfuscator)。

混淆器的運作原因,是解析編譯好的DLL或EXE檔,將其轉換成執行結果相同的組件,差別在於私有類別、屬性、方法、欄位、參數名稱都已改到面目全非,難以閱讀理解;更進一步還可以打亂程式碼的排列流程(執行順序不變)、加密程式碼中的字串常數,讓反組譯的程式碼亂如咒語天書,令有意破解者卻步,至少要讓對方追程式追到流涕痛哭。天下沒有破解不了的程式,混淆器的目標在於逼迫絕大部分的破解者儘早放棄,即使被破解也要對方付出極其可觀(甚至難以想像)的高昂代價。

印象中,專業等級的.NET混淆器價格不斐,甚至如PreEmptive限定由銷售團隊洽談應用狀況再決定報價(嗯,有沒有人跟我一樣覺得毛毛的?),最近在估評其他軟體時看到Red Gate的SmartAssembly,標準版約1,000 USD、專業版約1,500 USD,價格貼近公司採購的其他開發元件,便決定實測看看。註: SmartAssembly有個有趣的開發者版本,不到200 USD,限制混淆後的程式只能在開發者自己的機器上執行,且程式會在七天後爆炸失效,目的是讓開發者測試混淆後的程式是否執行正常(某些混淆技巧可能導致程式不正常,記得要實測及適度調整混淆參數),故實務上全公司可共用一套標準版/專業版,再視需要採購開發版,若不用開發版,每次上測試台前再統一混淆亦是可行策略。

SmartAssembly的操作還算簡單,新增一個專案,選取DLL或EXE,設定混淆參數,建置就可以得到混淆版。

SmartAssembly可混淆對象包含: Windows Form, WPF, Console, Silverlight XAP, 程式庫(DLL), .NET Web Service, ASP.NET Web bin下的DLL… 等,原則上只要是.NET編譯出的組件(Assembly,DLL或EXE)都可以處理。除了混淆功能外,SmartAssembly還有一些"額外"功能,例如:

  1. Strong Name Siging
    為混淆後的元件加上強式簽名防止變造
  2. Automated Error Reporting
    程式當掉時蒐集錯誤資訊建立當機報告(背後會送到Red Gate的Web Service,並可透過SmartAssembly UI瀏覽,見前圖左側選單的Reporting項目) [參考]

    一旦程式出錯,會彈出以下對話框(透過SDK,對話框可以客製化)
  3. Feature Usage Reporting
    可以統計程式執行環境、各功能被使用的次數(也是透過Red Gate的Web Service蒐集,第一次執行前徵求使用者同意可傳送統計資訊較不會有爭議)
  4. Dependencies Merging
    將參照的DLL也合併起來混淆(可提高混淆程度),並非所有DLL都適用,3rd Party的元件可考慮改用嵌入(Embedding)做法
  5. Dependencies Embedding
    將參照DLL嵌入程式中(預設會加密、壓縮)但不進行混淆,第一次執行時解密、解壓縮還原,能減少部署檔案數目。
  6. Pruning
    移除沒用到的Metadata,例如事件名稱、屬性、方法參數等,讓程式碼更難解讀,也有助於減少程式檔體積。[參考]
  7. Resource Compression and Encryption
    資源壓縮及加密,第一次執行時還原,能縮小檔案體積。
  8. Other Optimization
    減少保留但未使用的記憶體、自動seal所有類別禁止繼承

回到重點 -- 混淆,SmartAssembly透過以下技巧讓反組譯的程式碼難以解讀,包含:

  1. 將類別、方法名稱改成看不懂的怪字
  2. 把類別的欄位名稱亂改一通,甚至讓不同類別用相同的欄位名稱
  3. 將類別A的方法搬到類別B底下,但風險較高,有時會出錯,宜斟酌使用
  4. Control Flow Obfuscation,把程式碼的先後順序調亂,但執行順序保持不變
    題外話: 以上四招我曾見過有人能徒手運用,直接寫出沒人看得懂的程式碼,堪稱"人體混淆"大絕 orz
  5. 動態Proxy: 一律透過執行期間產生的Proxy呼叫參照DLL,如此Assembly一旦被篡改,程式就會壞掉無法執行
  6. 字串加密: 預設.NET組件中的字串被直接儲存,檢視檔案二進位內容時就看得到,SmartAssembly支援將字串內容加密混淆
  7. 加上註記禁止MSIL Disassembler(ildasm)對檔案進行反組譯
  8. 如果有pdb的話,混淆StackTrace中出現的原始碼檔案路徑

接著來實測一番,以下是簡單的Console程式:

using System;
using System.Reflection;
using Boo;
using Foo;
 
namespace ObfuscationTest
{
class Program
    {
staticstring dllPath = @"d:\Newtonsoft.Json.dll";
staticvoid Main(string[] args)
        {
            Assembly asm = Assembly.LoadFile(dllPath);
            Type t = asm.GetType("Newtonsoft.Json.JsonConverter", true);
foreach (var pi in t.GetProperties())
            {
                Console.WriteLine(pi.Name);
            }
            BooClass boo = new BooClass() { Name = "Jeffrey" };
            FooClass foo = new FooClass() { Name = "Darkthread" };
            Console.WriteLine("{0} / {1}", boo.Name, foo.Name);
string res = Console.ReadLine();
if (!string.IsNullOrEmpty(res))
thrownew ApplicationException("My Bad! XD");
        }
    }
}

使用反組譯工具解析ObfuscationTest.exe檔,可以看到幾乎原汁原味的原始程式碼:

接著我們透過SmartAssembly惡搞一番,另外Build成ObfuscationTestSA.exe,再用反組譯工具試著打開。嗯,很好,程式碼已經被整到連他娘都不認得!


註: 我是選擇不加密字串常數,好不容易才透過程式碼中"Jeffrey"、"Darkthread"找到main()的所在位置,試過連字串都加密,我選擇直接投降~

評估之後,SmartAssembly看來是值得考慮的.NET混淆器解決方案。如果擔心專案中的程式經混淆會出問題,倒是可以先用試用版(功能完整,但編譯結果只能在有安裝程式的機器執行,七天後失效)實測評估後再考量。

王道歸來! 純jQuery版地址輸入輔助器

$
0
0

兩年前寫過 把Silverlight跟jQuery摻在一起做成瀨尿牛丸等級的地址輸入輔助器,將郵遞區號與地址路段資料XML封存在Silverlight XAP中,並在Silverlight以C# + LINQ實作縣市、鄉鎮市區、路名、郵遞區號關鍵字查詢,與jQuery自動完成整合,實現能快速操作的地址輸入欄位。兩年過去,已無人質疑HTML5將是網頁技術當今霸主的事實,於是興起地址輸入輔助器的改良計畫,決定移除對Silverlight的依賴,回歸純粹的JavaScript(jQuery),讓它不再只侷限Windows平台。

事涉數萬筆路名與郵遞區號對照資料,回頭改走AJAX解決是最簡便直覺的解法,但考慮資料登錄時會密集呼叫反覆查詢,將查詢運算移回網頁端乃是較有效率的做法,更重要的是,一旦擺脫對網路連線的依賴,離線作業才可能實現。

將AJAX查詢移至網頁端的首要挑戰,在於大量地址資料的儲存運用。為方便JavaScript存取,我將XML轉為更方便查詢的JSON格式,大小約1.05MB:

{
"Data": {
"106": {
"台北市": {
"大安區": [
"敦化南路1段",
"敦化南路2段",
...略...
"芳蘭路",
"浦城街"
        ]
      }
    },
"201": {
"基隆市": {
"信義區": [
"花源五街",
"義七路",

1.05MB說大不大,說小不小,但每次開網頁重新下載純粹是在浪費頻寬,於是我把腦筋動到HTML5的本機儲存機制。IE8起開始支援localStorage,其他瀏覽器最新版更不在話下,就連在行動裝置上也不用擔心不支援。加上幾句簡單的程式碼,將首次AJAX載回資料存入localStorage,之後就不用反覆下載。餘下的挑戰是將原本用LINQ、C#輕鬆搞定的多樣化查詢方式改用JavaScript實作,原本以為很難,實際動手後還好;JavaScript的object[propName]能簡單實現Dictonary<string, T>資料結構,加上本例中最大量的路名資料,筆數約4萬1千筆,即便跑迴圈掃一輪,時間亦在可接受範圍。就這樣,將原本C#寫的查詢函數,一一用JavaScript函數抽換掉,純jQuery的地址輸入輔助元件就誕生了!!

我放了一個線上展示給大家試玩,操作示範如下,歡迎大家回饋意見,近期會再以Open Source方式釋出。

最後,來證實本次修改有達成終極目標--跨平台!! 薑!薑!薑!薑! iPad嘛A通~

IE8 JSON.stringify()的Unicode編碼問題

$
0
0

接獲回報,前幾天釋出的地址輸入輔助元件在IE8上爆炸了,使用loalStorage儲存資料物件的JSON字串時,彈出"記憶體不足"錯誤。

追蹤後,發現問題源於IE8在JSON.stringify()轉換資料物件時,很機車地將中文字元全部換成UCN(Univeral Character Name,即\u1234、\u4e2d這種格式)。用IE Dev Tools即可印證明:

由於地址資料有滿滿的中文,在經過UCN轉換的蹂躪後,大小由1MB爆增到6MB,超出localStorage的容量上限,導致程式錯誤。

轉JSON字串時,應不應該對Unicode文字進行編碼呢? 依據JSON規格: Any UNICODE character except " or \ or control character 都應視為有效字元,故理論上中文字元並不需要Escape處理!


(圖檔來源: http://json.org/)

比較其他瀏覽器的做法:

IE9,沒轉!

IE10,沒轉!

Chrome,沒轉!

Firefox,沒轉!

很好,大家都不轉,就只有IE8會雞婆轉碼是怎樣?? (捏碎滑鼠) 算算這已是IE8原生JSON第三次搗蛋! (前科1前科2)

暫時想不到更好解法,我的因應之道是用var badJsonStringify = JSON.stringify("中").length > 3;偵測JSON.stringify()是否雞婆把"中"轉成"\u4e2d",如果是討厭的IE8,就不用原生JSON,改用json2.js提供的JSON.stringify();但因json2.js偵測有原生JSON物件時會自行迴避,我改了一個json2ext.js版本,將物名更名為JSON2提供服務。解法不怎麼漂亮,但問題有解,待有更好的點子時再改進。

檢測localStorage容量上限

$
0
0

在處理localStorage問題時,在stackoverflow發現好用的網頁工具 -- Test of localStorage limits/quota

使用方法很簡單,連上該網頁,網頁裡的JavaScript會嘗試在瀏覽器localStorage塞資料,並持續增加儲存資料的長度,直到放不下為止,即可得到localStorage的容量上限。簡單測試手邊有的瀏覽器:

  • IE 9 = 7100K
  • IE 8 = 4200K
  • IE 10 = 7000K
  • Chrome 26 = 2600K
  • Safari 5.1 = 2600K
  • Firefox 17 = 5200K

結論,考量跨平台,存入localStorage的資料以不超過2.6MB為宜。

Viewing all 2458 articles
Browse latest View live


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