Quantcast
Channel: Darkthread
Viewing all 428 articles
Browse latest View live

【茶包射手日記】VAIO 筆電開機卡住經驗

$
0
0

我的 VAIO T13 筆電入手於 2012 年 Windows 8 上市之際,近五歲高齡但狀況不差,直到前陣子異常頻傳: 開始是桌面偶爾凍結,滑鼠游標能移動,但點選及鍵盤操作全無反應,關電重開機有效,但沒多久又再發生,常要重開數次才能完全排除,排除後可正常使用一陣子,但當機頻率愈來愈高。

終於來到這一天,某次當機重開後,電腦停在 Windows 啟動畫面轉圈圈(如下圖),等了三五分鐘還進不去,一直轉圈轉到人快被催眠,只好長按電源暴力關機重開,再開還是卡在轉圈畫面。反覆重試有時會進入 Windows 10 自動修復程序但以修復失敗收場,有兩次曾成功進入桌面,但沒多久又畫面凍結當掉,再陷入無法開機的循環。

太好了,我可以買新電腦了大事不妙,莫非我的老 V 氣數將盡?3C 設備用了四五年壞掉,最大的煩惱不是修不好,而是「修理 vs 換新」的艱難抉擇。過保固的維修費用往往逼近老機器殘值,有時再加一點都能買台新的,而修好能再用多久又是項人品考驗。To be or not to be, that it the question.

胡亂測試過程有一項重大發現 -- 拔掉電池,筆電就能順利開機且使用如常! 反覆測試數次,幾可推斷電池是導致電腦異常的源頭。在 FB 分享心得,具有相關背景的 Jesse 提到這可能是 EC(Embeded Controller,嵌入式控制器)故障造成的。

依據參考來源,EC(Embed Controller,嵌入式控制器)是一個16位單片機,它內部本身也有一定容量的Flash來存儲EC的代碼。在筆記型電腦中,無論你是在開機或者是關機狀態,EC都一直保持運行。EC其實就是一個單片機,是傳統KBC的延伸,基本上筆記型電腦上面的許多功能都是通過EC完成的,如:鍵盤、觸摸板、電池續航功能、風扇控制、溫度監控、待機或關機時期作業、智能電池電力檢測及充放電控制、特殊Hot Key… 等。

EC 與開機程序有關,與鍵盤滑鼠有關,與電池充放電控制也有關,因此電池故障導致前述種種異常是種合理解釋。只是我無法確定問題出在電池晶片還是主機EC,網拍買得到得新電池,但若問題不在電池就白花兩千大洋賭注有點大。另一個解法是找筆電維修店檢修,VAIO 在台灣市佔不高,維修選擇不多。怎麼想都是麻煩事,所以先逃避再說,拔掉電池把老 V 當成「要插電才能用的偽筆電」先擋著用了一陣子,倒也相安無事。

老天自有安排,就在上星期,比老 V 早一年服役的老 i7,原本配備兩顆 WD 黑標 1TB SATA3 硬碟,在今年五月有一顆升天了,不料幾個月後,剩下的硬碟飽受思念之苦傷心過度,幾天前在事件檢視器泣血成串,留下鮮紅一片,跟著老情人一起化成蝴蝶飛走了~ 冷氣壞了,筆電壞了,現在連電腦硬碟也壞了! (暗)

既然得去一趟光華商場補貨,就帶了老 V 碰碰運氣。VAIO 在台灣非主流廠牌,問了好幾家筆電維修店都搖搖頭說沒法修,連電池都問不到貨,最後才問到一家筆電維修店會修 VAIO。聽完我的故障狀況描述及電池禍首推論,正在拆筆電兼顧櫃枱的工程師大哥當場駁回電池是問題根源的猜想,斷定問題出在主機板,此時店內走出另一位較資深的先生,對問題是否出在電池態度較保留,提議幫我叫貨測試換新電池是否能解決問題,若非電池問題就付退貨及檢測費 800 元,感覺賭注有點大加上要留機檢修,決定先試試其他機會。最後,我在一家電池專賣店問到電池有現貨,好心的老闆還一口答應讓我先測過沒問題再買。現場換了電池重開機幾次,一切正常,判定搭配新電池可藥到病除,開開心心買單,老 V 又是一尾活龍。(不過,重出江湖的老 V 已另有安排,這裡先賣個關子)

筆電修好了但心中留下謎團,電池裡故障讓筆電開不了機的晶片,到底長什麼樣子? 網路上找不到前人分享的心得,熬不過好奇心,我做了一件危險的事 - 拆開電池一探究竟。警告: 拆卸鋰電池是危險行為,可能引發火災或爆炸(請看VCR),建議不要隨便嘗試。

小心翼翼打開電池外殼,裡面由多個 3.7V 鋰電池扁平 Cell 構成,整排四個平鋪,最左最右疊了兩層共計六個。

接頭處有塊電路板,上面有幾顆晶片,欠缺相關背景加上有一大塊被黑膠密封住,沒能力再推敲更多細節。連結頭有七根 PIN 腳加上電路板有晶片,意味電池可以對主機傳送資料,晶片故障狂發錯誤訊息讓主機端的 EC 程序陷入錯亂是有可能的。進一步確認需要更多設備、時間與精力,也超出電子麻瓜的能力範圍,就此打住。(註: 拆解研究完畢,已小心用三秒膠將電池外殼黏好送交資源回收)


JavaScript 開發者 ES6 小抄筆記

$
0
0

在網路上看到這篇 - Modern JavaScript Cheatsheet - Modern JS Cheatsheet,給既有 JavaScript 開發者看的小抄,指出因應 ES6/ES2015 新標準的注意事項。(註:如果你被 ECMAScript 6、ES6、ES2015 等術語搞到頭很昏,可以參考這篇) 而這篇則是我以一個 jQuery/TypeScript/C# 開發者角度閱讀小抄的筆記整理,主要供自己備忘(謎: 天哪,這年頭連看小抄都要做筆記了嗎?),順便分享給類似背景的同學參考。(若覺得筆記過於簡要,強烈建議閱讀原文,原文有不少文件詳解連結,包你看到懂)

var, let, const的差別與使用時機

let 宣告只在 Scope 內部有效,不會干擾外一層或全域範圍的同名變數,建議取代 var 使用;而 const 特性與 let 相似,差別在於 const 宣告後變數不能再改指向其他內容。

Arrow Function

相當於 TypeScript 或 C# LINQ 中的 (b) => b*2; 或 () => { ... } Lambda 寫法。有一點值得注意,在 Arrow Function 中 this 代表外一層執行環境的 Context,與 function() { }會另起 this 行為不同,這點與 TypeScript 依循的準則一致,小心不要踩坑。(參考: 再談TypeScript的this )

Desctucting 宣告

用 { prop1, prop2 } = someObject 快速取得 someObject 的 prop1, prop2 屬性值。[ x, y ] = [ 1, 2] 可快速取得 x=1, y=2。

Array 內建 map()/filter()/reduce()

前二者相當於 jQuery map()grep(),reduce() 則是將陣列元素彙總成單一值。(例如: 數字加總)

Spread Operator "..."

可用於陣列

const a1=["a","b","c"];
const a2=[a1,"d","e"]; // -> [["a","b","c"],"d","e"]
const a3=[...a1,"d","e"]; // ->["a","b","c","d","e"]

也可用於物件屬性

const myObj={x:1,y:2,a:3,b:4};
const {x,y,...z}=myObj;
//結果: x=1,y=2,z={a:3,b:4}
const n={x,y,...z};
//結果: n={x:1,y:2,a:3,b:4};

上例 Destructing 宣告「{x,y,…z} = myObj;」出現的 ... 稱為 Rest Operator,用於函式可實現 Function Rest Parameters,類似 C# 的 params 關鍵字觀念

function myFunc(x,y,...params) { ... }
myFunc("a","b","c","d","e");
//結果: x="a", y="b", params=["c","d","e"]
//此寫法稱為 Function Rest Parameters,類似 C# 的 params關鍵字

補充 MDN 的說明: Rest syntax looks exactly like spread syntax, but is used for destructuring arrays and objects. In a way, rest syntax is the opposite of spread syntax: spread 'expands' an array into its elements, while rest collects multiple elements and 'condenses' them into a single element. (感謝 Will 指正)

物件屬性簡寫

const x=1, y=2;
const myObj = { x, y };
//myObj.x=1, myObj.y=2

常寫 C# LINQ 的同學應該不陌生,相當於超精簡的 new { Prop1, Prop2 } 匿名型別寫法

原生Promise

var p = new Promise((resolveFunc, rejectFunc) => {
//作業成功時呼叫resolveFunc(resolveArg)
//作業失敗呼叫rejectFunc(err)
});
p
//呼叫resolveFunc觸發then
.then((resolveArg) => { ... })
//呼叫rejectFunc觸發catch
.catch((err) => { ... });

參考: 小試 JavaScript Promise

Template Literal

BJ4,就是TypeScript 1.4 加入的超級好用 Template String,用過一次就回不去了。

export/import 模組引用

//** blah.js **
export const pi = 3.14;
export const authorName = "Jeffrey";
 
//** samp1.js **
import {pi,authorName} from  "./blah.js";
//pi==3.14, autherName=="Jeffrey";
 
//** samp2.js **
import * as blah from "./blah.js";
//blah.pi==3.14, blah.authorName=="Jeffrey"
 
//** samp3.js **
import { pi as PI } from "./blah.hs";
//PI==3.14

實務上還有一種常見用法: 若模組只匯出單一變數、函式、物件,可寫成 export default theVarToExport,引用端可任意引用命名 import anyName from "./blah.js" 取得其預設匯出項目。

對初學者像謎一般的this

延伸閱讀:
Javascript - 淺談this與Closure
Javascript .apply()應用實例

ES6 class 關鍵字

用來簡化原本繁瑣的 prototype 宣告。相比之下, TypeScript 的 class 更強大,可以直接享用介面、繼承等特性。

async/await

跟.NET 4.5的async/await概念相似,要搭配Promise使用,可要求等待Promise()非同步執行完傳回結果再繼續往下執行

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

Truthy/Falsy

if (blah) 在什麼情況下 if 會成立?
當 blah 為以下內容時 if 將不成立,否則都視為 if 成立

  • false
  • 0
  • ""(空字串)
  • null
  • undefined
  • NaN

附註: 以上這些特性必須在支援 ES6/ES2015 規格的瀏覽器上才能運行,如果你跟我一樣必須考慮 IE 又想使用這些新方法,那麼 TypeScript 允許你用新語法寫程式並將它們轉成老瀏覽器也能執行的相容語法,更甭提強型別、物件導向這些讓程式易於維護擴充的優勢,絕對是一個好選擇。(沒聽過 TypeScript? 請參考: Hello, TypeScript!)

當心營運資訊裸奔-網站偵錯 Log 檔常犯的資安錯誤

$
0
0

「寫 Log」是很有效的線上系統偵錯手段,就像飛機黑盒子或行車記錄器,能在事故發生後提供寶貴資訊,釐清肇事原因,還能用於責任歸屬舉證。例如:

  1. 系統不定期爆炸,由 Log 歸納每次發生在某使用者進行某項操作之後
  2. 客戶否認下單,調閱 Log 舉證登入時間,來源 IP 以及操作順序,萬一客訴鬧上法院還可當作呈堂證供
  3. 資料庫發生 Deadlock,由 Log 找出事件當下兩名使用者執行的作業及輸入參數,鎖定可疑 SQL 語法進行調查

簡單來說,愈重要的網站系統愈需要 Log 機制協助維運,而 Log 該保存什麼內容,視作業性質而定,不外乎時間、IP 來源、使用者身分、操作參數… 等。而Log 儲放位置有很多選擇,本機檔案、Log 資料庫、Log 伺服器… 等。其中以本機檔案執行成本低、速度快,不易故障,可靠性最高,是最常見的 Log 選項。


Image by thom

由於 Log 會內含機敏資訊,若是處理不慎,Log 檔可能成為營運機密或個資洩漏的源頭。前陣子剛好看到營運資料透過 Log 檔在網站裸奔的案例,猜想有些開發朋友還沒意識到這類潛在風險。本文將整理我所想到處理網站 Log 常犯的錯誤及其可能衍生的資安風險:

未排除個資或機密資訊

以事後偵察的角度,保留資訊愈完整,愈能還原現場,故開發人員有時會將所有輸入參數與輸出結果都留在 Log 裡,但這裡有個常犯錯誤,機密敏感資料例如:身分證號、密碼、銀行帳號、信用卡號、地址、電話… 等也被寫入 Log,而系統管理者處理 Log 檔的資安敏感度不如原始碼、資料庫嚴謹,導致 Log 檔案外流的機率較其餘二者為高,一旦 Log 檔落入賊人之手,其中的個資會惹來無窮麻煩。

解決之道在於調整程式邏輯,排除個資或機密資料,若必須保留也請打上馬賽克,例如:身分證號寫成 A12XXXXX89,密碼只留頭尾一碼(甚至全部遮罩),電話號碼改成 0937XXX123,信用卡號改成 1234-XXXX-XXXX-5678。不保留完整內容,在某些情境會影響還原現場的完整度,但必須取捨,依實務經驗,即使參數被馬賽克,仍可從相關資訊識別請求來源,應付大部分偵錯需求綽綽有餘。衡量其風險及必需性,針對機密或個資,強烈建議排除或馬賽克處理。

將 Log 檔直接放在網站目錄下

有些開發者選擇在網站資料夾下開個 Log 之類子錄(例如:wwwroot\MyApp\Log)放記錄檔。將存取 Log 跟網站放在一起,相關資訊集中管理貌似天經地義,但要當心一個天大陷阱: 一旦 Log 路徑及檔名被惡意人士掌握,對方開個瀏覽器輸入 URL 就能光明正大將 Log 下載回去把玩,要是 Log 還包含個資、帳號密碼、交易內容... 事情就大條了。說不定接著會上演「黑先生,您最近有在我們網站買了一本單元測試的藝術 [第二版],因系統問題被設成 12 期分期每月扣款,需要您到 ATM 機器解除設定...」

因此,除了個資及敏感內容要馬賽克,也請不要將 Log 放在可直接下載的資料夾範圍。一般來說,外界很難得知 Log 路徑檔名,但無法排除以下狀況:

  1. 取得原始碼或網站檔案備份
  2. 由其他網站或測試台觀察到目錄結構
  3. 使用讀心術、通靈術瞎猜矇到

一旦路徑被掌握加上 Log 附檔名是網站允許的 MIME 型別(例如: .txt),後果會十分慘烈,不可不慎。如果一定要集中放在網站目錄,請設定排除規則禁止外部透過 GET/POST 該路徑下的內容,或是善用 ASP.NET 的 App_Data 隱身特性,但強烈建議在網站目錄之外為 Log 另開專屬目錄存放更安全。

備份與複製檔案時未排除 Log

在實務上,基於管理、偵錯需求有時我們需要備份或複製整個網站或整台主機檔案,對於程式檔案管理者的警覺性不像對待資料庫或資料檔那麼高規格,加上沒有意識到包含商業機密的 Log 檔案也在其中,備份檔可能被放在不夠安全地方且未嚴格限定存取,導致 Log 內容外流。

依我的經驗 ,備份或複製網站程式檔較常發生(換版前備份、複製到其他主機重現問題),將 Log 移出網站資料夾,除非備份整顆硬碟或刻意選取 Log 資料夾執行,Log 被意外備份或複製的機率可大幅下降。這也是我強烈建議另外為 Log 建立專屬資料夾的理由,除了避免不小心被備份或複製,管理者操作時能更明確認知到他在處理包含營運資訊的內容(前題是要有 Log 會內含機密資訊的認知),而不是當成程式有所輕忽。

非必要的偵錯詳細資訊

這個是較罕見的低級錯誤,我曾見過開發者為了偵錯時能萬無一失,居然在 Log 檔中印出資料庫連線字串備查。(我的老天鵝)
敢這麼做多半基於一個假設: Log 檔永遠被會嚴加保管,限制管理者存取。理應如此,但你知道的,實務上總會有豬隊友,有意外。面對資安議題,永遠假設機制會失靈,人為必有疏失就對了。決定要在 Log 寫入機密資料前,若評估一旦外洩會有嚴重後果,除非有絕對理由必須承擔此風險,否則別這麼做。

 

最後總結處理網站 Log 的幾點資安原則:

  1. 排除機密敏感內容,若必須保留要加馬賽克
  2. 避免允許使用者透過網站直接下載 Log 檔
  3. 為 Log 另建專屬資料夾避免因備份或複製而外流
  4. 開發者及系統管理者應建立 Log 可能內含機密資訊的認知
  5. 永遠假設 Log 可能外流,避免寫入非必要的機密資訊

TypeScript Template String 中文字元被轉為 \uxxxx 格式

$
0
0

Template String是 TypeScript 1.4 起加入的超好用功能(跟 C# Interpolated Strings 字串插值一樣,是用過就上癮的好物),今天發現一個問題 - Template String 內含的中文字元會被強制轉成 \uxxxx。(這種表示法術語叫 Unicode Escape Sequences)

例如以下範例:

var t2 = `ABC-中文-${n}`;

會變成:

var t2 = "ABC-\u4E2D\u6587-" + n;

想爬文找出避免轉換的做法,在 TypeScript Github 找到一則討論: Incorrect compilation of template strings containing Unicode characters

有不少開發者也發現了這個問題,認為 TypeScript 沒必要把所有非 ASCII 字元都換成 \uxxxx 格式。這個問題被認定是個 Bug,而開發團隊成員 DanielRosenwasser 提出解釋:

The reason I didn't preserve the original text when I implemented this was to avoid re-scanning the string when performing emit. We basically take the internal textual representation and call something that's basically an augmented JSON.stringify. This is good because it replaces newlines with \n,

However, the function takes a conservative approach and uses a unicode escape if something falls outside of ASCII. We take advantage of this if you ever use an extended unicode escapes in all strings.

For instance, the string "\u{12345} också" will get rewritten to

"\uD808\uDF45  ocks\u00E5"
in ES5 instead of

"\uD808\uDF45  också"
So what you're noticing is that this function does a teensy bit too much.

Also, given the fact that TypeScript can _finally_ assume the existence of JSON.stringify, this fix is probably a LOT easier.

DanielRosenwasser 提到不保留原始文字是為了避免注入過程需要重新掃瞄字串,程式內部借用了類似 JSON.stringify() 的函式進行轉換(好處是換行符號會換成 \n),該函式為求保險將 ASCII 字元表(0x00-0x7f)以外的字元通通換成 \uxxxx,雖然如此一舉解決罕用 Unicode 字元的轉換,但該函式的轉換範圍太過火,產生我們觀察到的後遺症。

好消息是 TypeScript 終於能假設 JSON.stringify 一定存在,這問題將比較容易修復,而壞消息是這個 Bug 被排在 Future 清單,何時會修正不得而知。

我唯一想到的 Workaround 是改寫成 `ABC-${"中文內容"}`,很醜且中英文穿插時會很噁心,算不上什麼好法子。

所幸,這在實務上不算嚴重問題。 TypeScript 預設會產生 js.map:

故實際偵錯時中斷點是停在 .ts 原始碼,可直接看到原始中文,.js 中文變成編碼的困擾不大。

只有一點要注意: 當你試圖在 js 搜尋中文關鍵字,要有可能會撲空的心理準備。

TIPS-VS2017 無法編譯新版 TypeScript 定義檔

$
0
0

以下為在 Visual Studio 2017 使用 TypeScript 定義檔可能出現的狀況。由 NuGet 或 Github 取得 TypeScript 定義檔,卻噴出大量編譯錯誤無法使用:

Visual Studio 2017 已更新至 9/19 才發行的 15.3.5 版本,TypeScript for Microsoft Visual Studio 也被一併更新至 15.3.10723.1:

前幾天剛好聽同事提起 VS2017 與 TypeScript 可各自更新(參考: Updating TypeScript in Visual Studio 2017 · Microsoft-TypeScript Wiki),猜想可能是我機器上的 TypeScript 版本太舊, Visual Studio 不認得定義檔使用的新語法(開源程式作者通常很早就開始應用新版特性)。查了一下,我專案預設使用的 TypeScript 版本是 2.1 版本,切換到 2.3 版即可正常編譯。

除非專案 TypeScript 程式有新版相容問題且不想修改升級,建議更新到最新版的 TypeScript SDK for Visual Studio並設成 Use latest available,可避免再遇類似狀況。至於主機安裝了哪些版本 TypeScript,可檢查 C:\Program Files (x86)\Microsoft SDKs\TypeScript:

擦屁股的藝術 – 聊聊前人 Bug 的緊急修補

$
0
0

身為程式開發人員,多少都有這種經驗:

線上系統出錯,原開發者已浪跡天涯,程式碼沒人熟,老闆面色猙獰問誰會修。
(遇到這種擦屁股的屎缺,同事們默契十足全都退了一步 )
老闆說「很好,想不到你剛進公司就想立此奇功!好好幹,公司不會虧待你的」...
你說「暗陰羊咧,陳近南是你?」「沒問題,這交給我!」(心中滿是狂奔的羚羊)

這類狀況跟修自己的 Bug 截然不同,有幾個特點:

  • 狀況緊急必須限時修好,砍掉重練不是選項。
  • 屬臨時救急,策略上不打算多花資源深入了解及翻修。例如: 有計劃另建新系統取代,舊系統已進入插管維生階段。
  • 處理者對系統架構、程式碼全然不熟悉,時間壓力下難以全面了解,也不敢大幅更動,需以最小幅度修改解決問題為目標,是一種「微創手術」的概念。
  • 最重要的一點,修復過程通常會五毒攻心氣血逆流,只求速速搞定:
    「喵的,又不是我搞壞的,為什麼是我」
    「暗!這是什麼鬼寫法啦?」

基於上述因素,修補者常會無視原本程式碼的明顯缺陷,陷入「讓修改幅度極小化」的迷思。最後,雖然只改一個字元就把問題修掉,但錯失了防範類似問題再次發生的機會,為下次爆炸埋入引信。

用一個範例來模擬情境。假設有個網頁程式由資料庫取得下拉選單選項,並取第二選項為預設值,前人的程式這麼寫:

    ddlSource.Items.Clear();
    ddlSource.Items.AddRange(
        GetSourceOptions()
        .Select(o => new ListItem(o.Value, o.Key)).ToArray());
    ddlSource.SelectedIndex = 1;

有一天資料庫端忽然冒出新選項,原本的第二選項變成第三個,使用者抱怨送出表單的預設選項錯誤,後續作業大亂。

你被指派修復這個問題,千辛萬苦追程式碼找到問題點,基於「微創手術」的概念,所以...

ddlSource.SelectedIndex = 2;

改完收工,跟老闆回報問題修好了。

只改了一個字元就把Bug修好了,但,這是良好的修復方式嗎?

小天使說: 如果下回資料庫再被塞入一筆資料,是不是系統又要再壞一次? 又不知是哪個倒楣鬼被踢下來辛苦追 Code,找出這段再改一次。(說不定還是你)
小惡魔說: 程式又不是我寫成這樣,我只奉命修好它,再壞掉也不是我的責任。更何況,使用者說這個選項不太會動,這次異動是個意外,以後應該不會再動。

看似兩難情境,分析利弊後不難抉擇:「如果成本不高,你應該把它改成強韌不易出錯的版本」,理由很簡單:

  • 依據墨菲定律,別人愈說不會修改,它愈可能被修改
  • 踢到石頭跌倒,把石頭搬到路邊防止別人摔跤,累積陰德抵過扶老太太過街三次
  • 「上回 XXX 改好過,這回又壞了」。就像修過的水管又漏水,就算主因是管線設計先天不良,你覺得倒楣鬼水電工會不會背負功夫差做事兩光的評價?

SelectedIndex = 2 寫法必須建立在「資料來源項目不變,順序固定」的前題上,在我眼裡脆弱得像玻璃,稍有風吹草動便碎裂一地。如果修改成本不高,改用防禦式設計可讓程式更強韌,不易因外部因素故障。這點也是有些人的程式三天兩頭故障,有些人的程式像大同電鍋一用數十年都不壞的關鍵之一。

基於預設選項順序可能不固定的考量,程式可以改成這樣:

    ddlSource.Items.Clear();
    var sources = GetSourceOptions()
        .Select(o => new ListItem(o.Value, o.Key)).ToArray();
    ddlSource.Items.AddRange(sources);
    var defaultSource = sources.SingleOrDefault(o => o.Value == "A2");
if (defaultSource == null)
thrownew ApplicationException("GetSourceOptions未包含A2項目");
    ddlSource.SelectedIndex =
        sources.ToList().IndexOf(defaultSource);

       
透過 SingleOrDefault() 用 Value 值找出預設選項的順序,既使查詢結果大風吹,它也能自動選對預設項目。要是資料庫裡的預設項目不知何故被他X的誤刪,這段程式還能明確指出問題出在"GetSourceOptions未包含A2項目",而不是噴出莫名其妙的 NullReferenceException,是不是貼心多了?

更進一步,如果某一天,預設值他X的要從 A2 改成 A3(對,那個規格書說永遠固定的A2),只能挖出程式碼重新編譯才能調整。於是我們還可以把預設值改成由 web.config appSettings 決定:

staticstring defaultSourceValue = 
    System.Configuration.ConfigurationManager.AppSettings["DefaultSource"] ?? "A2";
 
//...略...
 
    ddlSource.Items.Clear();
    var sources = GetSourceOptions()
        .Select(o => new ListItem(o.Value, o.Key)).ToArray();
    ddlSource.Items.AddRange(sources);
    var defaultSource = sources.SingleOrDefault(o => o.Value == defaultSourceValue);
if (defaultSource == null)
thrownew ApplicationException($"GetSourceOptions未包含{defaultSourceValue}項目");
    ddlSource.SelectedIndex =
        sources.ToList().IndexOf(defaultSource);

醬子,連改掉預設項目都不用重新編譯程式呢,是不是好捧捧?

原本只要 2 改成 3 就可以交差,多寫幾百個字元是比較費工,但說穿了仍在舉手之勞的範圍,多花不到10分鐘讓程式材質從玻璃升級到不鏽鋼,很划算。

擦屁股是苦差事沒人愛,你可以衛生紙抹一下交差,也可以搬出免治馬桶座洗個痛快,有人還會順便把脈開藥治好烙賽,
即使能做到什麼程度也與經驗能力相關,但最重要的是開發人員的心態。(Yo Yo,拎杯也有 Freestyle )

要成為別人眼中專業又可信賴的開發人員,先從擦得一手好屁股開始吧~

TypeScript Module 簡單練習

$
0
0

ES6 引進 Module(模組化) 概念,每個 Module 自成獨立 Scope,各 Module 可自由定義變數、型別,要開放外界存取的項目再透過 export 開放。當需要引用其他 Module 時,則必須明確使用 import 匯入才能使用。如此各 Module 可獨立開發維護而不彼此干擾,甚至能實現需要時再動態載入,大幅提升開發及應用彈性。

TypeScript 也支援 Module,我目前的專案沒用到這麼高級的技巧,原本並不打算深入了解,但發現苗頭不對。開源專案如 Angular 2、Vue 早就 Module 滿天飛,不懂 Module 就看不懂範例程式及原始碼,遇到狀況也不知從何查起,感覺自己很廢,好吧,硬著頭皮也要學會。

開始之前推薦幾篇先修知識文章:

先簡單歸納幾則重點:

  1. TypeScript 程式碼只要出現 import 或 export,就會被視為 Module,編譯結果與一般 TypeScript 大不相同。
  2. TypeScript Module 編譯產生的 js 不能單純用 <script src=".."> 載入,需依賴載入機制,載入機制分兩種:
    * 靜態載入: 編譯時將 Module js 檔打包成單一檔案,例如 Node.js 使用的 CommonJS
    * 動態載入: 網頁執行時視需要下載 Module js,例如: AMD 與 RequireJS
  3. 由於瀏覽器對 ES6 支援度不足,故需要額外的 Module 系統輔助, 目前還在百家爭鳴: AMD、CMD、closure、CommonJS、ES6。Visual Studio 的 TypeScript 編譯設定也可指定要用哪一種系統。
  4. 如果不想動用 Node.js,選擇 AMD Module 系統,網頁端使用 ReuqireJS 載入是最簡單的做法。

有了初步認識,來寫一個簡單範例當作練習。

在 ASP.NET MVC 專案 Scripts 目錄開一個 lab 資料夾,放入多個 ts 檔:

Module System 選擇 AMD:

在四個 ts ,我分別練習了不同的 export/import 寫法。首先是 common.ts,class 及 interface 前方加上 export 開放 Message 類別及 IOutput 介面:

//直接在const、function、class、interface前加上export關鍵字
export class Message {
    Time: Date = new Date();
    Text: string;
    constructor(text: string) {
this.Text = text;
    }
}
 
export interface IOutput {
    Write(msg: Message);
}

console.ts 先從 common.ts 引用 IOutput 及 Message,最後將自己定義的新類別 ConsoleOutput 跟來自 common 的 Message export 出去(註: 如果自己不用純分享,可以寫成 export { Blah } from "./blah")。

import { IOutput, Message } from "./common";
 
class ConsoleOutput implements IOutput {
    Write(msg: Message) {
        console.log(`${msg.Time.toLocaleTimeString()} ${msg.Text}`);
    }
}
 
//各模組可export相同名稱項目
export const Version = "ConsoleOutput 1.0";
 
export { ConsoleOutput, Message };

另一個 dom.ts,import 的做法不太一樣。 * as com 會將 common 所有 export 項目包入名為 com 的變數,使用時需寫成 com.IOutput、com.Message,有點像 Namespace 的觀念。dom.ts 跟 console.ts 都匯出了名為 Version 的 const,由於引用方會用 import 明確宣告,我們不用擔心名稱衝突。最後 export 時加上 default 關鍵字,引用方可以直接寫 import 名稱 from "./dom" 取得 DomOutput。

//取得模組所有匯出項目,包成變數com的成員
import * as com from "./common";
 
class DomOutput implements com.IOutput {
    Write(msg: com.Message) {
var div = $("<div></div>");
        div.text(`${msg.Time.toLocaleTimeString()} ${msg.Text }`);
        div.appendTo("body");
    }
}
 
//各模組可export相同名稱項目
export const Version = "DomOutput 1.0";
 
//export為預設項目,import時可直接引用
export default DomOutput;

測試程式 main.ts 如下,分別由 dom.ts/console.ts import 取得 DomOutput、ConsoleOutput、Message,餘下的寫法跟一般 TypeScript 無異。

import DomOutput from "./dom";
import { ConsoleOutput, Message } from "./console";
 
var c = new ConsoleOutput();
c.Write(new Message("console test"));
var d = new DomOutput();
d.Write(new Message("dom test"));

編譯出來的 main.js 如下: (注意: TypeScript Module 編譯成的 js 不能以 <script src="scripts/lab/main.js"> 直接載入,會出現 "Mismatched anonymous define() modules" 錯誤,必須改用 require.js require() 函式載入。)

define(["require", "exports", "./dom", "./console"], function (require, exports, dom_1, console_1) {
"use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
var c = new console_1.ConsoleOutput();
    c.Write(new console_1.Message("console test"));
var d = new dom_1.default();
    d.Write(new console_1.Message("dom test"));
});
//# sourceMappingURL=main.js.map

測試網頁如下: (require.js 可使用 NuGet 安裝或從官網下載,接著用 require(["scripts/lab/main"]) 就能順利載入 main.js 並執行之。)

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
<title>TypeScript Module Test</title>
</head>
<body>
<scriptsrc="Scripts/jquery-3.2.1.min.js"></script>
<script src="Scripts/require.js"></script>
<script>
        require(["scripts/lab/main"]);
</script>
</body>
</html>

測試 OK,網頁與 Console 都成功印出訊息。而我們也能觀察到 RequireJS 先載入 main.js 再陸續載入 dom.js/console.js/common.js 的過程,代表它能解析相依性自動載入所需模組。

以上就是簡單的 TypeScript Module 演練,謝謝收看。

Vue 筆記1–也來寫 Vue.js 好了

$
0
0

觀注 Vue.js 已有很長一段時間,上個月我慣用的前端元件庫-Kendo UI 正式支援 Vue 及 React,感覺時機成熟,是可以投入心力研究的時候了。

說來尷尬,手邊已有用 knockout.js 開發的線上系統,開發專案的主力則走 Angular.js 1。短短幾年就在公司裡搞出兩套框架(我還為 KO 跟 NG 都寫了 CRUD 基礎模版及元件庫),讓同事無所適從天怒人怨(我也是萬般不願意呀,這就是他X的前端咩),貿然宣布要玩第三套新框架,有預感會被拖到牆角餵磚頭。

不過衡量情勢,維持現狀不是選項,遲早還是要做打算。Knockout.js 已熄火這點無庸置疑,Angular 版本已更新到 NG4,不升級相關資源將逐漸凋零,長遠來看還是得跟上主流。但 NG2 形式大改,NG1 專案升級形同砍掉重練,佔不到什麼便宜,不如抛棄歷史包袱,重新評估擇選擇好了。

當前檯面公認的主流前端框架不脫 React、Angular 與 Vue.js 三家。網頁開發者各有自己的設計哲學與偏好,只要系統穩定效能不差又好維護,能抓耗子就是好貓(其實還要不挑嘴不亂叫不隨地大小便不抓壞沙發,想當好貓不簡單呀),網站設計方式與開發哲學,是決定開發者與前端框架是否看對眼的關鍵。只是有情人未必終成眷屬,因父母之命(老闆指定)、媒妁之言(顧問推薦)、家族身世(前人專案選定)、政治聯姻(資源豐富可少奮鬥30年)… 貌合神離的怨偶也不在少數。

以我慣用的網站設計模式,核心邏輯主要靠 C# 後端處理,即便前端改用 TypeScript 比 JavaScript 更適合寫中型規模以上的程式,但相同功能用 C# 寫開發效率及順手度還是完勝 TypeScript,更何況部分作業(如資料庫存取、檔案讀寫、加解密運算)在性質上屬伺服器端限定。因此我的網站設計,TypeScript/JavaScript 就專注於負責即時性的網頁 UI 呈現及 AJAX 資料交換,核心邏輯主要靠 C# 實作。

在這樣的設計概念下,Angular 內含完整 MVC、UI Routing,對我來說「太多」,功能強大與擴充性的代價是複雜度高,未蒙其利前都屬無效益的學習成本。我心中理想的前端框架只需要專注打理好 UI 層 View 這段就夠了,反正 Model 跟 Controller 交給 ASP.NET MVC 做得又快又好。(雖然改用 Angular 多時,但 Knockout 只專注 View 的定位更接近我的期望,我跟 Knockout 是情深緣淺呀)

回到下一代框架的選擇,來看看 React、Angular 4 與 Vue.js。這三者能在競爭激烈的前端框架界脫穎而出,其效能與功能肯定不在話下,選擇考量聚焦在誰比較符合我的應用情境與個人偏好就好。(網路上有不少比較文章可參考: JavaScript 框架大比拼:Vue、React、AngularJS 與 Angular2 該用哪一個? - TechOrangeReact,Vue,Angular简介 - 简书)

React 可以寫行動版 App 這點很吸引人,但與傳統先做出 HTML 再加掛標籤加上 MVVM 的設計思維截然不同,學習曲線不會太平滑。另外重要一點是我的開發生涯從 ASP 義大利麵時代開始,歷經 WebForm、MVC,到最後連 CSS、JS、HTML 都獨立成檔,已經習慣個別維護不互干擾的優點,看到 React 強推 JSX 又把 HTML、CSS、JavaScript 摻在一起做瀨尿牛丸,在我眼中就像美麗的番邦公主,有種國情不同的隔閡感(個人偏好,勿戰);蘭妃 Angular 4 聲勢浩大,靠過去肯定能吃香喝辣,但有前面提到太過龐大複雜的缺點;見到 Vue.js 只專注於 View 層的理念,加上講求輕巧簡單易學,讓我想起了純元皇后(Knockout),去吧,嬛嬛,就決定是你了。(甄嬛傳混搭寶可夢來著)

現在才開始學習 Vue.js 已經慢了別人一大截,但好處是網路上的學習資源十分豐富,攀附在前輩身上吸經驗值覺得很讚。列舉我這段時間參考的一些資料:

初步心得

  • Vue.js 語法與 Angular 相似度很高,以我過去累積的 NG 經驗,在看完 Kuro 的兩小時簡介影片後就有自己可以出新手村的錯覺。(依過去學習新技術的經驗,八成是錯覺,魔鬼都在他X的細節裡呀!)
  • Kuro 介紹影片裡一些令人驚喜的語法: {{{ rawHtml }}}、.once、.sync(註: 2.3版又加回來了)、limitBy,filterBy,orderBy Filter,2.0 版都拿掉了,不知道該哭該笑,難過的是少了簡便寫法可用,慶幸的是還好不是先狂用一番等升級再連夜改掉。
  • 以 Angular 1 熟手的觀點,Vue 在語法與概念上高度相似,範圍縮小到只觀注 DOM 呈現與互動,少了 NG 裡彈性但複雜的 設計模式(例如: Dependency Injection),很容易上手。基本上花一天把官方教學(中文版)從「介紹」到「過濾器」幾十個章節讀完就差不多夠了,範圍小加上官方教學很完整(而且有中文),跟 Knockout 一樣好上手,在我心中 Vue 更適合成為 NG1 的接班人。
  • 工作上採用 KO 與 NG 專案運作正常,短期內不會積極改版翻寫,手上的 NG 共用元件庫與 CRUD 模版已很順手成熟,正在開發的專案也會繼續使用。對於 Vue 我只打算先在 Coding4Fun 閒暇專案試槍,等累積足夠經驗再評估帶到工作上。
  • 我打算跟大部分 Vue 或前端開發者走不一樣的冷僻小徑: 用 Visual Studio 2017(不是 Visual Studio Code) + ASP.NET MVC + TypeScript 寫 Vue.js,盡量避免扯到 npm、webpack 讓開發背景需求單純化,期望若有朝一日要在公司推廣,阻力可以小一點。
    初步爬文,這麼搞的人真的不多,使用 VSC、npm 寫 Vue 有很多方便的套件模版相助,教學文件充足,要用 VS2017寫又不想碰 npm,有許多環節得靠自己打通。管他的,就先試試吧! (握拳) 但也說不定深入了解後很快就投降了 XD

好一片蒼翠茂盛的森林,但我好像選了左邊這條路。 XD

之前為 KO 跟 NG 寫過三十多篇筆記,依循慣例,Vue 筆記也要來囉~


小黑 ThinkPad 懷舊暨迎新

$
0
0

我人生買的第一台筆電是小黑 ThinkPad X21,CPU 是當年的主流 Pentium III 700MHz,記憶體還插滿封頂直上"384M"呢! 加上 Dock 有的沒的,十幾年前一口氣花掉菜鳥工程師兩個月薪水。仗著經常在外遊走唬爛工作需要,高舉「投資自己」大旗,說穿了也在享受敗家樂趣 XD 當年能從電腦背包掏出一台輕薄小黑做簡報,講得好不好是一回事,光氣勢就取得先機。不過不得不承認,小黑在設計、用料方面真沒話說,踩上去也不會壞的螢幕背蓋、貼心的鍵盤頂燈、比觸控板更精準易控的小紅點、手感絕佳的鍵盤。(難忘美好的敲擊手感,我後來甚至買了 UltraNav 鍵盤接 PC 用)

從防潮箱挖出明代古物,鋰電池早已死透,但插電居然還能開機! 可惜上下鍵12年前就壞了進 BIOS 沒法調時間,再也開不進 Windows XP。

順便曬一下當年買的 Dock(套在筆電背後,讓筆電可以使用印表埠、RS232 序列埠、光碟機、3 1/2" 軟碟機... 一堆年輕人沒聽過的古董玩意兒)、WiFi PCMCIA 網卡,跟 IBM 340MB MicroDrive(CF 記憶卡大小的機械式硬碟),拍完照乖乖放回防潮箱,下回再見天日不知何年何月。

前陣子老 VAIO 先頻頻當機終至無法開機,雖然幸運查出是電池問題,換新電池後已康復。但,冥冥之中一切早有安排,VAIO 壞掉期間我預做了採購新筆電的市場調查評估,在此時家裡 PC 的硬碟恰巧也壞了,內務府順勢稟報,皇后大人老早就在抱怨她的專用 PC 又老又舊又慢,因為少用鍵盤滑鼠常被拔走移做他用,臨時要處理事情連台電腦都沒有… (以下省略三千字,奏摺太長朕沒讀完) 我於是考慮還是該買新筆電,讓修好的老 VAIO 退居第二線回鄉服務以保耳根清靜。好死不死前陣子評估筆電去過 Lenovo 網站,之後爬文滑臉書甚至看自己的部落格,到處都是 ThinkPad 25 週年紀念限時優惠的 AdSense 廣告,鼓吹自訂規格升級記憶體、SSD、三年保固有半價優惠,臨門一腳則是連我要加買放公司的第二顆整流器也一起包了,根本是為我量身打造的促銷,加上之前使用小黑的美好回憶湧上心頭,腦波一弱就…

薑! 薑! 薑! 薑~

我的全新行動配備: ThinkPad T470p i5 7440HQ + 16G RAM + 256G SSD + 2560x1440 IPS。

Mobile01 關於 T470 的精美詳實開箱文很多,在此不班門弄斧了,以上照片就當開箱交差。

試玩一天的感想:

  1. 外殼質感不錯,接縫精密度與上蓋密合度挺好,螢幕的金屬鉸鏈十分穩固但轉動很順(這點大勝老 VAIO T13)
  2. 原本在 i5/i7 間猶豫,但 T470p 若選 i7 一定得配獨顯,對我是發熱耗電卻沒大用的雞肋,評估 i5 7440HQ 與 i7 7800HQ 都是四 Core 頻率相同,只差在 Thread 數是 4 vs 8,非強調多核運算的應用效能 7440 不輸 7800,省錢又不綁獨顯,i5 勝出。試了一些日常使用,感覺效能的確不輸家裡的 i7 2600。
  3. 指紋登入好好用! (顯示為從沒用過的土包子)
  4. 比較懷念 RGB 三原色 IBM 字樣 Thinkpad Logo(見本文第一張照片),每回開機看到 Lenovo 紅底白字的大 Logo,我有面前正攤著一條 LEVIS 牛仔褲的錯覺。
  5. 聽說近代筆電拿掉硬碟動作燈是趨勢,但不知道磁碟現在有沒有在忙,心理超不踏實。 (有在工作列顯示 HD 活動的軟體替代方案,但沒法在當機或開關機關鍵期提供資訊,功能不大)
  6. 要不要從 1920x1280 FullHD 加價升級成 2560x1440 WQHD 當初也很掙扎。有一種說法是 WQHD 塞進 14" 螢幕,字型不調大很傷眼,調大則又失去高解析度的意義。原本想去光華看看實機,問過一輪才知客製規格沒有展示機可以摸,只能賭一把。
    初步試用的感想: 就算字調大可視內容沒有變多,字體細緻度大增倒很賞心悅目,至於實際使用會不會處處是坑留待時間驗證。

    2560 寬度有個優點,左右開弓兩個視窗並列時各自的顯示空間都還堪用,方便對照做事。
  7. 有些舊程式沒考慮到 Windows 字型縮放,版面會跑掉。不過這問題無關WQHD,只要調了 Windows 字型大小就會爆炸,故不列入優劣評比。
  8. 跟 iPad/iPhone/Mac 相比,Windows 字型總不如 Apple 家族好看。
    將瀏覽器預設字型改成思源體後體驗明顯提升,但在某些網站會被保守的 CSS 打回原形,呵!
  9. 字型調大能克服大部分解析度過高的問題,但不是全部。像螢幕擷圖寬度動輒一千多 Pixel,有時必須縮到 25% 以符合常用圖檔尺寸,縮小後的原圖很吃眼力,需要 Zoom In 才好操作,這點有待適應。(下圖為 WQHD 桌面檢視 571x443 圖檔的比例示意)

    (另外有想到兩個 Workaround: 拉到 1280 或 1024 外接螢幕操作,或暫時切換成 1280x720)
  10. 在我心中小黑傳統七列鍵盤的手感已成傳奇,如今改用孤島式鍵盤自然不可能追上,但回饋感比預期的好很多,有點小驚喜。
    忽然想到一事,從櫃子挖出大掃除時本要丟掉的小紅點備品,居然有再派上用場的一天,呵~

Vue筆記2-在 ASP.NET 專案使用 Vue.js

$
0
0

相信大家看完官方教學已經躍躍欲試,就讓我們動手在 VS2017 ASP.NET 專案開個網頁試試 Vue.js。

好消息是 NuGet 上使用 vue 關鍵字就能找到 Vue.js 作者(Evan You, 尤雨溪)自己維護的 Vue 套件,Developer 版本包含較完整詳細的錯誤訊息,如果你沒有自虐傾向,建議裝 Vue.js.Developers.Version。(註: 這兩天剛好發佈了 Vue 2.5 版,NuGet Package 版本近期應該會更新)

安裝 Package 後 /Scripts/vue.js 已就定位,新增一個 Lab2\First.html 加入幾行程式:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
<title>Vue Lab 2</title>
</head>
<body>
<divid="main">
<inputtype="text"v-model="firstName"/>
<inputtype="text"v-model="lastName"/>
<br/>
<span>{{ fullName }}</span>
</div>
<scriptsrc="../Scripts/vue.js"></script>
<script>
new Vue({
            el: "#main",
            data: {
                firstName: "Jeffrey",
                lastName: "Lee"
            },
            computed: {
                fullName: function () {
returnthis.firstName + " " + this.lastName;
                } 
            }
        });
</script>
</body>
</html>

搞定收工,就這麼簡單!

不過如果要玩真的,我還是會選擇用 TypeScript 寫 Vue,初步試了發現若不依循 npm、webpack、VSC 這類主流前端開發模式,單純用 VS2017 + TypeScript + Vue 這條路不如想像中好走... 但我們先不要破壞寫出 Hello World 的喜悅氣氛,下回再談。

Vue筆記3-Vue TypeScript 定義檔簡便做法

$
0
0

要用 TypeScript 寫 Vue 程式,首先要取得 Vue.js TypeScript 定義檔才能享受強型別的好處。

Vue 2.0 釋出於 2016/9/30(最新版為 2.5 版),NuGet 上的 vue.TypeScript.DefinitelyTyped 更新時間為 2016/9/26,版本只到 1.0,己不適用最新版 Vue.js。(前端開發者已多改從 npm 體系取得定義檔,猜想 NuGet 定義檔因此不再更新)

因此,我們可改由 Vue Github 取得最新定義檔: https://github.com/vuejs/vue/tree/dev/types

Github 上的 Vue TypeScript 定義檔除了 vue.d.ts 之外還有好幾個檔案,有個 index.d.ts,同時每個 d.ts 出現的大量 export/import,代表它們 TypeScript Module 系統,你的 TypeScript 檔案必須先 import { Vue } from "vue" 才認得 new Vue()。過去用 TypeScript 寫 Knockout、Angular 1,NuGet 裝好 TypeSciprt 定義檔開始幹活的做法已不適用 Vue 2.0 的 TypeScript 開發。然而,一旦使用 import 會讓我們的 .ts 變成 Module 的副作用,得引進 AMD/RequireJS 或使用 Browserify或 Webpack 等工具打包才能用在網頁上,十分麻煩。(延伸閱讀: TypeScript Module 簡單練習 )

我希望 TypeScript 寫 Vue 可以比照 jQuery/Angular 1 下載 d.ts 就開心寫 Code,不要改變過去單純的開發模式,為達成此理想,得花點功夫處理定義檔。

將前述的一堆 d.ts 檔放入 Scripts/typings/vue 目錄,但由於它採用 Module 系統,不像 jQuery.d.ts 只要放在 typings 目錄就直接生效,我們需要加點工。

如果你跟我一樣不想為了喝牛奶養牛,為了加定義檔被迫使用 TypeScript Module 系統(當然,使用 Module 有很多額外好處,但需付出複雜化的代價,在簡單應用情境有揮牛刀之嫌),請在 Scripts/typings/vue 新增一個 vue-global.d.ts,加入以下內容 :

//https://stackoverflow.com/a/43257916/288936
import * as _vue from "./vue";
 
declare global {
const Vue: typeof _vue.Vue;
}

vue-global.d.ts 將引用 vue Module 並將 Vue 定義開放為全域範圍。

接著如下圖所示,一般 .ts 不必使用 import 也能大方享用 Vue 強型別囉~

JavaScript 中文排序問題

$
0
0

今天才發現 JavaScript 中文字串排序有個大問題! 下圖是 KendoGrid 在 Chrome 使用 JavaScript 排序的結果,如圖所示,一到七由小到大排序結果為一、七、三、二、五、六、四,既不是依筆劃,也不是依注音: (SQL 的中文定序就區分筆劃跟注音,例如: Chinese_Taiwan_Stroke_CI_AS vs Chinese_Taiwan_Bopomofo_CI_AS 參考)

爬文後得知這是 JavaScript 中文字串排序的己知問題(我 Lag 真大),字串型別有個 localeCompare()可傳入語系參數進行比較,貌似能解決問題但實際不行。

我寫了測試程式如下。測試字元陣列擷取自 BIG5 字碼表,前面的 BIG5 內碼較小,筆劃較少,並插花加入黑、暗兩個超過五劃的字。分別測試三種排序方法,分別是 直接使用 sort()、sort() 搭配 localeCompare() 不指定語系、sort() 搭配 localeCompare() 指定語系,排序結果以 document.write() 印出。

<html>
<bodystyle="font-size: 9pt">
<script>
var raw = "一乙丁七乃九二人八力十三丈久元六公四黑暗";
var ary = [];
for (var i = 0; i < raw.length; i++)
  ary.push(raw.substr(i, 1));
document.write("原始順序(BIG5/筆劃)<br />")
document.write(JSON.stringify(ary, null ," "));
document.write("<hr />")
ary.sort();
document.write("內建 sort()<br />")
document.write(JSON.stringify(ary, null ," "));
document.write("<hr />")
ary.sort(function(a,b) { return a.localeCompare(b); });
document.write("localeCompare 排序<br />")
document.write(JSON.stringify(ary, null ," "));
document.write("<hr />")
ary.sort(function(a,b) { return a.localeCompare(b, "zh-TW"); });
document.write("localeCompare zh-TW 排序<br />")
document.write(JSON.stringify(ary, null ," "));
</script>
</body>
</html>

以下擷圖分別是 Edge、IE、Firefox 的執行結果,如圖所示,內建 sort() 順序即一開始 KendoGrid 案例的排序結果,而 localeCompare 排序與 localeCompare zh-TW 排序結果相同,雖然與 BIG5 順序有一些出入,至少有遵循筆劃由少到多的順序。

內建 sort() 應是依據 UTF-8 Byte 排序,數字部分依序為一、七、三、二、六、四,使用 Encoding.UTF8.GetBytes() 轉成位元組可證實推測:

同樣的測試在 Chrome 及 Safari 就精采了,sort() 與 localeCompare() 不指定語系的排序結果相同,即上述 UTF8 轉 Byte 後的順序;而 localeCompare() 指定 zh-TW 語系的排序結果讓人莫名其妙,暗字衝到第一個,我說不出是依什麼規則。(2017-10-20 補充,zh-TW 被 Chrome 誤為簡體漢語拼音,請見文章留言及文末補充說明)

另外我還試了用 C# 排序,其結果與 Edge、IE、Firefox 的 localeCompare() 排序順序一致。

由以上測試,我的結論是 localeCompare() 無法跨瀏覽器實現符合預期且一致的中文字串排序,可能因瀏覽器實作不同出現非預期結果,若求保險還是在 C# 或資料庫端處理排序為上。

2017-10-20 更新

貼文後蒙網友 Hsi-Lien Chin、diberium 留言解惑,在 Chrome 使用 localeCompare 排序正體中文時語系參數應傳入 zh-Hant 或 zh-Hant-TW,實測 Chrome 執行 localeCompare(b, "zh-Hant") ,結果就與 Edge/IE/Firefox 一致了。特此感謝。

IE showModalDialog + IFrame 內嵌網頁無法複製貼上

$
0
0

今天遇到的奇妙 IE 問題。同事報案,有個網頁單獨開啟操作正常,使用 ModalDialog 顯示時無法複製貼上。( Ctrl-C/Ctrl-V 快速鍵與右鍵選單同步失效)

深入研究後發現這現象在特殊條件下才會發生: 網頁 A 先以 showModalDialog 顯示網頁 B,網頁 B 以 <iframe> 內嵌來自另一站台的網頁 C,此時在網頁 C 上將無法執行複製貼上作業。

使用以下程式重現問題。

Parent.html

以<iframe>內嵌跨站台(localhost vs 127.0.0.1,視為不同站台) Child.html,另有按鈕以 showModalDialog() 彈出 Dialog.html。

<!DOCTYPEhtml>
<html>
<head>
<script>
function test(url) {
            showModalDialog(url, "",
"dialogTop:10px;dialogLeft:10px;dialogHeight:400px");
        }
</script>
</head>
<body>
<h4>Parent</h4>
<buttononclick="test('dialog.html')">Modal Dialog</button>
<br/>
<iframesrc="http://127.0.0.1/aspnet/child.html"></iframe>
</body>
</html>

Dialog.html

有兩個 <iframe>,一個內嵌同站台的 Child.html,一個內嵌跨站台的 httq://127.0.0.1/aspnet/child.html,以方便對照比較。

<!DOCTYPEhtml>
<html>
<body>
<h4>
<script>document.write(location.href)</script>
</h4>
<iframesrc="child.html"></iframe><br/>
<iframesrc="http://127.0.0.1/aspnet/child.html"></iframe>
</body>
</html>

Child.html

內含 <textarea> 方便測試複製貼上功能

<!DOCTYPEhtml>
<html>
<body>
<h4>
<script>document.write(location.href)</script>
</h4>
<textarea>ABC</textarea>
</body>
</html>

實測結果如下:

測試1 Parent 內嵌跨站台 Child 可複製貼上
測試2 Dialog 內嵌同站台 Child 可複製貼上
測試3 Dialog 內嵌跨站台 Child 無法複製貼上

經查該問題是測試台配置特殊造成,並可藉由網頁移入同站台避免,而 IE + ModalDialog 設計方式將逐步淘汰,故不花精神深入研究,僅記錄此一特性備查,結案。

參考: IE8-IE9 Copy-Cut-Paste doesn't work on cross-domain IFRAME in Showmodaldialog window

【茶包射手日記】只能跑 32 位元的 AnyCPU .NET 程式

$
0
0

測試某個 COM+ 元件應用專案,開發者所附的範例專案測試成功,我自己新增 Console Application 或 Windows Form 專案則卡在找不到 Registry 無法執行。強烈懷疑與 x86/x64 有關,由於只有註冊 64 位元 COM+,專案跑 x86 找不到 Registry 是意料中事,但詭異之處在於我已確認過範例專案跟我新增的專案都是設 Any CPU 無誤,甚至放在同一個 Solution 測試,卻一個成功一個失敗。

實測將新增 WinForm 或 Console 專案平台目標(Platform Target)改為 x64 可排除問題,但無法解答為什麼範例專案設 Any CPU 可以,我卻得寫死 x64 才行的疑惑。

挖出 CorFlags 工具一探究竟,發現一個奇妙差異,有個沒學過的新旗標: 32BITPREF,我新增的專案被設為 1,範例專案則為 0。

有了這項新發現,重新檢查 Visual Studio 的專案設定,這才看出玄機,兩個專案都設定 Any CPU, 但範例專案沒勾選 Prefer 32-bit,我新增的專案有。

用關鍵字查詢,我學到了新知識。參考: Make sure "Prefer 32-bit" option is turned off for .NET 4.5 executables

原來這是 Visual Studio 2012 起針對 .NET 4.5 專案的新增選項,在新增 WinForm、WPF、Console 專案時,預設為 Any CPU + Prefer 32-bit。

AnyCPU + Prefer 32-bit 在 Windows 平台永遠跑 32 位元模式,跟平台目標設成 x86 的行為相同,唯一差別在於 AnyCPU + Perfer 32-bit 可以在 ARM 機器執行。參考: What AnyCPU Really Means As Of .NET 4.5 and Visual Studio 11

換句話說,AnyCPU + Prefer 32-bit 骨子裡根本是 x86,取消 Prefer 32-bit 後才是我原本想像的那個 32/64 都能跑的 AnyCPU。 下回看到 .NET 4.5+ 設定 AnyCPU 時,記得要確認沒有勾選 Prefer 32-bit,才代表 .NET 程式能 32/64 通吃。

又上了一課~

為 PDF、Office 檔案產生文字索引

$
0
0

遇到文件檔全文檢索需求,打算用 SQL Server 全文檢索或 lucent.net實現,無論使用何者都免不了從 Word、Excel、PowerPoint 或 PDF 檔萃取純文字內容建立索引的程序。經簡單評估,使用微軟的 IFilter 介面應是較簡單可行的做法。搜索引擎面對的檔案種類五花八門,不太可能涵蓋各種檔案格式,知道從中取出文字內容的方法,IFilter 制定統一程式介面,不管檔案格式為何,只要廠商或第三方有提供專屬 IFilter,搜尋引擎便可使用呼叫統一的 API 方法傳入檔案名稱取得文字內容,再為文字建立索引方便日後查詢。

專案面臨的檔案種類還算單純,只需涵蓋 Office 文件及 PDF 檔,而二者都有現成的 IFilter 可用:

  • PDF iFilter 64 下載
  • Office 2010 Filter Packs 下載
    包含 Legacy Office Filter (97-2003; .doc, .ppt, .xls)、Metro Office Filter (2007; .docx, .pptx, .xlsx) 、Zip Filter、
    OneNote filter、Visio Filter、Publisher Filter、Open Document Format Filter
    有 32/64 版本可選擇,由於 PDF iFilter 為 64,建議 Office Filter 也裝 64bit

簡單如何說明由 Registry 找出副檔名對應 IFilter 的原理。首先在 NTLM\SOFTWARE\Classes\.副檔名 可以找到 PersistentHandler 機碼,預設值指向一個 GUID:

在 NTLM\SOFTWARE\Classes\CLSID\{PersistentHandler GUID}\PersistentAddinsRegistered 可以找到名稱是 GUID 的機碼,預設值再指向另一個 GUID:

繼續在 CLSID 找尋該 GUID,InprocSever32 預設值即指向其 IFilter DLL: (下圖以 PDF iFilter 11 為例)

同理,我們也能找到 Office Filter 的實際位置:

上述的 Registry 大地遊戲過程有點繁瑣,加上爬文發現 PDF iFilter 有些眉角要克服,我找到網友寫好的懶人包元件(參考: Adobe PDF IFilter 11 - My Technical Diary),經實測只需幾行程式可通吃 Office/PDF,方便許多。

為了測試,我準備了doc, docx, ppt, pptx, xls, xlsx, pdf 七種檔案,內容都只有單純一行"XXXX測試"字樣。

程式如下:

        [STAThread] 
staticvoid Main(string[] args) 
        {
 
            List<string> names = 
"測試.pdf,測試.docx,測試.doc,測試.xlsx,測試.xls,測試.pptx,測試.ppt"
                .Split(',').ToList();
 
            names.ForEach(f => 
            { 
                Console.WriteLine($"[{f}]"); 
using (var reader = 
new EPocalipse.IFilter.FilterReader($"e:\\tests\\{f}")) 
                { 
string text = reader.ReadToEnd(); 
                    Console.WriteLine(text); 
                } 
            });
 
            Console.Read();
 
        } 

測試成功!


Oracle 自訂函式查詢加速密技–Scalar Subquery Caching

$
0
0

在 SELECT 指令對欄位執行自訂函式行運算通常很傷效能,但實務上無法完全避免。查詢一萬筆資料代表要呼叫自訂函式一萬次,若函式包含資料表查詢,如同在迴圈裡跑 SQL,是典型的效能殺手,經驗裡也是許多複雜查詢逾時的主因。

見識到同事露了一手,簡單加幾個字元一口氣將內含自訂函式的 Oracle SELECT 加速數十倍! 瞠目結舌之餘,立馬實驗證明效果驚人,特筆記並分享如下。

以下為實驗環境,JeffTest 資料表有 IDX, N 兩個 NUMBER 欄位,先塞入 256 筆資料,IDX 由 0 - 255,N 則是 IDX 除 4 的餘數,依序為 0, 1, 2, 3, 0, 1, 2, 3, ...。

我寫了一個無聊的自訂函式 FN_SQR 計算傳入數值的平方,為了讓效能差異明顯一點,還加了一個 dbms_lock.sleep(0.01) 延遲 0.01 秒,順便加上 dbms_output.put_line(n) 方便觀察執行次數:

createor replace function FN_SQR(n number) return number is
  FunctionResult number;
begin
  FunctionResult := n * n;
  dbms_lock.sleep(0.01);
  dbms_output.put_line(n);
return(FunctionResult);
end FN_SQR;

以下是兩個結果相同的查詢,第一個查詢是最直覺的寫法,SELECT 256 筆資料的 IDX 及 FN_SQR(N);第二個查詢動點手腳,將 FN_SQR(N) 改成子查詢 (SELECT FN_SQR(N) FROM DUAL),猜猜二者效能差多少?

2.655 秒 vs 0.145 秒! 快了近 20 倍,為什麼?

看一下 dbms_output.put_line() 的輸出結果就知道差異何在,SELECT IDX, FN_SQR(N) 呼叫了 FN_SQR() 256 次,而 SELECT IDX, (SELECT FN_SQR(N) FROM DUAL) 只執行了四次,0,1,2,3 各一次,參數相同的函式呼叫可使用 Cache 結果,不必重複執行。

這裡的效能提升來自 Scalar Subquery Caching– Oracle 會為 SELECT 出現的 Scalar Subquery (只傳回單一值的子查詢) 在記憶體建立一個 Hashtable,整理不同參數與查詢結果的對應表。若參數欄位值先前出現過,即可直接由 Hashtable 取值不用重新計算。在這個案例中,N 欄位只有 0,1,2,3 四種變化,故只有前四筆資料需要計算,之後的 252 筆全由 Hashtable 取值。

換句話說,使用 SELECT 自訂函數(參數欄位) FROM DUAL 技巧將自訂函式呼叫包成子查詢,就可享受 Oracle Scalar Subquery Caching 的加速效果,資料筆數愈多、參數欄位值重複性愈高、自訂函式運算愈耗時,就愈能感受到明顯的效能提升。使用 Oracle 的同學們別錯過這招加速技。

2017 根除小兒麻痺扶輪社公益路跑

$
0
0

避暑沈寂了大半年,下半年第一場馬拉松登場 - 2017 扶輪社根除小身麻痺公益路跑。(學到新單字 POLIO - 小兒麻痺,目前全球僅存阿富汗及巴基斯坦仍有病例,扶輪社長期致力於讓小兒麻痺從地球絕跡,並可望於今年提前達標)

跟觀音山馬一樣從微風運河出發,繞河畔一周近 4 公里再進入河濱。多雲無風,溫度 22 度左右,是慢跑的好天氣。

六點多,朝陽從雲間探出頭來,難得一睹日出美景~

跑淡水河左岸必經的關渡大橋,拍到有點膩了,但沒拍又怪怪的,所以來一張吧。

看到有人開著小船(如下圖)在河中央撒網捕魚,心中浮現兩個問題: 1) 捕到的魚要拿到市場賣嗎? 2) 在淡水河開船捕魚合法嗎? 需要執照嗎? 我可以弄艘船開著玩嗎?
解惑:
1) 近出海口污染較少且魚獲頗豐,多銷往台北市場(參考),關渡大橋上游因污染嚴重則禁捕(參考)
2) 的確有【動力小船駕駛執照】這種東西,坊間也有駕訓班,如果有閒錢,養一艘船也不是遙不可及的事
3) 河川捕魚行為屬漁業法管轄範圍,需申請

跑過淡水河左岸多次,但最北只跑到媽媽嘴,這回倒是踏上沒來過的八里渡船頭,碰上沙雕展,胡亂拍了些照片也算意外收獲。

往北一路都是沒見過的風景,渡船頭再過去有好些景觀餐廳,全馬半馬在此分道揚鑣,半馬折返,全馬繼續前進。

再往北跑到接近十三行博物館換全馬折返,但不是原地調頭迴轉,而要沿著林間小道繞一圈迴轉,此時有樹蔭有涼風,很讓人陶醉的一段路程。

從八里遠眺對岸,可以看到淡水福容飯店。

今年跑運不錯,賽事當天幾乎都是微雨到微陰的好天氣,雖然一度陽光露臉,但更多是陽光從雲隙穿出的壯觀耶穌光,哈里路亞~

折返通過會場還要向南跑到忠孝橋第二次折返。早上車行經過重陽橋到會場,現在又從會場跑回重陽橋,等下折返後要再從重陽橋跑回會場,想想自己都覺得好笑! XD

太久沒跑長距離,平日練跑最多只有 10K,一口氣加碼到 42 大小腿不意外都跳出來抗議,明明沒跑快也又僵又酸,腳趾還起了一個水泡,但為了不負好天氣,還是連滾帶爬跑進五小時,以 4:58:19 再下一馬。

說說心得: 不到千人(本場約650)的小比賽還是我的最愛,開跑前十分鐘有實體廁所上還不用排隊,水站滿滿的水、舒跑、維大力、香蕉、葡萄(超甜)、西瓜、小蕃茄、蘇打餅、小蛋糕... 我還喝到綠豆湯、仙草冰、檸檬汁。然後,在賽道旁看到這一幕,誰好意思隨手亂扔水杯? (PS: 我有自備水杯,更上層樓) 整場賽道十分乾淨整潔,為大會的用心跟跑友們的素質按個讚~

賽後便當是豪邁的滷雞腿,在會場看到廚餘回收、垃圾分類區,果然也很環保。

沈甸甸的黃金色完賽獎牌深得我心,色澤與份量頗有金磚的氣勢,而齒輪造型符合工程師調性又巧妙減少土豪感,I Love It,哈!

 

【茶包射手日記】Win7 + Chrome 才看得到的網頁特殊字元

$
0
0

使用者報案網頁多了一個像 L 的字元,在同事的電腦可重現,但在我的機器卻看不到。進一步測試,發現這個像 L 的字元在同事的 Windows 7 要用 Chrome 才會出現,用 IE 看不到;而在我的 Windows 10 上,不管用 Chrome、IE 還是 Firefox 都看不到。透過 F12 開發者工具鎖定可疑字元,複製貼上到中文編碼解析工具,發現原來是 ASCII 0x03 字元,有可能是網頁製作者從 Office 文件複製貼上被夾帶過來的。ASCII 0x03 這類控制碼字元,看不見是合理的,為何在 Win7 + Chrome 會現形則是個謎。

於是我寫了一個測試網頁,列舉從 ASCII 01-65("A") 的 65 個字元。

StringBuilder sb = new StringBuilder();
sb.AppendLine("<html><body><table>");
for (int i = 1; i < 65; i++)
{
char c = (char) i;
    sb.AppendLine($"<tr><td>{i}</td><td>{c}</td></tr>");
}
sb.AppendLine("</table></body></html>");
System.IO.File.WriteAllText("e:\\ASCII.html", sb.ToString());

用 Notepad++ 看會像這樣,那些 SOH、STX、ETX 就是 ASCII 32 以下的隱形控制字元。

用我的電腦實測,不管 Chrome、IE 還是 Firefox,都看不到空白字 (ASCII 32) 之前的控制字元。

改用 Windows 7 + Chrome 檢視,原本該隱形的字元都冒出來了。如下圖,ASCII 1-4 分別是方框的左上、右上、左下、右下角,問題網頁內容的 ASCII 3 是左下角,才被使用者誤認為 L。進一步使用 F12 開發者工具檢查,得知關鍵在於 ASCII 控制碼所在的 <TD> 使用了 Gulim 這個字型,Gulim 這個韓文字型有個特色,ASCII 控制碼被對應到可見的特殊符號,造成我們遇到的狀況。

我查不到關於 Chrome 此一行為的官方說明,由找到的文章,Gulim 似乎是 Windows 7 Chrome 針對 CJK(中日韓) 字型無對應時的 Fallback 字型。參考:

  • 最佳 Web 中文默認字體 - 問問題
    ... 在英文 Win7 下,只要 charset=“gbk”, 當 font-family 為 arial, sans-serif 時, 是 fallback 到瞭韓文字體 Dotum/Gulim(gulim.ttc)來顯示 ...
  • Chrome 3.0的字有夠醜! – 蘋果豬日記V4
    ... 把Arialuni殺掉之後,接下來fallback的順序依序是:Arial Unicode > Simsun > Gulim > MS Gothic > Mingliu
    分別是Unicode預設>簡體中文預設>韓文預設>日文預設>正體中文預設。...

由以上這些線索,大致可以推論「在 Win 7 Chrome 中,小於 ASCII 32 的控制字元會使用 Gulim 這個字型顯示而變成可見的符號。」

結案~

Notepad++ 7.5 取消預設安裝 Plugin Manager

$
0
0

在新安裝的 Notepad++找不到 Plugin Manager 可用,先前遇過安裝 64bit 版本有些 Plugin(插件) 無法使用,但確定我裝的是 32bit 版本沒錯,所以是哪邊出了問題? (什麼? 你沒聽過 Notepad++,快安裝它取代記事本 Notepad 吧! 好用豈止十倍? 而且還是台灣開發者的開放原始碼專案,舉世聞名獲獎無數,又一項台灣之光! 維基百科)

Release Note 載明預設安裝的插件只剩 NppExport、 Converter、Mime Tool,確實未包含 Plugin Manager。 再爬文得知是作者厭惡 Plugin Manager 夾帶廣告,自 2017 年 8 月 7.5 版起將它移除,並承諾儘快寫出官方版本取代它。

You may notice that Plugin Manager plugin has been removed from the official distribution. The reason is Plugin Manager contains the advertising in its dialog. I hate Ads in applications, and I ensure you that there was no, and there will never be Ads in Notepad++.

A built-in Plugin Manager is in progress, and I will do my best to ship it with Notepad++ ASAP.

由 Notepad++ 討論串追出故事大概是這樣:

nppPluginManager需要伺服器提供清單及插件檔下載,故有主機及頻寬的營運成本(總流量 127GB/月,若由免費 CDN 服務廠商 CloudFlare扛下大部分下載流量,仍需要約 1 CPU/2GB RAM/每月 2GB 流量的主機資源),Plugin Manager 作者因此在操作介面放上贊助廠商 Logo 廣告交換免費使用主機服務(如下圖所示,Why is this here? 連結可看原委)。但 Notepad++ 作者認為此舉違返了開源免費精神,斷然從內建安裝移除了 Plugin Manager,即使 nppPluginManager 作者事後找到免費贊助商願意取消廣告,但似乎已無轉圜空間。

目前的狀況,要安裝插件我們有兩種選擇:

第一種選擇是自己手動把 Plugin Manager 裝回來。到 Github 下載 nppPluginManager ZIP 檔,解壓縮後有兩個目錄,將 plugins 下的 PluginManager.dll 放到 C:\Program Files (x86)\Notepad++\plugins,將 updater 下的 gpup.exe 放在 C:\Program Files (x86)\Notepad++\,Plugin Manager 就回來了。

第二種選擇則是自己下載插件手動安裝。在 Notepad++ 官網有一份完整的 Plugin 清單,找到所需項目,連結到下載網頁取得檔案,再依各插件的安裝說明將所需檔案放到 C:\Program Files (x86)\Notepad++ 的 plugins 目錄及程式主目錄即可。

使用 Open XML SDK 在 Word 插入圖片

$
0
0

客戶提了需求,套表應用想在文件範本的特定位置插入圖片。花了點時間研究如何用 OpenXML SDK 實現,以下是我的筆記。

Word docx 其實是一個 ZIP 檔,文件主體是一份 XML。如果你有興趣研究,可以將 docx 更名成 zip 解壓縮(或在 docx 按右鍵選單直接用 7-Zip 解開),其中 word 資料夾有一個 document.xml,打開它會發現 Word 文件是由一堆 <w:p> 包 <w:r> 組成,其中 <w:p> 對應到 Open XML 中的 Paragraph,<w:r> 則對應到 Run

例如以下的 Test.docx:

解壓縮 Test.docx 後檢視 word\document.xml,可看到 Paragraph 文字內容被拆得很細,像「圖片插入位置 –> CAT」幾個字就被拆成 6 個 Run,字型顏色大小不同要拆成不同的 Run 無可厚非,但連中文、英文、符號也被獨立切開,甚至「圖片插入」與「位置」被分成兩個 Run。如何拆成 Run 對文件編輯者沒有任何影響,Word 的確可以全權做主,但使用 Open XML SDK 讀取時就得留心其中差異。

Open XML SDK 官方文件有一個完整的插入圖片範例: 如何: 將圖片插入文書處理文件 (開啟 XML SDK),照方煎藥就能在文件末端插入圖片。不過,我遇到的需求還需要調整圖檔大小及插入位置,便試著改寫較有彈性的版本。

我先將圖片內容及設定抽取成獨立類別 ImageData,偵測 附檔名決定 OpenXML ImagePartType(我只打算支援 JPG、PNG、GIF、BMP),由圖檔寬度高度 Pixel 數除以 DPI(預設300) 換算出以公分為單位的預設寬度與高度,但圖片寬高允許自由調整。Open XML 使用 EMU 作為長度單位,使用時公分要乘上 360000 轉成 EMU 以符合 OpenXML 要求。我選擇不直接插入圖片,而是透過一個 GenerateImageRun() 公用函式將圖片轉成 Run,開發者再視需要決定該插入到文件末端、特定位置或置換現有 Run。

publicclass ImageData
    {
publicstring FileName = string.Empty;
publicbyte[] BinaryData;
public Stream DataStream => new MemoryStream(BinaryData);
public ImagePartType ImageType
        {
            get
            {
                var ext = Path.GetExtension(FileName).TrimStart('.').ToLower();
switch (ext)
                {
case"jpg":
return ImagePartType.Jpeg;
case"png":
return ImagePartType.Png;
case"":
return ImagePartType.Gif;
case"bmp":
return ImagePartType.Bmp;
                }
thrownew ApplicationException($"Unsupported image type: {ext}");
            }
        }
publicint SourceWidth;
publicint SourceHeight;
publicdecimal Width;
publicdecimal Height;
publiclong WidthInEMU => Convert.ToInt64(Width * CM_TO_EMU);
publiclong HeightInEMU => Convert.ToInt64(Height * CM_TO_EMU);
privateconstdecimal INCH_TO_CM = 2.54M;
privateconstdecimal CM_TO_EMU = 360000M;       
publicstring ImageName;
public ImageData(string fileName, byte[] data, int dpi = 300)
        {
            FileName = fileName;
            BinaryData = data;
            Bitmap img = new Bitmap(new MemoryStream(data));
            SourceWidth = img.Width;
            SourceHeight = img.Height;
            Width = ((decimal)SourceWidth) / dpi * INCH_TO_CM;
            Height = ((decimal)SourceHeight) / dpi * INCH_TO_CM;
            ImageName = $"IMG_{Guid.NewGuid().ToString().Substring(0, 8)}";
        }
public ImageData(string fileName, int dpi = 300) : 
this(fileName, File.ReadAllBytes(fileName), dpi)
        {
        }
    }
publicclass DocxImgHelper
    {
publicstatic Run GenerateImageRun(WordprocessingDocument wordDoc, ImageData img)
        {
            MainDocumentPart mainPart = wordDoc.MainDocumentPart;
 
            ImagePart imagePart = mainPart.AddImagePart(ImagePartType.Jpeg);
            var relationshipId = mainPart.GetIdOfPart(imagePart);
            imagePart.FeedData(img.DataStream);
 
// Define the reference of the image.
            var element =
new Drawing(
new DW.Inline(
//Size of image, unit = EMU(English Metric Unit)
//1 cm = 360000 EMUs
new DW.Extent() { Cx = img.WidthInEMU, Cy = img.HeightInEMU },
new DW.EffectExtent()
                         {
                             LeftEdge = 0L,
                             TopEdge = 0L,
                             RightEdge = 0L,
                             BottomEdge = 0L
                         },
new DW.DocProperties()
                         {
                             Id = (UInt32Value)1U,
                             Name = img.ImageName
                         },
new DW.NonVisualGraphicFrameDrawingProperties(
new A.GraphicFrameLocks() { NoChangeAspect = true }),
new A.Graphic(
new A.GraphicData(
new PIC.Picture(
new PIC.NonVisualPictureProperties(
new PIC.NonVisualDrawingProperties()
                                         {
                                             Id = (UInt32Value)0U,
                                             Name = img.FileName
                                         },
new PIC.NonVisualPictureDrawingProperties()),
new PIC.BlipFill(
new A.Blip(
new A.BlipExtensionList(
new A.BlipExtension()
                                                 {
                                                     Uri =
"{28A0092B-C50C-407E-A947-70E740481C1C}"
                                                 })
                                         )
                                         {
                                             Embed = relationshipId,
                                             CompressionState =
                                             A.BlipCompressionValues.Print
                                         },
new A.Stretch(
new A.FillRectangle())),
new PIC.ShapeProperties(
new A.Transform2D(
new A.Offset() { X = 0L, Y = 0L },
new A.Extents() { 
                                                    Cx = img.WidthInEMU, Cy = img.HeightInEMU }),
new A.PresetGeometry(
new A.AdjustValueList()
                                         )
                                         { Preset = A.ShapeTypeValues.Rectangle }))
                             )
                             { Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" })
                     )
                     {
                         DistanceFromTop = (UInt32Value)0U,
                         DistanceFromBottom = (UInt32Value)0U,
                         DistanceFromLeft = (UInt32Value)0U,
                         DistanceFromRight = (UInt32Value)0U,
                         EditId = "50D07946"
                     });
returnnew Run(element);
        }
    }

有了公用函式,要插入圖片就簡單了,試試將圖片加在文件末端:

staticvoid Main(string[] args)
{
    var workFileName = $"Test-{DateTime.Now:HHmmss}.docx";
    File.Copy("Test.docx", workFileName);
using (WordprocessingDocument document =
        WordprocessingDocument.Open(workFileName, true))
    {
        var cat2Img = new ImageData("Cat2.png");
        var imgRun = DocxImgHelper.GenerateImageRun(document, cat2Img);
        document.MainDocumentPart.Document.Body.AppendChild(new Paragraph(imgRun));
    }
}

測試成功! 測試圖片尺寸為 300x300,因預設 300 DPI,300 Pixel 等於 1 吋,故變成 2.54cm x 2.54cm 的圖片,置於文件最後一段 Paragraph。

接著再來測試置換現有內容。用 Document.Body.Descendants() 取回 document.xml 所有 XML 節點,如果我們確定 CAT 文字被包在單一 <w:r> 中(小訣竅: 使用純英文並套用不同字型可確保該段文字自成一個 Run),用 LINQ .Single(o => o.Local == "r" && o.InnerText == "CAT") 可找到 CAT 所在的 Run,接著將其 InnerXml 換成圖片的 InnerXml,CAT 文字就變成圖檔囉~ (本例順便示範改變圖片寬高為 1cm x 1cm)

staticvoid Main(string[] args)
{
    var workFileName = $"Test-{DateTime.Now:HHmmss}.docx";
    File.Copy("Test.docx", workFileName);
using (WordprocessingDocument document =
        WordprocessingDocument.Open(workFileName, true))
    {
        var cat1Img = new ImageData("Cat1.gif")
        {
            Width = 1,
            Height = 1
        };
        var imgRun = DocxImgHelper.GenerateImageRun(document, cat1Img);
//找到 CAT 所在的 Run
        var runCAT = document.MainDocumentPart.Document.Body.Descendants()
            .Single(o => o.LocalName == "r"&& o.InnerText == "CAT");
 
//將 InnerXML 置換成圖片 Run 的 InnerXML
        runCAT.InnerXml = imgRun.InnerXml;
    }
}

測試成功,YA!

Viewing all 428 articles
Browse latest View live