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

關於 Decimal 小數尾數零

$
0
0

C#的實數型別有三種:float、double、decimal。其中 float、double 為浮點數,本站的老讀者們一定知道-「算錢用浮點,遲早被人扁」的道理,因此只要涉及金額計算,我一律改用 decimal 型別。前幾天,踩到 decimal 小數尾數零地雷一枚。

以下程式為例,大家猜猜結果為何?

class Program
    {
 
staticvoid Main(string[] args)
        {
float flt = 1.2300F;
            Console.WriteLine(flt);
double dbl = 1.2300D;
            Console.WriteLine(dbl);
decimal dcm = 1.2300M;
decimal cmp = 1.23M;
            Console.WriteLine(dcm == cmp);
            Console.WriteLine($"{dcm} vs {cmp}");
            Console.Read();
        }
    }

註:實數常值(Real Literal)後方需加上尾碼以明確宣告型別,F f 代表 float、D d 或不加尾碼代表 double、M m 則代表 decimal。

由結果可知,浮點數型別(float、double)不會保留小數尾數零,decimal 則可做到 1.23 與 1.2300 有別:進行 = 比對二者相等,但 ToString() 時小數尾數零可忠實還原。

事情發生在一段分別由 SQL 及 Oracle 取出對應資料比對的程式,目的在驗證兩邊資料庫是否一致。為簡化比對邏輯,我將所有欄位內容 ToString() 轉為字串,心想 SQL 與 Oracle 的 Schema 一致,欄位都是 DECIMAL(5,4) 且儲存數值相同,豈有寫入 decimal 後 ToString() 結果不同的道理?結果,真的被我遇上了。

用以下範例重現問題,在 SQL 與 Oracle 端分別將 1.23 存入 DECIMAL(5,4),再用 Dapper 讀取到 decimal:

staticvoid Main(string[] args)
        {
using (var cn = new SqlConnection(csSql))
            {
decimal d = cn.Query<decimal>(
@"
declare @d decimal(5,4);
set @d=1.23;
select @d;").Single();
                Console.WriteLine(d);
            }
using (var cn = new OracleConnection(csOra)) 
            {
                cn.Open();
                cn.Execute(
@"create global temporary table decimal_test (d decimal(5,4)) on commit preserve rows");
                cn.Execute("insert into decimal_test values (1.23)");
 
decimal d = cn.Query<decimal>("select d from decimal_test").First();
                Console.WriteLine(d);
                cn.Execute("truncate table decimal_test");
                cn.Execute("drop table decimal_test");
 
            }
            Console.Read();
        }

結果分別為 1.2300 與 1.23。SQL Client 讀取 DECIMAL(5,4) 轉入 decimal 時後方會補足 0 到精確位數,而 ODP.NET 不會,二者的行為差異造成讀取 decimal 的小數尾數零數目不同,不影響大於小於等於比對,遇到 ToString() 轉字串,便會得到不同結果。

至於要怎麼去除 decimal ToString() 夾帶的尾數零,有幾種做法

  1. 如果確定要保留小數位數上限,可以寫成 ToString("#.####")。缺點是若 # 個數小於實際小數位數會被四捨五入影響精確度。若求保險,小數點後寫上 28 個 # 肯定安全。(decimal 精確度上限為 29 位)
  2. 用 ToString("G29") 轉為科學記號,但要求 29 位精準位數,成為位數足又不會出現 E 的幾次方的偽科學計號。缺點是 0.00001 這類微小數會被轉成 1E-05。
  3. 在 Stackoverflow 看到奇妙解法,decimal 除上 1.00000…(29個0):
    publicstaticdecimal Normalize(thisdecimalvalue)
    {
    returnvalue/1.000000000000000000000000000000000m;
    }
       
    寫成擴充方法,1.2300m.Normalize() 尾數零就會清光光。

簡要心得

  • decimal 會保存小數尾數零,float、double 等浮點型別不會
  • 小數尾數零不影響大於等於小於比對,但會影響 ToString() 結果
  • SQL Client 與 ODP.NET 讀取 DECIMAL(m, n) 欄位寫入 decimal 時對於小數尾數零的處理原則不同
  • 去除 decimal 小數尾數零有幾種做法:ToString("#.####")、ToString("G29") 以及奇妙的 Normalize() 方法
  • 政令宣導時間:「算錢用浮點,遲早被人扁」。早晚複誦,永誌不忘~

Windows 10 磁碟使用率持續 100%

$
0
0

無意在工作管理員發現 C 碟的啟用時間持續停在 100% 降不下來,但讀寫資料量不高,此種狀況發生在 SSD 上格外讓人心驚。

開啟資源監視器查到可疑檔案讀寫活動:C:\Windows\Temp\WPR_initiated_DiagTrackAotLogger_WPR System Collector.etl,以每秒 1MB 左右的速度不斷寫入資料,長期高居磁碟讀寫量榜首,是頭號疑犯。

使用關鍵字爬文找到兩篇相關討論:12,都是這兩天才有的新貼文,不少人遇到相同情況,眾人推測與 WPR 服務、DiagTrack 服務以及最近的 Windows 更新有關聯。確實原因不明,目前找到的 Workaround 是停用 DiagTrack 服務:

實測停用 DiagTrack 服務,我的 C 碟使用率立即恢復正常水準。

猜想可能是近期 Windows 更新隱藏的 Bug,為防止磁碟被搞壞,決定先停用 DiagTrack 以為上策。以上發現供遇到類似狀況的朋友參考。

2016-12-13 補充:發現手動關閉 DiagTrack 服務後仍會自行重啟,需停用「Connected User Experiences and Telemetry」服務才可一勞永逸。

2016 飛龍盃烘爐地馬拉松

$
0
0

去年參加第一屆感覺不錯,今年順理成章連一拉一。五點半起跑,五點出頭抵達南山福德宮會場還有夜景可看。

貴賓致詞完畢,準時出發。

路線與去年相同,何時上坡何時下坡心裡有數,少了新奇感,但多了幾分篤定。

起跑後以兩公里陡下揭開序幕,緊接華夏科技大學旁一百多公尺高的小山頭陡上又陡下,接著轉進河濱。

市區賽道左變右拐還得穿越不少巷弄,所幸指引義工部署密集,不然應該會迷路迷到地老天荒。

超沒多久遇到烏來馬曾海放我的印地安姐,今天配合「飛龍」主題穿了龍袍,頭上還有條龍~不過我今天比較爭氣,緊咬近半馬才不支脫落。

老天幫忙,多雲微晴偶有毛毛雨,是再理想不過的跑步天氣。或許是天氣宜人,或許是吃錯藥,原本遠足等級的山路馬,我跑得好賣力,認真到自己都覺得害怕。

賣力歸賣力,還是少不了胡亂照相一番,也為萬一跑不好預留下台階。(要不是一路照相,我不會只跑出這種成績… 笑)

 

 

(兩位唐代古人貌似在檢查警用手電筒是否有電池尚未取出,但手臂比例怪怪DER)

 

 

 


好大一群單車部隊,領隊精神講話兼指導跟車技巧中

跑得太認真,竟對補給沒啥印象,熱咖啡跟薑母茶蠻特別,意外發現薑母茶跟馬拉松挺搭的。

好驚人的 Cosplay,熊本熊來著,雖然只跑 10K,還是強!

回到華夏科技大學旁的小山頭,近 30% 坡度讓人燃起熊熊的 WTF 感~

 

在小山頭頂倒數第二補給站幸運吃到限量版隱藏補給品:烤山豬肉跟小米酒(一大杯)的啦~開心!感謝志工。

終於看到土地公在山頂對我微笑了,最後 1.5K 爬 200 米,看準時間一鼔作氣,最後以 4:39:39.39 完賽。

總升降近八百公尺的山路全馬,我竟然跑出 43X!我有這麼快?我很滿意。(感謝土地公保佑)

趁著早跑完人少,趕搭接駁車到南勢角捷運站,坐捷運回政大要繞一大圈,我想到好點子:薑!薑!薑!薑~

破山路馬 PB 之餘,順便解除跑 42 公里+ 9 公里單車的成就。

新北市前半小時免費,U Bike 車資:10 元;完成 51K 偽雙鐵,無價!

小試 JavaScript Promise

$
0
0

非同步邏輯是寫 JavaScript 逃不掉的複雜課題,古早流行的做法是傳入 Callback 函式當參數,待特定作業完成再呼叫,缺點是串接程序一旦變多,就會出現波動拳式排版,寫到渾然不知身處夢境第幾層:

asyncJob1(function() {
//Callback 函式: asyncJob1 完成後呼叫
//......
    ayncJob2(function() {
//Callback 函式: asyncJob2 完成後呼叫
//......
        ayncJob3(function() {
//Callback 函式: asyncJob3 完成後呼叫
//......
            ayncJob4(function() {
//Callback 函式: asyncJob4 完成後呼叫
//......
                ayncJob5(function() {
//Callback 函式: asyncJob5 完成後呼叫
//......
                });
 
            });
 
        });
 
    });
});

後來,使用 Promise 串連非同步邏輯漸成主流,作業成功或失敗 Callback 寫在 Promise 物件 done()/then()/fail() 等方法,另外還有 always() 指定不論成功失敗都要執行的程序。各大程式庫與框架(jQuery 1.5+、Angular)都有自己實做的 Promise 版本,原理大同小異,要串接循序執行的連續作業是小事一椿,就以 jQuery 為例,上述程式碼可以改寫美化如下:(註:1.8 起建議以 then() 取代 pipe())

var dfd = jQuery.Deferred();
dfd.resolve()
    .then(function() {
return asyncJob1();
    })
    .then(function() {
//Callback 函式: asyncJob1 完成後呼叫
//......
return asyncJob2();
    })
    .then(function() {
//Callback 函式: asyncJob2 完成後呼叫
//......
return asyncJob3();
    })
    .then(function() {
//Callback 函式: asyncJob3 完成後呼叫
//......
return asyncJob4();
    })
    .then(function() {
//Callback 函式: asyncJob4 完成後呼叫
//......
return asyncJob5();
    })
    .done(function() {
//Callback 函式: asyncJob5 完成後呼叫
//......
    });

理論上 jQuery 或 Angular 實做的 Promise 已經很夠用,但自從 Promise被納入 ECMAScript 6 規範,意味著新一代瀏覽器都內建支援,不再需要第三方程式庫,未來使用標準 Promise 處理非同步邏輯將成主流,又有新東西要學了。

ES6 的 Promise 依循 Promise/A+規範,寫法跟我慣用的 jQuery Deferred 有點出入,搞個對照,熟悉 Promise 寫法是必要的。(註:如果你被 ECMAScript 6、ES6、ES2015 等術語搞到頭很昏,可以參考這篇

程式操作以上,程式用 Promise 處理非同步流程,按下 Resolve 或 Reject 彈出不同訊息並停用兩顆按鈕。如果用 jQuery + TypeScript,寫法如下:

module test1 {
var dfd = jQuery.Deferred();
    $("#btnResolve").click(() => dfd.resolve());
    $("#btnReject").click(() => dfd.reject());
var task = dfd.promise();
 
    task.done(
        () => {
            alert("Button Resolve Clicked!");
        })
        .fail(
        () => {
            alert("Button Reject Clicked!");
        })
        .always(() => {
            $("button").prop("disabled", true);
        });
}

建立一個 jQuery.Deferred,呼叫 promise() 產生 Promise 物件,在 done()、fail()、always() 分別掛上事件,呼叫 resolve() 將觸發 done()、呼叫 reject() 則會觸發 fail(),而不管 resolve() 或 reject() 最後都會觸發 always()。

來看看改成 ES6 Promise 要怎麼寫:

module test2 {
    declare var Promise: any;
 
var task = new Promise((resolve, reject) => {
        $("#btnResolve").click(() => resolve());
        $("#btnReject").click(() => reject());
    });
 
    task.then(
        () => {
            alert("Button Resolve Clicked!");
        })
        .catch(
        () => {
            alert("Button Reject Clicked!");
        })
        .then(
        () => {
            $("button").prop("disabled", true);
        });
}

原理大同小異,主要差別在 resolve、reject 在 Promise 建構時傳入,而 resolve() 觸發 then()、reject() 觸發 catch(),要實現 jQuery Deferred always() 則可在 catch() 後方再加一個 then()。

開啟 Chrome、Edge 或 Firefox,可以觀察到 ES6 Promise 版網頁的操作效果跟 jQuery Deferred 版完全相同。等等,那 IE …

是的,即便是 IE11 也沒內建 Promise!該怎麼辦?

網路上有不少 Promise Polyfill,例如:ES6 Promise、BludbirdJS,Promise 的原理不複雜,自己寫一個也非不可能,MSDN Blog 甚至有一篇範例,但應該很少人會想為此造輪子。研究後,我找到引用 Lightweight ES6 Promise polyfill的簡單做法,下載 Github 上的 promise.js 或 promise.min.js,網頁加上 <script src="Scripts/promise.js"></script>就一切搞定。如果你身為要考慮 IE678 的悲情攻城獅(老師,金包銀前奏請下),又偏偏不肯向命運低頭誓死要挑戰在老 IE 用 Promise,由於 catch 對老 IE 是保留字,.catch(…) 得改寫成 ["catch"](…) 才不會出錯。(話說回來,跨瀏覽器又要考慮 IE,放著 jQuery 不用硬要橫柴入灶是為哪椿呢?)

最後補充一點,jQuery 3 調整了 jQuery.Deferred 以符合 Promise/A+,若 jQuery 已升級到 3.0,Deferred 可以當成標準 Promise 使用,遇到要求型別為 Promise 的整合應用可以通行無阻,火力升級。

JavaScript 非同步程式革命-async、await 與 TypeScript 2.1

$
0
0

.NET 4 的 Task大幅簡化多執行緒程式碼複雜度,而 4.5 推出的 async/await (參考)則讓非同步程式的寫法更精簡。(註:在博客園找到一篇從 Task、ThreadPool 談到 await 的廣泛介紹,可以一讀)

網頁開發老鳥都知道,JavaScript 端的非同步邏輯不比 C# 簡單,複雜起來會讓人寫程式寫到嘔出幾十兩鮮血。於是在前端也看到類似 Thread、Task、async/await 的發展史:最早只能靠 Callback、setTimeout 苦手硬刻,ES6 有了 Promise, 到了 ES7,async/await 也出現了。撤底搞懂 ES6 Promise、Generator,ES7 async/await 是很有成就感的事(如果你想試試的話: 1 2),不過如果你跟我一樣,要寫前端但不想走在最前端,讓 TypeScript 負責封裝背後的複雜性,輕鬆下指令也是很好的選擇。

TypeScript 早在 1.7 版就支援 async 與 await,但有個大罩門,編譯出來的 JavaScript 只能在 ES2015/ES6 目標平台執行(參考:ES 規格檢查表),連 IE11 都不支援,對必須考量 IE 相容的開發者來說,英雄無用武之地,差不多是這種感覺:

前陣子推出的 TypeScript 2.1 有個天大好消息,async / await 向下相容 ES5 目標平台,IE9 嘛A通,都到了這份兒上,還有不學不用的道理?

先來看個範例:

<%@ Page Language="C#" %>
 
<scriptrunat="server">
void Page_Load(object sender, EventArgs e)
    {
var m = Request["m"];
if (m == "guid")
        {
            System.Threading.Thread.Sleep(2000);
            Response.Write(Guid.NewGuid().ToString());
            Response.End();
        }
elseif (m == "time")
        {
            System.Threading.Thread.Sleep(1000);
            Response.Write(DateTime.Now.ToString("HH:mm:ss"));
            Response.End();
        }
    }
</script>
 
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
</head>
<body>
<button>Call AJAX</button>
<div class="msg"></div>
<script src="Scripts/jquery-3.1.1.js"></script>
<script src="Scripts/promise.js"></script>
<script src="Scripts/async.js"></script>
</body>
</html>

網頁流程為按下按鈕後先呼叫 ?m=guid 由 Server 端取得 Guid,再呼叫 ?m=time 取回時間,將結果顯示在<div>上。若以 jQuery.get 實作,程式如下:

var div = $(".msg");
var button = $("button");
    button.click(() => {
        button.prop("disabled", true);
        $.get("/Lab.aspx?m=guid").done((guid) => {
            div.text(guid);
            $.get("/Lab.aspx?m=time").done((time) => {
                div.text(div.text() + " " + time);
                button.prop("disabled", false);
            });
        });
    });

執行結果:

接著我們用 async/await 來改寫一番:

var div = $(".msg");
var button = $("button");
 
    async function doAjaxJob() {
        button.prop("disabled", true);
        await $.get("/Lab.aspx?m=guid").then(guid => div.text(guid));
        await $.get("/Lab.aspx?m=time").then((time) => {
            div.text(div.text() + " " + time);
            button.prop("disabled", false);
        });
    }
 
    button.click(doAjaxJob);

跟 C# 做法類似,function 加上 async 後,在其中便可使用 await 等待非同步程式結束再往下執行,await 函式傳回的必須是 Promise,而先前提到 jQuery 3 特意調整符合 Promise/A+ 規範的優勢也在此展現,$.get() 傳回值為 Promise 可直接搭配 await 使用,寫法簡潔易讀許多。

async 寫法在 TypeScript 2.1 以前有目標平台限制,低於  ES6(ES2015)會出現錯誤訊息。

安裝TypeScript for VS2015 2.1+版可以解除限制,編譯出相容 ES5 的程式碼。但由於 ES5 欠缺 Promise 定義,還需要使用 NuGet 安裝 es-promise 或其他包含 Promise 的 TypeScript 定義檔,才能順利編譯。

再稍稍走深一點,切換 ES 版本來觀察 TypeScript 如何在 ES5 及 ES6 實現 async / await:

TypeScript 在 ES5 實現 async、await 的祕密如下:

很可怕,不要問。不過也不需要懂(有興趣搞懂可以看這篇),有了 TypeScript 2.1, 我們放心寫 async / await 就好,就算要相容 IE9-11 也不怕,再加上先前介紹過的 Template String,教人怎能不愛它?

最後用一個美妙的 async/await 範例收尾,大家知道要怎麼在 JavaScript 實現 Thread.Delay(…) 嗎?Yes,setTimeout(),如果要做到一秒後印1s,再兩秒後印3s,再3秒後印6s,程式大約會像這樣:

var div = $(".msg");
var button = $("button");
 
function doAjaxJob() {
        button.prop("disabled", true);
        setTimeout(() => {
            div.text("1s");
            setTimeout(() => {
                div.text("3s");
                setTimeout(() => {
                    div.text("6s");
                    button.prop("disabled", false);
                }, 3000);
            }, 2000);
        }, 1000);
    }
 
    button.click(doAjaxJob);

讓我們用 await 改寫看看:

var div = $(".msg");
var button = $("button");
 
    async function delay(duration: number) {
returnnew Promise((resolve, reject) => {
            setTimeout(resolve, duration);
        });
    }
 
    async function doAjaxJob() {
        button.prop("disabled", true);
        await delay(1000);
        div.text("1s");
        await delay(2000);
        div.text("3s");
        await delay(3000);
        div.text("6s");
    }
 
    button.click(doAjaxJob);

看到 JavaScript 可以 Thread.Sleep,程式還這麼清爽,我感動到都快哭了~ 快升級 TypeScript 2.1 感受一下吧!

【茶包射手日記】打不開的PDF檔與檔案格式鑑定

$
0
0

接獲報案,某套表程式忽然故障導致產出的PDF檔案無法開啟。

檢視查檔案內容如下,二進位內容當然看不懂,但由表頭判斷一定不是 PDF。檔案一開始的「俵」跟「遄」字元經 Google 在網際網路上發現不少兄弟姐妹,大致可知這是 Office 相關格式,但試著將副檔名改成 .doc、.docx、.rtf 都無法開啟。

爬文找到一個神奇的小工具-Marco Pontello's TrID,它搜羅整理了 7886 種檔案格式特徵,能掃瞄檔案內容推測檔案類型。到網站下載工具包含主程式 trid.exe 以及 檔案特徵資料庫 triddefs.trd,下個指令一秒就知結果:

如上圖所示,鑑定結果檔案很可能是 .wps 檔(Microsoft Works的文件檔),使用 Word 2010 開啟舊檔,切換檔案類型為「Works 6-9 文件(*.wps)」,果然順利開啟檔案。

到此,案情可定調為「原本產生 PDF 的套表程式,不知何故產出檔案格式變成 WPS」,這… 未免也太懸疑?

同事深入探訪後找出原因:套表程式原本搭 Word 2007 執行,前些時候配合另一套古老系統需求在同台主機安裝了 Word 2003。由於舊版軟體不知道新版軟體的存在,先裝新版再裝舊版可能造成共用元件被不當覆寫。而問題發生時點跟安裝 Word 2003 時間大致吻合,推測是元件覆寫導致檔案格式參數錯亂,才讓 PDF 變成 WPS。

問題在重新安裝 Word 2007 後排除,Case Closed,收工。

核武級ODP.NET版本暴力破解工具

$
0
0

最近又遇到 ODP.NET 版本問題。(警告:本文涉及邪門歪道雞鳴狗盜之技,正義魔人與衛道人士請自行迴避)

古老 ASP.NET 網站參照 ODP.NET 9207 版,移到 x64 平台必須改用新版 ODP.NET,而 ODP.NET 存在版號從 9.2 10.1 降回 2.102 的鬼問題,新版號比舊版號數字小在某些情況下會讓「bindingRedirect 大絕」破功。很無奈,必須調整參照版號才能解決,重新取原始檔編譯太麻煩,於是我找到 ildasm 反組譯成 MSIL 程式碼,修改參數版號再 ilasm 編譯回 DLL 的奧步妙招,後來更進一步寫了自動化懶人工具

多年後再遇老茶包,遇上小麻煩。由於古老網站採用 .NET 2.0,發現在 Windows 8.1 平台上就算用 .NET 2.0 SDK ilasm 編譯出的 DLL 還是 .NET 4 版本,得翻出老機器用 VS2008 開發環境處理才能產出 .NET 2.0 版本。另一方面,網站為 Web Site Project 使用部署專案編譯成一個資料夾一顆 DLL,換句話說,總共有數十個 DLL 要處理。沒耐性的我又動起歪腦筋,索性不要 ildasm / ilasm 了,直接將 DLL 裡的 ODP.NET 版號資料改掉更快。

於是,花十分鐘寫了以下工具,用二進位資料比對的方法 1 秒鐘內把數十個 DLL 的 ODP.NET 版號從 9.2.0.700 換成 2.112.3.0!與之前一個個 DLL ildasm/ilasm 的繁瑣過程相比,威力直逼核武等級呀~

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace OdpNet9207RefFixer
{
class Program
    {
staticvoid Main(string[] args)
        {
foreach (var file in Directory.GetFiles("x:\\src-bin", "*.dll"))
            {
byte[] buff = File.ReadAllBytes(file);
                var idx = 0;
                var foundCount = 0;
while (idx < buff.Length - 8)
                {
if (buff[idx] == 9 && buff[idx + 1] == 0 &&
                        buff[idx + 2] == 2 && buff[idx + 3] == 0 &&
                        buff[idx + 4] == 0 && buff[idx + 5] == 0 &&
                        buff[idx + 6] == 0xbc && buff[idx + 7] == 0x02
                        )
                    {
                        buff[idx] = 2;
                        buff[idx + 2] = 112;
                        buff[idx + 4] = 3;
                        buff[idx + 6] = 0;
                        buff[idx + 7] = 0;
                        foundCount++;
                    }
                    idx++;
                }
if (foundCount == 1)
                {
                    File.WriteAllBytes(file.Replace("src-bin\\", "fixed-bin\\"), buff);
                }
            }
        }
    }
}

這個做法的風險在於誤判,若好死不死 DLL 裡程式碼或資源的二進位資料剛好組成 0x090002000000bc02 就會被錯換,因此我加了一道保險,若在 DLL 中發現一處以上吻合,就不進行置換。所幸 9.2.0.700 這串內容夠特殊,一般程式內容撞上的機率不高,故不再投資心力閃避,而是補上事後使用 JustDecompile 檢查參照版號做為第二道防線,降低誤改風險。

就這樣,原本預估要花個把小時處理的 ODP.NET 參照修改問題,花了幾分鐘便搞定。

奧步雖然可恥但有用~ XD

用 TypeScript await 讓操作確認流程回歸直覺

$
0
0

上回提到 TypeScript 2.1 讓 ES5 平台也能支援 async、await,形同 JavaScript 非同步程式的一場革命,衝著這點大家都該認真考慮改用 TypeScript。但 async、await 當真如此神奇?想想,上回漏講一個 await 殺手級應用案例,說服力有點弱,趕緊補上。

大家小時侯都有寫過這種需求:網頁進行更新、刪除操作前跳出對話框請使用者三思,回答「取消」可以反悔取消,回答「確定」才正式執行。在那個古老而純真的年代,只要寫一行就搞定:

if (confirm("下好離手,您確定要洗頭?")) { …倒洗髮精… }

等待 window.confirm() 傳回結果,依傳回值 true 或 false 決定後面流程,程式邏輯動線清楚明瞭。

但 confirm() 有幾個缺點,第一是畫面配置、文字、按鈕樣式由瀏覽器控器無法客製;第二點是等待使用者輸入的當下 JavaScript 執行緒將完全凍結,以 JavaScript 驅動的網頁元素互動或是 AJAX 連線都陷入失效狀態;第三,一旦呼叫 confirm 後我們就失去主導權,因此不可能實現「逾時未回應視為取消」,若使用者不回應,網頁只能地老天荒被卡著。

用個實例示範:

<!DOCTYPEhtml>
<html>
<body>
<button>重新倒數</button>
<divclass="cnt-down"></div>
<scriptsrc="Scripts/jquery-3.1.1.js"></script>
<script>
var countDown = 100;
var hnd = setInterval(function () {
if (countDown == 0) {
                alert("時間到!");
                clearInterval(hnd);
            }
else
                $(".cnt-down").text(countDown--);
        }, 1000);
        $("button").click(function () {
if (confirm("確定要重新開始倒數?"))
                countDown = 100;
        });
</script>
</body>
</html>

如以下展示,confirm 彈出「確定要重新開始倒數?」後,故意等幾秒才按取消,這段期間由 setInterval 驅動的倒數是停止的,直到按下取消才繼續。至於要做到「使用者五秒沒回應就取消 confirm」?黑洗謀摳零A代擠。

要克服上述缺點,就只能走上用 HTML 元素打造對話框(或使用 jQuery confirmKendo UI等現成程式庫)的路。在非同步模式下想要依使用者按不同鈕執行不同邏輯,需仰賴 jQuery Deferred 或 Promise,並將確認及取消邏輯分別寫在 done()/fail() 或 then()/catch():(參考:使用自訂確認對話框取代window.confirm

<!DOCTYPEhtml>
<html>
<head>
<title>Confirm Example</title>
<metacharset="utf-8"/>
</head>
<body>
<button>重新倒數</button>
<divclass="cnt-down"></div>
 
<divclass='dialog'style='display:none'>
<divclass="my-cnfrm-diag"style='border: 1px solid blue; padding: 12px;'>
<divclass='m'></div><br/>
<inputtype='button'value='是'/>
<inputtype='button'value='否'/>
</div>
</div>
 
<scriptsrc="Scripts/jquery-3.1.1.js"></script>
<script src="Scripts/jquery.blockUI.js"></script>
<script>
function myConfirm(msg) {
var df = $.Deferred(); //建立Deferred物件
 
//使用BlockUI顯示對話框
            $.blockUI({
                message: $(".dialog").html(),
                css: { width: "50%" }
            });
//關閉對話框並傳回結果
function close(result) {
                $.unblockUI(); //將對話框移除
                clearTimeout(hnd); //取消自動關閉排程
if (result) df.resolve(); //使用者按下是
else df.reject(); //使用者按下否
            }
 
//若使用者未回應,五秒後自動關閉
var hnd = setTimeout(function () {
                close(false);
            }, 5000);
 
var $div = $(".my-cnfrm-diag");
            $div.find(".m").text(msg); //設定顯示訊息
 
//加上按鈕事件
            $div.on("click", "input", function () {
                close(this.value == "是");
            });
 
//傳回Promise
return df.promise();
        }
</script>
<script>
var countDown = 100;
var hnd = setInterval(function () {
if (countDown == 0) {
                alert("時間到!");
                clearInterval(hnd);
            }
else
                $(".cnt-down").text(countDown--);
        }, 1000);
 
        $("button").click(function () {
            myConfirm("確定要重新開始倒數?")
                .done(function () {
                    countDown = 100;
                });
        });
</script>
</body>
</html>

示範如下,顯示確認對話框的同時,數字仍會繼續倒數,第一次帶出對話框時,故意等五秒不操作,可以看到對話框被逾時機制自動關閉,直接認定取消;而第二次按下「是」時會觸發 jQuery Promise.done() 所設定的邏輯,將 countDown 重設回 100。

一切都符合需求,但確認後的執行動作必須寫在 myConfirm(…).done(function() { … }) 裡,不如過往直覺,要是能寫成 if (myConfirm(…)) { … } 就更完美了!

讓 await 登場實現我們的願望吧!

將程式搬進 TypeScript,TypeScript 是 JavaScript 的超集合,JavaScript 程式貼進 TypeScript 裡不用修改也能運作。myConfirm 部分先不動,先在最後一段動點手腳:

function myConfirm(msg) {
var df = $.Deferred(); //建立Deferred物件
 
//使用BlockUI顯示對話框
            $.blockUI({
                message: $(".dialog").html(),
                css: { width: "50%" }
            });
//關閉對話框並傳回結果
function close(result) {
                $.unblockUI(); //將對話框移除
                clearTimeout(hnd); //取消自動關閉排程
df.resolve(result);//傳回true/false
            }
 
//若使用者未回應,五秒後自動關閉
var hnd = setTimeout(function () {
                close(false);
            }, 5000);
 
var $div = $(".my-cnfrm-diag");
            $div.find(".m").text(msg); //設定顯示訊息
 
//加上按鈕事件
            $div.on("click", "input", function () {
                close(this.value == "是");
            });
 
//傳回Promise
return df.promise();
        }
 
var countDown = 100;
var hnd = setInterval(() => {
if (countDown == 0) {
        alert("時間到!");
        clearInterval(hnd);
    }
else
        $(".cnt-down").text(countDown--);
}, 1000);
 
$("button").click(async () => {
if (await myConfirm("確定要重新開始倒數?")) {
        countDown = 100;
    }
});

button click 事件稍做修改,在匿名函式 () => { … } 前方加上 async 修飾,以便在其中使用 await。而 await myConfirm(…) 將等待 Promise Resolve 或 Reject 才繼續執行,讓邏輯回歸 if (confirm(…)) { … } 般的單純直覺。

還有一個地方要小調,await myConfirm() 傳回結果將等於 Resolve() 傳回值,若遇上 Reject() 會得到 undefined 並產生 "Uncaught (in promise)" 錯誤,故 myConfirm 裡原本 if (result) df.resolve() else df.reject() 寫法改為一律 Resolve(),再依傳回值 true/false 區分使用者按是或按否。小事一椿,改成 df.resolve(result) 就搞定。

從單純的 if (confirm(…)) { … } 演進好用但寫法不直覺的 myConfirm(…).done(function() { … }),回歸 if (await myConfirm(…)) { … } 的清楚流程 ,大師兄回來了,謝謝 TypeScript await!


Visual Studio 擴充套件自動停用問題

$
0
0

最近發生 SCSS無法自動編譯的狀況,查看 Extension and Updates,問題出在 Web Compiler被停用。

手動重新啟用後一切功能如常,原以為是一時系統秀逗,但接連發生好幾次才感覺不對勁。觀察發現,即時手動啟用,重開 Visual Studio 再開,Web Compiler 又是停用狀態。進一步檢查,則發現另一套件 Bundler & Minifier 也有相同狀況。

爬文找到一篇發生在 Web Essentials 的類似案例,似乎是擴件套件 Cache 錯亂重覆造成的。照著作者做法用 devenv /log 開啟 Visual Studio Log 模式,在 %APPDATA%\Microsoft\VisualStudio\14.0\ActivityLog.xml 記錄中找到一堆 ID 相同導致套件無法載入的錯誤訊息:

2931 ERROR Extension will not be loaded because an extension with the same ID '148ffa77-d70a-407f-892b-9ee542346862' is already loaded at C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\1KE1MKUT.YWG\...
          C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\AVOVISND.T3V\   Extension Manager 2017/01/01 09:12:06.894
2932 ERROR Extension will not be loaded because an extension with the same ID '148ffa77-d70a-407f-892b-9ee542346862' is already loaded at C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\1KE1MKUT.YWG\...
          C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\CY10ZHN0.GEN\   Extension Manager 2017/01/01 09:12:06.894
2933 ERROR Extension will not be loaded because an extension with the same ID 'a0ae318b-4f07-4f71-93cb-f21d3f03c6d3' is already loaded at C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\01YIXUDP.4S4\...
          C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\DGLLFOVT.UKJ\   Extension Manager 2017/01/01 09:12:06.894
2934 ERROR Extension will not be loaded because an extension with the same ID '148ffa77-d70a-407f-892b-9ee542346862' is already loaded at C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\1KE1MKUT.YWG\...
          C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\LW0SFC1S.34X\   Extension Manager 2017/01/01 09:12:06.894
2935 ERROR Extension will not be loaded because an extension with the same ID 'a0ae318b-4f07-4f71-93cb-f21d3f03c6d3' is already loaded at C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\01YIXUDP.4S4\...
          C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\NDMEKS5M.CYT\   Extension Manager 2017/01/01 09:12:06.894
2936 ERROR Extension will not be loaded because an extension with the same ID 'a0ae318b-4f07-4f71-93cb-f21d3f03c6d3' is already loaded at C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\01YIXUDP.4S4\...
          C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\R24FBB2G.B1A\   Extension Manager 2017/01/01 09:12:06.894
2937 ERROR Extension will not be loaded because an extension with the same ID 'a0ae318b-4f07-4f71-93cb-f21d3f03c6d3' is already loaded at C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\01YIXUDP.4S4\...
          C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\UHHDU3FN.P1K\   Extension Manager 2017/01/01 09:12:06.894
2938 ERROR Extension will not be loaded because an extension with the same ID '148ffa77-d70a-407f-892b-9ee542346862' is already loaded at C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\1KE1MKUT.YWG\...
          C:\USERS\jeffrey\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\14.0\EXTENSIONS\WCSBI30Y.CVD\   Extension Manager

檢查各資料夾內容分別為:

*1KE1MKUT.YWG\ WebCompiler
*01YIXUDP.4S4\ Bundler and Minifier
AVOVISND.T3V\ Bundler and Minifier
CY10ZHN0.GEN\ WebCompiler
DGLLFOVT.UKJ\ Bundler and Minifier
LW0SFC1S.34X\ WebCompiler
NDMEKS5M.CYT\ Bundler and Minifier
R24FBB2G.B1A\ Bundler and Minifier
UHHDU3FN.P1K\ Bundler and Minifier
WCSBI30Y.CVD\ WebCompiler

由此推論我遇到文章所說的狀況-系統存在多個重複的 WebCompiler 與 Bundler and Minifier 資料夾,Visual Studio 啟動時檢查有誤導致擴充套件被強制停用。依照文章裡的做法,連續移除套件直到它從清單消失(在我的案例 Bundler 有六份,Web Compiler 有四份,故連移了十次才移完),再重新安裝一次,問題排除。

後來進一步發現擴件套件資料夾重複應是常見問題,已有善心人士寫好找出重複擴充套件並自動刪除的潛盾機小工具,可以瞬間排除問題,如果懶得反覆手動移除重安裝可考慮使用工具修正問題。

onbeforeunload 事件不再支援自訂訊息

$
0
0

要防止使用者網頁輸入資料時誤按超連結或回上頁鍵,來不及儲存(送出)就離開,有個古老技巧是攔截 onbeforeunload 事件,使用 return "…" 傳回提醒文字,讓使用者有機會反悔,選擇停留在原頁面。(參考:如何避免使用者在特定網頁表單在未經送出時意外離開

這招用了多年,今天在寫某個設定網頁時卻發現 Chrome 沒有顯示我在 onbeforeunload 傳回的提醒文字,而是出現「系統可能不會儲存你所做的變更」字樣。

原以為是寫法有誤不符 Chrome 要求,爬文後發現背後有故事:

Chrome 從 51 版(2016 年 4 月)起取消 onbeforeunload 對話框顯示自訂訊號功能,理由是防止自訂訊息被用於詐騙用途。

規格討論裡提到當代瀏覽器的 onbeforeunlod 訊息有兩大用途:

  1. 防止使用者不慎遺失編輯中的資料
  2. 詐騙使用者
    (來個惡搞範例好了 :P)

目標是保留第一種用途並防止第二種情境發生,不開放自訂訊息是最直接有效的做法。瀏覽器顯示「離開網頁可能遺失資料」之類的萬用訊息,明確度及提醒效果當然比不上自訂訊息,但還是能多少發揮提醒作用,這算是為了安全所做的折衷。討論過程不乏反對的意見,但 Firefox 在更早之前就已這麼做,提供強而有力的助攻。

借用微軟的 onbeforeunload 範例,我測試了 IE11、Edge、Firefox 及 Opera,目前只剩 IE 與 Edge 還支援 onbeforeunload 自訂訊息:


Firefox

Opera

IE11

Edge

結論

當代瀏覽器已取消 onbeforeunload 自訂訊息功能,改以通用文字提醒資料可能遺失,設計網頁時應留意此一改變。

ODP.NET 發行者原則檔經驗一則

$
0
0

之前處理過一個鳥問題,使用 ODP.NET 12.1 連線 Oracle Server 10.2.0.4 時無法參與分散式交易,傳回「Unable to enlist in a distributed transaction /無法列於分散式交易中」錯誤。依網路討論 Server 升級到 10.2.0.5 以上可解決,當時決定將資料庫移至另一台 Oracle Server 11.2 成功脫逃,安全下莊。

半年後鳥問題捲土重來,一樣是 ODP.NET 12.1 連 Oracle 10.2.0.4 無法分散式交易,但這回資料庫沒得搬也很難升級,只能乖乖面對。

確認其他機器曾有 ODP.NET 11.2.0.3 成功與 Oracle 10.2.0.4 建立分散式交易,便在同台主機裝了 ODAC 11.2.0.3,程式換用 ODP.NET 11.2.0.3 後錯誤訊息卻完全沒有改變,揮棒落空,心中茫茫然…

招喚茶包一哥-Process Monitor,幸運挖到關鍵線索:

被 ODP.NET 版本惡整經驗豐富,對於發行者原則檔(Publisher Policy)倒也略懂略懂,在讀取上述 Registry 後,之後程式存取的都是 ODP.NET 2.112.0 DLL 檔案,由此識別出這是安裝 Oracle Client 12.1.0 加入的版本強制導向。

在 product\12.1.10\client32\odp.net\PublisherPolicy\2.x 可以找到多個發行者原則檔,分別將 2.102、2.111、2.112、2.121 導向 2.121 版。

Policy.2.112.Oracle.DataAccess.config 內容如下,這解釋為何我們已明確改用 2.112.0.3,仍被導向 2.121.1.0, 導致無法與舊版 Oracle Server 完成分散式交易。

<configuration>
<runtime>
<assemblyBindingxmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentityname="Oracle.DataAccess"publicKeyToken="89B483F429C47342"/>
<bindingRedirectoldVersion="2.112.0.0-2.112.9999.9999"newVersion="2.121.1.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

從組件管理 GUI 將 2.112 導向 2.121 的原則檔刪除,問題排除!

CSHTML 匿名型別資料繫結技巧一則

$
0
0

情境如下, 在 ASP.NET MVC 用一小段程式顯示部門下拉清單,資料來自資料庫,因欄位較多且命名不直覺,我將由資料庫取得的集合轉成匿名型別 Select(o => new { DeptId = o.DI, DeptName = o.DN },再以 Razor 語法 @foreach (var dept in ViewBag.Depts) { <option value="@dept.DeptId">@dept.DeptName</option> } 轉成下拉選單選項。程式碼範例如下:
HomeController.cs

publicclass HomeController : Controller
    {
public ActionResult Index()
        {
            ViewBag.Depts = DataHelper.GetDepts()
                .Select(o => new { DeptId = o.DI, DeptName = o.DN })
                .ToList();
return View();
        }
    }

Index.cshtml

@{
    Layout = null;
}
 
<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>MVC Test</title>
</head>
<body>
<div>
<select>
            @foreach (var d in ViewBag.Depts)
            {
<optionvalue="@d.DeptId">@d.DeptName</option>
            }
</select>
</div>
</body>
</html>

看似正常,執行時卻遇到錯誤,資料繫結(Data Binding)抱怨不認得匿名型別的 DeptId 屬性。

這才想到,我踩了匿名型別的紅線:參考

您無法將欄位、屬性、事件或方法的傳回類型,宣告為具有匿名類型。 同樣地,您無法將方法、屬性、建構函式或索引子的型式參數宣告為具有匿名類型。 若要以方法引數的形式來傳遞匿名類型或含有匿名類型的集合,您可以將參數宣告為物件(object)。 但是這樣做將失去強式類型的目的。如果您必須在方法界限外儲存或傳遞查詢結果,請考慮使用一般具名結構或類別來取代匿名類型。

匿名型別在 CSHTML 被當成 object 型別,無法滿足資料繫結的強型別需求。改用 @Newtonsoft.Json.JsonConvert.SerializeObject(ViewBag.Depts) 測試,驗證資料已正確傳到前端,其中差別在於 Json.NET 靠 Reflection 解析欄位可以正確讀取屬性,而資料繫結需要強型別。

有幾種解法:第一種是從 JavaScript 下手,既然資料能正確轉為 JSON,便可再轉為 JavaScript 物件陣列,用 Angular、Knockout 等 MVVM 框架可輕易繫結成下拉選單。[參考]

若想在伺服器端處理,第二種做法是放棄匿名型別,乖乖宣告一個 DeptInfo 之類的具名型別,其中定義 DeptId、DeptName 屬性做為 CSHTML 與 Controller 間的共通規格,這是最守規矩的正統解法。

如果你像我一樣崇尚簡潔勝過嚴謹,討厭為了一丁點限制搞出一堆只用一次的雞肋型別,可以參考看看我找到的第三種做法。(如果你也愛用 Dapper、Tuple,那我們應該是一國的)

在 Stackoverflow 找到一則相關討論,提到以前介紹過的既然要動態就動個痛快 - ExpandoObject,可以兼顧動態及強型別繫結要求。關鍵在於將匿名型別轉成 ExpandoObject,為求簡便,轉換程序可寫成擴充方法,再透過 .Select(o => new { DeptId = o.DI, DeptName = o.DN }.ToExpando()) 將匿名型別物件轉成 ExpandoObject 即可。ToExpando() 有個值得偷學的技巧:RouteValueDictionary 可將任何物件轉成 IDictionary<string, object>,再用 foreach + Add 方式將屬性複製到 ExpandObject 上,比 Reflection 寫法簡潔很多,但要記住 ExpandoObject 與 dynamic 背後仍是走 Reflection,留意可能的效能代價。

publicclass HomeController : Controller
    {
public ActionResult Index()
        {
            ViewBag.Depts = DataHelper.GetDepts()
                .Select(o => new { DeptId = o.DI, DeptName = o.DN }.ToExpando())
                .ToList();
return View();
        }
    }
 
publicstaticclass ExpandoExtensions
    {
//http://stackoverflow.com/a/5670899/4335757
publicstatic ExpandoObject ToExpando(thisobject anonymousObject)
        {
            IDictionary<string, object> anonymousDictionary = 
new RouteValueDictionary(anonymousObject);
            IDictionary<string, object> expando = new ExpandoObject();
foreach (var item in anonymousDictionary)
                expando.Add(item);
return (ExpandoObject)expando;
        }
    }

就這樣,匿名型別也可以做資料繫結囉~

最後補充一點,匿名型別真的不能在不同方法間傳遞嗎?倒也未必,如下例,善用 dynamic 就可以克服:

不過,以上範例並不能解決本次案例遇到的狀況。CSHTML 雖然支援 dynamic 資料繫結(例如 ViewBag 本身就是 dynamic) ,但 foreach 時不適用,猜想與 foreach 情境的資料繫結實作方式有關,這部分就交給 ExpandoObject 搞定囉~

IE 內嵌 IFrame 之 IE 相容模式組合問題

$
0
0

先前研究 IE 內嵌 IFrame 相容模式規則時,得到一個結論

透過IFrame內嵌網頁會沿用父網頁的文件模式,透過X-UA-Compatible亦無法改變

前幾天同事回報一個黑天鵝案例:IE8 相容模式網頁內嵌 IFrame,裡面再內嵌一個 IFrame,依先前理解,兩個 IFrame 都應沿用 IE8 相容模式。實測卻發現,只要內層 IFrame 沒宣告 X-UA-Compatible,網頁會處於比 IE8 還舊的相容模式。

這引發我的好奇,莫非先前結論有誤?

工作網站還有很多需要依賴 IE 相容模式再戰十年,這些眉角不徹底弄懂,將來少不了一堆見鬼的情境。於是我設計了以下實驗:

共有 Parent.aspx、Frame.aspx、SubFrame.aspx 三個網頁。Parent.aspx 用 IFrame 內嵌 Frame.aspx,Frame.aspx 再用 IFrame 內嵌 SubFrame.aspx,三個網頁可以各自控制 X-UA-Compatible 宣告,用一小段程式偵測 X-UA-Compatible 宣告及文件模式:(寫在 ietool.js 供三個網頁共用)

var meta = document.getElementsByTagName("meta");
var ieMeta = meta.length ? meta[0].getAttribute("content").split('=')[1] : "?";
var ieMode = document.documentMode;
document.getElementById("result").innerHTML = 
"META=" + ieMeta + ",MODE=" + ieMode;

Parent.aspx、Frame.aspx 如下,經由 QueryString 參數控制 X-UA-Compatible,並用 IFrame 內嵌下一層網頁:

<%@Page Language="C#"%>
<!DOCTYPEhtml>
<html>
<head>
<%
    var p = Request["p"];
if (!string.IsNullOrEmpty(p))
    {
%>
<metahttp-equiv="X-UA-Compatible"content="IE=<%=p%>"/>
<%
    }
%>
<style>body { font-size: 9pt; }</style>
</head>
<body>
<divstyle="width:520px">
<h2>Parent</h2>
<divid="result"></div>
<scriptsrc="ietool.js"></script>
<br/>
<iframesrc="frame.aspx<%=Request.Url.Query%>"style="width: 300px; height: 200px">
</iframe>
</div>
</body>

SubFrame.aspx 則多了一小段程式碼,將 Parent.aspx、Frame.aspx 與自己的 X-UA-Compatible 宣告及 IE 文件模式寫入 localStorage。

最後寫一個 TestRunner.html,三個網頁的 X-UA-Compatible 各有 Edge、10、9、8、7、5 及不指定七種選項,7x7x7 共有 343 種組合,透過程式試遍所有組合,最後將結果轉成 CSV 做成 Excel 報表。

<html>
<head>
<metahttp-equiv="X-UA-Compatible"content="IE=Edge"/>
</head>
<body>
<buttononclick="run()">Test</button><buttononclick="genReport()">Report</button>
<preid="rpt">
</pre>
<script>
function run() {
var list = ["Edge","10","9","8","7","5", ""];
var jobs = [];
for (var i = 0; i < list.length; i++) {
for (var j = 0; j < list.length; j++) {
for (var k = 0; k < list.length; k++) {
                    jobs.push("./parent.aspx?p=" + list[i] + "&f=" + list[j] + "&s=" + list[k] + 
"&n=" + (i + 1) + "-" + (j + 1) + "-" + (k + 1));
                }
            }
        }
        localStorage.clear();
var hnd = setInterval(function() {
if (jobs.length == 0) {
                clearInterval(hnd);
return;
            }
var win = window.open(jobs.pop());
            setTimeout(function() {
                win.close();
            }, 2000);
        }, 250);
    }
function genReport() {
var keys = Object.keys(localStorage);
        keys.sort();
var rpt = [];
for (var i = 0; i < keys.length; i++) {
var p = localStorage[keys[i]].split(',');
for (var j = 0; j < p.length; j++)
                p[j] = p[j].split('=')[1];
            rpt.push([p[0],p[2],p[4],p[1],p[3],p[5]].join(","));
        }
        document.getElementById("rpt").innerHTML = rpt.join("\n");
    }
</script>
<div>
</div>
</body>
</html>

測試過程發現: 當 Parent 為 IE8 相容模式,出現內外 IFrame 相容模式不同的狀況。重看MSDN文件的說明:

As of IE9 mode, webpages cannot display multiple document modes. For example, consider a standards-based webpage that contains a frame element that displays content in quirks mode. IE9 mode displays the child frame in standards mode (because the parent document is in standards mode). Starting with Internet Explorer 10, however, child frames can emulate quirks mode. For more info, see IEBlog: HTML5 Quirks mode in IE10. For best results, however, use document modes consistently.

IE9起,檢視網頁只允許存在一種文件模式,故IFrame網頁必須沿用父網頁的IE標準模式。IE10做了點改善,允許IFrame網頁啟用Quirks相容。

我有一個新體會,原以我以為文件說的 IE9 指的是實體瀏覽器的版本 9,但實際上用 IE11 模擬 IE9 模式就適用。結果出爐,符合新版推測:

當 Parent.aspx 為 Edge、IE=10、IE=9 時,Frame.aspx 與 SubFrame.aspx 不管 X-UA-Compatible 怎麼設定,其 IE 模式永遠與 Parent.aspx 一致。

 

當 Parent.aspx 是 IE=8、IE=7、IE=5 時,Frame.aspx、SubFrame.aspx 可隨 X-UA-Compatible 切換成不同 IE 模式,三者的 IE 模式可各自獨立,但 IE=Edge、10、9 一律降至 IE=8,當未指定 X-UA-Compatible 時,則取 IE=7。

由此新發現,先前的結論應修正為:

當父網頁為 IE11、IE10、IE9 模式,透過IFrame內嵌網頁會沿用父網頁的文件模式,透過X-UA-Compatible亦無法改變。
當父網頁為 IE8、IE7、IE5 模式,IFrame 內嵌的網頁可透過 X-UA-Compatible 切換與父網頁不同的 IE 模式,但 Edge、IE10、IE9 會降為 IE8。

Hi, XBOX ONE!

$
0
0

去年因為一些事,對人生無常頗有感慨,頓時發現自己的人生嚴肅到有些乏味,老在計較每一分每一秒時間的投資報酬率。小木頭恰巧是鮮明的對比,小小年紀就有七八十歲老先生的豁達(如此「早熟」令人堪憂呀),功課考試什麼的最討厭了,腦海老繞著看到的好玩電腦遊戲打轉。兩個不良示範掛在天平兩端也不是辦法,弄條管道擴大交集,鼓勵二者向中央靠攏,而身為阿宅想到的是-買台遊戲機。吸引嚴肅老頭有點玩心,當成貪玩小鬼努力唸書的誘餌,就這麼辦吧!

在 PS4 與 XBOX 間猶豫許久,最後選了 XBOX ONE。會被網友綁架的線上遊戲是無底洞,不碰為妙;太空戰士、古墓奇兵等史詩巨作耗神耗時,升學在即沒本錢沈迷;能全家同樂的體感遊戲或趣味小品,是我心中家用遊戲機的最大發揮,聽說 KINECT 2.0 比前一代精確靈敏,表現比 XBOX 360、PS4 好。PS4 在 VR 方面比較成熟是一項優勢,但我對於會把人封在自己小天地的 3C 科技有排斥感… 總之,最後就決定是 XBOX ONE 啦!(當然,XBOX 的微軟基因應該也是雀屏中選的非理性因素啦!)

上網挑來挑去,找到挺優惠的同綁包:XBOX ONE + KINECT + HALO 最後一戰:士官長合集。自己笨手笨腳,估計不是玩 HALO 這類講求耳聰目明手腳敏捷的第一人稱射擊遊戲的料,但鑑於它是 XBOX 獨家經典,威名顯赫,不想空留「平生不識陳近南,便稱英雄也枉然」之憾,看看豬走路也好,就它吧!

就不開箱了,簡單整理入手一個月的心得:

  1. KINECT 蠻厲害的,肢體動作捕捉精準度比想像高,而且還支援人臉辨識,從電視前方走過就會被它認出來,螢幕會出現「Hi, Jeffrey!」,頗有親切感 XD 
  2. 基本上 XBOX ONE 向下支援許多 XBOX 360 遊戲,但基於 KINECT 版本不同,無法向下相容 XBOX 360 的經典體感遊戲如全民運動會,讓人頗失望。
  3. HALO 我只玩到這裡而已。說過我不是玩第一人稱射擊的料,手腳反應奇慢,推搖桿精確度極差,螢幕看久了頭還會暈。
    指揮官,對不起讓你失望了,這是我的退伍申請…
  4. Cortana,是妳?
    在 HALO 裡見到久仰大名的 Cortana,原來長這樣…

    微軟在 Windows Phone、Microsoft Band(智慧手環)、Windows 10 的語音助理也叫 Cortana(小娜),典故便來自 HALO 這款經典遊戲。原本是戰艦的人工智慧輔助系統,後來轉移到士官長的戰鬥裝裡跟主角一起出生入死。(咦?把太空船等級的系統搬進個人移動裝備,供電、散熱跟記憶體容量真的不會有問題嗎?)
  5. 朗朗上口多年的「解除XX的成就」,玩了 XBOX 才首次真實體驗,我「解除了真的在 XBOX 上解除成就的成就」~
  6. 有 KINECT 卻玩不了全民運動會,找到口碑不錯的水果忍者加減填補空缺。

    好玩!但運動強度直逼跑五分速,玩沒幾分鐘就喘了~
  7. 開放世界遊戲是我的菜,沒有固定路線或邊界,沒有急迫逼人的倒數壓迫感,系統模擬出近乎真實的世界,其中的物體或角色會因你的行為有對應的反應,跟寫程式很像,任由你發揮創意嘗試各種行動觀察結果,愛怎麼玩就怎麼玩,有趣極了。
    趁著耶誕節打折,入手開放世界遊戲的經典大作-俠盗獵車手 GTA 5!

    遊戲我玩得超爛,射擊準星亂飄老打不中,開車一路蛇行四處亂撞,每次調頭方向盤肯定打反,撞車卡住倒車兩分鐘還在原地打轉,為「缺乏玩動作遊戲天分」做出最佳詮釋。主線第一件任務:跟車開到指定地點,我就失敗重試不下五十次,我想起無限接關卻過不了關的湯姆克魯斯,把 GTA 玩成明日邊界,我是史上第一人吧?

    但很奇妙,主線任務打不過一點都不影響遊戲樂趣,開放世界允許你用自己喜歡的方式與步調探索,嘗試各種有創意的死法玩法,超 級 好 玩!

XBOX 發揮了它的影響力,為了可以摸到手把,小木頭眼中閃過亮光,抱起課本…

最後,在 XBOX 上寫程式看來不難,用 C# 寫 UWP 就能搞定,在 XBOX 跑程式開啟開發者模式就能部署程式上去測試,然後地表最強的開發工具 Visual Studio 再次不辱盛名,能遠端逐行偵錯程式… 呸呸呸,怎麼又繞回程式(拍額頭),下回再說。

筆記-Scott Hanselman 的 2017 .NET 開發者須知

$
0
0

Scott Hanselman 前兩天有篇文章-What .NET Developers ought to know to start in 2017,我的工作(甚至生活)跟 .NET 息息相關,重量級人物的觀點自然不容錯過,整理筆記如下:

前言

  • Scott 之前整理過類似的 .NET 須知,結果被大家拿來當作面試時折磨新人的刑具…
  • 清單很長,但並不是每則都必須搞懂弄通,應視自己所需以及學習習慣取捨,有些知道名詞即可,有些應該深入了解。
  • https://dot.net是 .NET 技術資源的新入口,首頁有個線上 C# 編譯器可以玩玩。

必備知識

  • 新的 .NET 架構,分成 .NET Framework、.NET Core、Xamarin 三種 Runtime,底層是 .NET Standard…
    延伸閱讀:.NET Standard 2.0 是什麼?可以吃嗎?
  • .NET Framework - 用於 Windows 平台
  • .NET Core – 可通行於 Windows、Linux、Mac
  • Mono for Xamarin – 整合 .NET 與手機原生 API,可開發 iOS 及 Android App
  • 主要語言:C#、F#、VB.NET
  • 如何開始?
  • Frameworks
    指可使用的 API 集合,例如:.NET 4.6 Framework、.NET Standard 等,有時會以 TFM 表示
  • Platforms 平台
    如 Windows、Linux、Mac、Android、iOS 等,還會進一步區分 x86、x64。
  • TFMs (Target Framework Moniker)
    用來表示平台版本的簡稱,例如 net462(.NET 4.6.2)、net35(.NET 3.5)、uap(Universal Windows Platform),指定 TPM 決定可以使用的 API 範圍。
  • NuGet
    .NET 愈來愈依賴透過 NuGet 下載必要程式庫及核心元件,許多東西不再預先安裝於本機,而是新起專案時才下載,這年頭 .NET 開發者不會 NuGet 恐怕混不下去。
    延伸閱讀:還在揮汗徒手安裝程式庫? 試試NuGet
  • Assembly 組件
    .NET 程式編譯後的產出,多以 DLL、EXE 方式存在,是部署、版本管理、重複利用以及權限控管的基本單位。
    .NET Core 的編譯結果則是一個 NuGet Package,包含組件以及額外的 Metadata。
  • .NET Framework vs. .NET Core
    .NET Framework 聚焦於 Windows 平台(桌機、平板、手機、XBOX),.NET Core 則可跨平台。

應該知道

  • CLR 
    Common Language Runtime (CLR),執行 .NET Framework 的虛擬機器元件(for Windows)
  • CoreCLR
    .NET Core 用的 Runtime
  • Mono
    Xamarin 及 Linux 系統用的 .NET Runtime
  • CoreFX
    .NET Core 的 .NET 類別程式庫,部分程式碼與 Mono 共用。
  • Roslyn
    C# 與 Visual Basic 編譯器,有開放讀、寫、分析程式碼的 API  可供延伸應用。
  • GC
    .NET 使用記憶體回收機制,免除開發者自行管理記憶體的負擔。延伸閱讀:Fundamentals of garbage collection (GC).
  • "Managed Code"
    指使用 .NET 語言開發的程式,相對另一種是 Unmanaged Code,指用 C/C++/VB/Delphi 寫的程式、ActiveX、COM+元件。
  • IL
    .NET 編譯結果非機器碼,而是一種中間語言(Intermediate Language),執行時才由 Runtime JIT 編譯成機器語言。
    Scott 的比喻:C# 是蘋果,IL 是蘋果醬、JIT 及 CLR 再將它磨成磨成蘋果汁。
  • JIT
    Just in Time Compiler,即時將 IL 編譯成機器語言。
  • .NET Framework 的儲存位置在 C:\Windows\Microsoft.NET,而 .NET Core 在 C:\Program Files\dotnet,在 Mac 則為 /usr/local/share。但 .NET Core 允許把 Runtime 包進程式的 Package 一起部署,如此客戶端不需事先安裝 .NET Core Runtime,只要 xcopy 就可以部署(xcopy-deployable or bin-deployable),這種做法稱為:Self-Contained Application,反之則稱為 Shared Framework Apps。
  • async and await
    async 與 await 指令可解決執行耗時動作(例如查詢資料庫,呼叫 Web API)程式卡住的問題。
  • Portable Class Libraries
    一種允許跨平台使用的「最大公約數」性質的共用程式庫,未來建議改用.NET Standard。
  • .NET Core
    .NET runtime、一組 Framework Libraries以及一組 SDK 工具以及語言編譯器組成,這一切可由.NET Core SDK取得。
    'dotnet' 程式可用於啟動 .NET Core 程式,它會選取並執行適當的 Runtime,提供組件載入原則並啟動程式,SDK 工具也是用相同方式啟動。

錦上添花


使用 FileChangeMonitor 實現檔案資料快取自動更新

$
0
0

有個開發老鳥專屬的「成功經驗」陷阱:遇到難題,想出一套簡單有效解法,或許有些小缺點,但造成的麻煩在可忍受範圍,於是 日後再遇到同樣狀況,一律照方煎藥,數十年如一日。

但技術會革新、元件會改進,善用一些新特性,小缺點其實可以化為無形。可怕的地方在於:如果每次都能順利解決問題,就不會圖謀改進,直到有天發現洋人船堅砲利,才知自己已成滿清… 老鳥想一直寫程式又不想被時代淘汰,就得提高警覺。有個超簡單的實踐方法-對自己機車一點。當有人反應不方便時,別一句「就多一個動作會死嗎?」頂回去,改成問自己:「連這個動作都省不掉?嫩!」,對自己愈GY一點才能撐久一點。以下算是個實例:

專案有時會遇到上傳檔案更新資料的機制,每天由排程將當天的資料寫到固定資料夾,網站執行時解析檔案轉為必要的資料格式。由於檔案每天只更新一次,每次重新讀檔解析太沒效率,解析完將資料寫入 Cache 並設定當日有效,隔日再用到時 Cache 已逾時再讀取新資料,如此兼顧效能與資料即時性,看似挺完美。但有個問題,若營運過程發現檔案有誤重新上傳,此時 Cache 仍有效,網站將繼續涗用舊資料。因此得多設計清除 Cache 的 API,而中途更新檔案的 SOP 要改成:1) 上傳檔案 2) 呼叫清除 Cache API。

以下是實作範例:

DataHelper.cs

publicclass ProductItem
{
publicstring Name;
publicdecimal Price;
}
 
conststring CACHE_KEY = "PriceData";
 
publicstatic List<ProductItem> GetPriceData()
{
//實務上可寫成共用函式GetCachableData,參考:https://goo.gl/K0IeTb
//這裡為示範原理,直接操作MemoryCache
    var cache = MemoryCache.Default;
lock (cache)
    {
if (cache[CACHE_KEY] == null)
        {
string filePath = HostingEnvironment.MapPath("~/App_Data/Price.txt");
//將文字資料轉為物件陣列
            List<ProductItem> list = System.IO.File.ReadAllLines(filePath)
                .Select(o =>
                {
                    var p = o.Split(' ');
returnnew ProductItem()
                    {
                        Name = p[0],
                        Price = decimal.Parse(p[1])
                    };
                }).ToList();
//第一筆塞入Cache產生時間
            list.Insert(0, new ProductItem() { Name = DateTime.Now.ToString("HH:mm:ss") });
 
            cache.Add(CACHE_KEY, list, new CacheItemPolicy()
            {
                AbsoluteExpiration = DateTime.Today.AddDays(1)
            });
        }
return cache[CACHE_KEY] as List<ProductItem>;
    }
}
 
publicstaticvoid ClearPriceData()
{
    MemoryCache.Default.Remove(CACHE_KEY);
}        

HomeController.cs

public ActionResult ShowDailyPrice()
{
return View(DataHelper.GetPriceDataEx());
}
 
public ActionResult ClearDailyPrice()
{
    DataHelper.ClearPriceData();
return Content("OK");
}

ShowDailyPrice.cshtml

@model List<MyMvc.Models.DataHelper.ProductItem>
@{
    Layout = null;
}
 
<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>ShowDailyPrice</title>
<style>
        table { width: 200px; font-size: 11pt; }
        td,th { text-align: center; padding: 6px; }
        tr:nth-child(even) { background-color: #eee; }
        tr.hdr { background-color: #0094ff; color: white; }
        .name { width: 70%; }
        .prz { width: 30%; text-align: right; }
</style>
</head>
<body>
<div>
<table>
<trclass="hdr"><th>品名</th><th>價格</th></tr>
            @foreach (MyMvc.Models.DataHelper.ProductItem prod in Model.Skip(1))
            {
<tr>
<tdclass="name">@prod.Name</td>
<tdclass="prz">@string.Format("{0:n1}", prod.Price)</td>
</tr>
            }
</table>
</div>
<br/>
<small>
快取產生時間:@Model.First().Name
</small>
</body>
</html>

這套寫法我用在很多專案,由於中途更新檔案頻率不高,SOP 多一個動作大家覺得還好。但如果 GY 一點:手動清 Cache 的動作真的不能省嗎?這都搞不定,你有臉說自己是資深程式設計師?

其實,都用了 MemoryCache 做檔案快取卻要手動清 Cache 真的有點 Low。在存入 Cache 時,CacheItemPolicy除了設定保存期限、移除事件外,還可以指定 ChangeMonitor 物件,跟檔案、資料庫建立相依關係,在資料異動時自動清除快取。.NET 提供了幾個現成實作元件,包含:CacheEntryChangeMonitor(綁定另一個 Cache 項目,當其被移除時一併移除)、SqlChangeMonitor(利用 SQL Server 的 SqlDepedency在某個 DB 查詢結果改變時自動移除)以及 HostFileChangeMonitor(偵測檔案或資料夾異動時自動移除快取),而我們的案例即可藉由 FileChangeMonitor 實現重傳檔案時自動清除快取,省去手動清除的多餘步驟。

寫法很簡單,CacheItemPolicy.ChangeMonitors.Add(new HostFileChangeMonitor(string[] 檔案或路徑)) 就大功告成!

publicstatic List<ProductItem> GetPriceData()
{
//實務上可寫成共用函式GetCachableData,參考:https://goo.gl/K0IeTb
//這裡為示範原理,直接操作MemoryCache
    var cache = MemoryCache.Default;
lock (cache)
    {
if (cache[CACHE_KEY] == null)
        {
string filePath = HostingEnvironment.MapPath("~/App_Data/Price.txt");
//將文字資料轉為物件陣列
            List<ProductItem> list = System.IO.File.ReadAllLines(filePath)
                .Select(o =>
                {
                    var p = o.Split(' ');
returnnew ProductItem()
                    {
                        Name = p[0],
                        Price = decimal.Parse(p[1])
                    };
                }).ToList();
//第一筆塞入Cache產生時間
            list.Insert(0, new ProductItem() { Name = DateTime.Now.ToString("HH:mm:ss") });
 
            CacheItemPolicy policy = new CacheItemPolicy()
            {
                AbsoluteExpiration = DateTime.Today.AddDays(1)
            };
//指定檔案異動時自動移除Cache内容
            policy.ChangeMonitors.Add(new HostFileChangeMonitor(
//HostFileChangeMonitor接受IList<string>,此處用Split小技巧將單一或多檔名轉成IList
                filePath.Split('\n')));
            cache.Add(CACHE_KEY, list, policy);
        }
return cache[CACHE_KEY] as List<ProductItem>;
    }
}

實際展示如下,下方的檔案快取時間可用於驗證資料是否來自快取,先重新整理兩次時間未變,代表使用的是快取中的資料;修改檔案後儲存,重新整理網頁價格數字跟快取時間立即更新!

就醬,老狗又學會了新把戲。

【茶包射手日記】Windows 沒有足夠資訊可以確認這個憑證

$
0
0

某台持續爬網頁抓資料的排程忽然出現 The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel 訊息,推測為 SSL 憑證失效引起。

連至該主機使用瀏覽器檢視,果真憑證顯示異常:

錯誤訊息如下:

Windows does not have enough information to verify this certificate.
Windows 沒有足夠資訊可以確認這個憑證。

對照其他主機看到的憑證則很正常。

爬文得知此類問題出在缺少受信任的根憑證授權單位(Trusted Root Certification Authorities)或中繼憑證授權單位(Intermediate Certification Authorities)憑證資料造成。實際檢查問題主機,VeriSign CA 憑證 OK,但少了 Symantec Class 3 Secure Server CA - G4 中繼 CA 憑證。

最後,由其他主機匯出 Symantec 中繼 CA 憑證安裝在問題主機後順利排除問題!

問題已解,留下幾個疑點:

  1. 缺憑證問題多發生於第一次連網站。早上還正常,近中午才出現缺少中繼 CA 憑證錯誤有些令人費解。
  2. 依我的理解,SSL 協商過程網站有責任附上中繼 CA 憑證供瀏覽器驗證信任鏈,為何需要手動補安裝?

因無法重現問題反覆檢驗,先歸入 X 檔案。

Autofac筆記6-Resolve時依參數傳回不同型別

$
0
0

好久沒寫 Autofac 筆記,記錄一則最近遇到的小需求。系統中針對介面(例如:IBlah)實作了多個型別,Resolve<IBlah>() 時希望透過參數指定傳回不同型別。

依據官方文件,實現這類需求的最簡單做法是使用 Named Service(具名服務)或 Keyed Service(鍵值對應服務), Register<T>() 後不使用 As<IBlah>(),而改用 Named<IBlah>("服務名稱") 或 Keyed<IBlah>(列舉值) 註冊。之後呼叫端改用 ResolveNamed<IBlah>("服務名稱") 或 ResolveKeyed<IBlah>(列舉值) 即可取得不同執行個體(Instance)。如此我們可為 IBlah 註冊多種型別,彼此以服務名稱字串或列舉值區隔,Resolve 時透過服務名稱字串或列舉值可取得指定型別的執行個體。Named 與 Keyed 功能相同,差異在於使用字串或列舉當參數,我傾向使用列舉以充分享受強型別的優勢。

講了一堆,其實用起來蠻簡單的,來段程式範例大家就明白了:

using Autofac;
using System;
 
namespace AutofacLab
{
publicinterface ISensor
    {
string Detect();
    }
publicclass TemperatureeSensor : ISensor
    {
publicstring Detect()
        {
return"It's hot";
        }
    }
publicclass SoundSensor: ISensor
    {
publicstring Detect()
        {
return"It's noisy";
        }
    }
 
publicenum SensorType
    {
        Temperature,
        Sound
    }
 
class Program
    {
staticvoid _Main(string[] args)
        {
            ContainerBuilder builder = new ContainerBuilder();
//http://docs.autofac.org/en/latest/advanced/keyed-services.html
            builder.RegisterType<SoundSensor>().Keyed<ISensor>(SensorType.Sound);
            builder.Register((c) =>
            {
returnnew TemperatureeSensor();
            }).Keyed<ISensor>(SensorType.Temperature);
            IContainer container = builder.Build();
 
            var ss = container.ResolveKeyed<ISensor>(SensorType.Sound);
            Console.WriteLine(ss.Detect());
            var ts = container.ResolveKeyed<ISensor>(SensorType.Temperature);
            Console.WriteLine(ts.Detect());
            Console.Read();
 
        }
    }
}

在 Chrome/Edge 網頁用 IE 開啟超連結

$
0
0

這是 IE Only 網站親衛隊才有的困擾。

許多內部系統年代久遠,寫於全天下瀏覽器只有一種(IE)的時代(2004 年 IE 市佔高達 95% [參考]),寫成 IE Only 也是很合理的事。但你我都知道,時代不同了,滿天都是飛機啊,滿街都是電腦啊,HTML5 世代 IE 早已不是最好的瀏覽器選擇。望著公司那堆 IE Only 的生財工具營運系統網站,即使它們遲早要汰換,但也不是說翻就翻?有些規模數十人月的大專案,問君能有幾副肝,恰似鞭炮爆不完?

所以囉,繼續再跟 IE Only 網站和平共處十年,是每一位內部系統開發維護人員要有的心理建設。但尷尬的是-不少新網站改用 HTML5 新技術、新框架打造,JavaScript 角色日益吃重,而 IE 在這方面的效能表現明顯不如 Chrome 或 Edge,因此我們會常常明示暗示使用者改用 Chrome 開啟網頁以享受順暢的操作體驗,但一遇到要切換回還沒翻新的 IE Only 網頁功能就糗了,只能很心虛地跟使用者說「要用兩個功能記得要另外開 IE,不要直接在 Chrome 點哦,啾咪~」,想當然,使用者當場白眼都翻到後腦杓去了。

因此我常被問到「能不能從 Chrome 用 IE 開網頁?」,雖然腦中閃過幾個點子:寫個 Chrome Plugin?在使用者機器裝個常駐內應程式接收 Web API 啟動 IE?衡量部署及後續客服難度後,我的答案一直都是「辦不到」,直到這兩天… 我想到一個好點子:

影片

原理是借用 Windows 的自訂 URI Schema 功能,我定義一個 iehttp://… URI Schema,並透過 Shell Open 方式呼叫 iexplore.exe 開啟該超連結。其中有個特殊需求,%1 接收字串參數中的 iehttp 要換成 http,用了點 DOS 指令技巧,用 cmd /v /c 執行程式,將 %1 存入變數,再對變數(在 cmd 要加 /v,變數 %var_name% 要改成 !var_name!)進行置換(語法為 !var_name:find_str=replace_str!),處理完畢傳給 iexplore.exe 開啟網頁。接著,將要用 IE 開啟的連結由 http: 改成 iehttp:,大功告成!(實務上應加入自動偵測,遇到非 IE 瀏覽器開啟網頁時,再將 IE Only 連結的 URL 改掉)

附上 iehttp URI Schema 註冊機碼如下:(若作業系統為 32 位元,Program Files (x86) 請改為 Program Files)

Windows Registry Editor Version 5.00
 
[HKEY_CLASSES_ROOT\iehttp]
@="URL:Open with IE Protocol"
"URL Protocol"=""
 
[HKEY_CLASSES_ROOT\iehttp\shell]
 
[HKEY_CLASSES_ROOT\iehttp\shell\open]
 
[HKEY_CLASSES_ROOT\iehttp\shell\open\command]
@="cmd /V /C \"set URL=%1&& set URL=!URL:iehttp=http!&&cmd /c \"\"C:\\Program Files (x86)\\Internet Explorer\\iexplore.exe\"\" !URL!\""

提醒,跨瀏覽器開啟方式還是有些小缺點,雙方網頁即使同網域也無法共用 Session、Cookie,彼此的 DOM 也完全不相通,開啟過程會閃一下 DOS 視窗(可靠另寫小程式取代 cmd /c,請自行衡量是否需要)… 但對我來說,已能滿足不少單純的 Chrome / IE 併用需求,前進了一大步(灑花轉圈)~

另外附上完整操作示範影片

生態池日記-豆娘羽化

$
0
0

【警告】照片裡挺可愛的豆娘,小時候住水裡叫做水蠆(發音同「菜」),長得不太稱頭(其實是醜到讓人不蘇湖),文章內有水蠆的「近距離寫真」,請大家視身心狀態自行評估,看完以上照片就關掉網頁,只留下美好印象也是不錯選擇。

【背景知識】蜻蜓豆娘都屬於蜻蛉目,主要差異在於停止時翅膀平放展開或收合。二者的稚蟲都叫做水蠆,生活在池塘或溪流,以昆蟲、小魚或蝌蚪為食。延伸閱讀

防雷緩衝區起點

 

 

 

 

 

 

 

 

防雷緩衝區終點

願意把捲軸拉到這裡的同學,已展現對自然生態的好奇與熱愛,我們進入正題。

前些日子在水蘊草上看到一隻怪蟲,對水生昆蟲生態沒啥研究的我,搞不清楚它是什麼妖魔鬼怪,只覺得樣子挺噁心,拍完照片沒多久小蟲迅速游進水草叢間,之後一個多月,沒再發現其身影。

直到今天,醜醜蟲再度現身,身體呈深咖啡色爬出水面趴在葉子上做日光浴。難得有生物靜止不動自願當麻豆,趕緊拿出相機拍照,拍了幾張想撥葉子調角度,不慎讓牠滑進水裡,但牠沒有潛下去的意思,游到池邊被我補捉到一張完整全身照。沒多久,牠爬上另一片葉子繼續曬太陽。

   

觀察到完整蟲型,認真爬文,這才發現原來牠是豆娘稚蟲-「水蠆」來著,而爬出水面意味著牠要羽化了:參考

水蠆經過多次蛻皮,成長到終齡稚蟲,可以看到胸部會有明顯的翅芽,這是與其他齡期稚蟲的一個簡單的區別方法。通常蜻蜓的終齡稚蟲會在羽化前幾個小時爬出水面附近的地方,如枯枝、植物葉子、石塊、圍牆、橋墩等物體都可供蜻蜓穩定攀爬以進行羽化,據目前所知大多數蜻蜓於夜晚羽化,少部分蜻蜓成員在清晨羽化。剛羽化的成蟲通常體色很淡,翅膀很薄且有強烈金屬反光,等過一天之後,成蟲體色就會加深,翅膀也會變硬且沒有明顯反光,未成熟的成蟲要經一些日子才會變成熟。

生態池水面沒有可以向上攀爬的地方,我很有效率地來水桶、樹枝、吊繩,迅速打造出「頂級專屬羽化架」,資料寫羽化前數小時會爬出水面,而羽化時間多在夜晚或清晨,心想還有得等,搞不好像上次拍紋白蝶羽化得熬夜才能目睹。

誰知,去吃個午餐回來一看,豆娘早已現身,我錯過了蛻皮而出的黃金時刻!啊啊啊啊~

只拍到醜醜的空殼… Orz

8分鐘後

20分鐘後

30分鐘後

一小時後,哇,這身形也太修長了吧?

豆娘拍了拍翅膀,飛上螃蟹蘭繼續曬翅膀,我則留下這張畢業照,祝牠一生平安~

PS:小蟲小草拍多了,有想買單眼百微鏡頭的衝動,哈!

Viewing all 2324 articles
Browse latest View live


Latest Images