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

【茶包射手日記】網頁 Enter 鍵誤擊刪除鈕疑雲

$
0
0

同事報案,在「以 MVVM 清單實作資料編輯介面」的經典應用場景(Knockout版範例Angular版範例)遇見怪事。新増一筆資料後,將焦點移至 <input type="text"> 輸入欄位,若按下 Enter 資料會莫名消失,按一次消失一筆…

程式用了 jQuery、Bootstrape、Knockout、KendoUI,加上一堆自訂程式庫,無法斷定是誰造成,只好抽絲剝繭,以能重現問題為原則,將掛載的程式庫及 DOM 元素一一拆除。歷經一番功夫,最後竟發現是個 HTML 基本觀念,某自以為資深的網頁設計老鳥,乖乖上了一課。

用一個超精簡範例重現問題:Live Demo

將焦點停在 Input A,按下 Enter 鍵會觸發 Button A 的 onclick 事件;但同樣狀況則不會發生在 Input B 與 Button B 上,關鍵在於 Input A 與 Button A 被包在 <form></form> 之中,而 Input B / Button B 沒有:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>Enter on form</title>
</head>
<body>
<form>
<fieldset>
<legend>Inside Form</legend>
<inputtype="text"value="Input A"/>
<buttononclick="alert('Button A Clicked');return false;">Button A</button>
</fieldset>
</form>
<fieldset>
<legend>Outside Form</legend>
<inputtype="text"value="Input B"/>
<buttononclick="alert('Button B Clicked')">Button B</button>
</fieldset>
</body>
</html>

追究原因,「在輸入欄位按 Enter 送出表單」幾乎是所有瀏覽器的預設行為(感覺是網頁設計基本常識,但先前以寫 AJAX 跟 SPA 為主,碰 Form 的經驗不夠多,沒遇過還真就沒學到),Enter 送出表單可以理解,但瀏覽器送出前還幫忙按下 <form> 裡的第一顆按鈕倒是出乎意料。(在以上範例,Button A onclick alert 完要 return false,不然會觸發表單送出行為)再回頭觀察一開始的展示,按 Enter 被刪除的永遠是第一筆,由此可證。

2017-6-29補充: 貼文後不少網友提醒我 <button> 在各瀏覽器的預設 type 可能不同,而我這也才發現對 Chrome 與 IE 而言,<button> type 預設為 submit,是按 Enter 會連帶觸發按鈕的原因,改為 type="button" 後不再被 Enter 觸發點擊,但在這個案例中要防止表單被送出。感謝大家的回饋~(再上一課)

至於解決方案,用 prevent enter submit form 可以 Google 到一大票詢問與討論,常見做法是攔截 document 或 input 的 keypress 事件,在遇到 Enter 鍵時取消動作。但本案例倒不用這麼麻煩,該段程式以 AJAX 方式運作,用不到 <form> ,是因為寫在 ASP.NET WebForm 才被包在 <form> 中,只需將該段 HTML 移至 <form> 之外,問題即刻消失。

撇開這次遇到的特殊狀況,回到「按 Enter 會送出 Form 」行為上,其實存在爭議。Stackoverflow 上有一堆人詢問如何取消,卻也有人強力主張不該改掉(The Enter Key should Submit Forms, Stop Suppressing it),我則喜歡 StackExchange 的這篇觀點,作者以 Facebook 提供 Press Enter to send 選項為例(過去式 ,現已改版),認為交由使用者決定才是最佳選擇,附議!


CODE - 數字轉英文 C# 函式庫

$
0
0

專案遇上數字轉成英文的需求,例如:1234 需轉為 one thousand two hundred thirty four。

網路上有許多解決方案,程式範例、現成函式庫都有,選擇眾多之下,透過 NuGet 即裝即用才是王道。我找到一個,在 NuGet 搜尋 NUT,第一筆 Nut, Number To Text Converter & Money To Text Converter:

程式為 Open Source, 使用說明與原始碼在 Github 上: https://github.com/emrahyumuk/NUT-number-to-text。使用說明略嫌簡略,「Use the source, Luke!」 所有的疑問看完程式碼全明白了。

Nut 程式庫在 long、int 及 decimal 加了擴充方法(所以要記得 using Nut),透過 ToText() 即可數字轉英文字:

using Nut;
using System;
 
namespace N2T
{
class Program
    {
staticvoid Main(string[] args)
        {
int i = 123456;
            Console.WriteLine(i.ToText("en"));
 
decimal n = 123456.78m;
            Console.WriteLine(n.ToText(Nut.Currency.USD, "en"));
            Console.Read();
       }
    }
}

測試結果如下:
one hundred twenty three thousand four hundred fifty six
one hundred twenty three thousand four hundred fifty six dollars seventy eight cents

轉換整數時 ToText() 可傳入語系參數轉成不同語言,程式支援英文、法文、俄文、西班牙文、土耳其文跟烏克蘭文。語系預設為英文,要轉英文不傳參數也可。含小數數字轉英文多用於表示金額,數字需用 decimal 型別,第一個參數傳入幣別決定幣值單元,例如美元是 dollar/cent、歐元是 euro/eurocent、俄羅斯是 ruble/kopek … 等,第二個參數可傳入語系。另外還有一些進階選項可設定第一個字母要不要大寫、零要不要顯示… 等,Github 原始碼附了一個 Nut.Demo 專案貼心提供選項展示及試玩。如果用得不開心,Fork 專案改成你要的樣子吧,充分體現 Open Source 精神~

愈來愈感受到 Github、NuGet 已讓程式開發產生變革,當常見需求幾乎都能快速找到現成且可修改的解決方案,排除這些瑣碎需求的牽絆,開發者更能專注核心商業邏輯,開發節奏就加快了。但負面效應也隨之而來-愈來愈短的開發時程要求、愈來愈難開口說「我做不出來」、後浪挾資訊流通之便來勢洶洶… 嗯,開發生涯好像也變幸福耶,哈!

【同場加映】阿拉伯數字轉中文大寫

部落格 1000 萬次點閱紀念暨抽獎活動

$
0
0

(灑花灑花再灑花)

2004 起開始寫部落格,2006/4/15 啟用 statcounter 計數器,歷經 4096 天,部落格點閱數正式突破 1000 萬次囉!

部落格一寫 13 年,熱血青年已成老鹹魚,學習能力與鑽研熱情早不復當年,與年輕小新肝們相比天差地別,唯一能佔便宜只剩多點經驗。算算這輩子是定型了,不是當將領的材料,頂多當個身經百戰的老士官長,沒本事帶兵攻城掠地,面對戰場種種倒是駕輕就熟,水坑沼澤獨木橋,三行四進挖戰壕,掃雷爆破拼刺刀,生火包紮吃野草,什麼挑戰都難不倒,是那種登場後能撐很久不領便當的角色,哈。也因為如此,近年來部落格少有高深的東西,多是瑣碎的知識經驗分享(其實很大一部分是為自己所寫,擔心下回遇上想不起來),謝謝大家對老士官長不離不棄一路相挺,分享之餘還常從讀者朋友們的回饋學到更多,在此致謝。

依照慣例,慶祝里程碑達成就,抽獎是一定要的。上回 Facebook 粉絲專頁破萬推出的爛木頭黑話紀念書籤佳評如潮,據聞黑市價格飆破兩百八十萬,有幸運得主為此換車買房,這次想不出梗再推出升級版紀念書籤,採不鏽鋼材質電蝕刻精印(電蝕刻做法可參考黃色小鴨除錯之原力升級版),保用百年金剛不壞,書都爛光了書籤還是好的!(謎:強調這點的用意是?)

前後試了幾種做法(其實是自己想玩),包含請廠商製作絹版模版、卡典西德雷射割字、感光藍油曝光顯影… 試了十來次,效果都不理想,猜想電解液用食鹽水太簡陋,追求效果恐得回歸腐蝕性或毒性較強的專業化學配方,但既然要彰顯土砲精神,就保持它質樸有缺陷的樣子吧!本次預計送出五枚書籤,片片不一致,個個有瑕疵,品質之荒唐令人瞠目結舌,但大家肯定能感受創作者想傳達的意念:每個人在世間都是獨一無二的,沒有任何人是完美的… 是充滿人文情懷的深刻作品~(謎:哇塞!好敢講,分明只是粗製濫造的破鐵片)

抽獎方式遵循古法(公平公正公開電腦抽獎法),想參加的捧油請在臉書抽獎貼文留言「抽」或「+1」報名~(以臉書留言為準,部落格文章留言恕不計入)報名截止時間(D日T時)以 JavaScript 取出名單,以「最新動態」排序,將名單送入程式輸入 D 日台股指數當亂數種子,取出 5 位得獎者。(照片中第一排左起為1、2,中間為3,最下排左起為4、5)

PS1:獎品很鳥,就不請律師公證了,大會保留任意修改及解釋抽獎規則的權利(靠,有沒有這麼蠻橫?),如果擔心規則不公權益受損,請勿參加,不要為了幾塊破鐵片受氣。
PS2:屆時將透過粉專訊息連絡中獎者,由於獎品將採郵寄方式,依慣例若中獎者住在海外、外星球或其他銀河系,恕只代寄到指定的台灣住址。

【成長歷程】

Visual Studio 開啟專案出現 SQL Server Express 未安裝警告

$
0
0

在公司開啟某些工作專案時,我的 Visaul Studio 常會彈出像這樣的警告訊息:

The Web project 'MyWeb' requires SQL Server Express LocalDB, whick is not installed on this computer.

To upgrade the project database to use latest SQL Server Express LocalDB, double-click the database file and follow the instructions. Note: After this upgrade, the project database can't be modified using earlier verions of Visual Studio.

大意是警告我這個網站專案需要 SQL Server Express,但我沒有安裝,要不就要升級新版,要不就安裝舊版… 然而,該網站所有資料都放在 SQL Server 或 Oracle,即使沒有 SQL Server Express 也可正常運作沒半點問題,就只是每次開專案會跳出來煩人警示。之前沒想太多點掉就算,但心中不免嘀咕,怎麼沒人嫌煩處理一下?(謎:巴望別人解決問題?說好的射手魂呢?)

最新接手的 sln裡面有三個專案都有這問題,每次開解決方案要點三次,加上初期編譯有誤需要反覆重開測試,於是惰性再強也壓不住竄升的煩躁感,驅使我尋找解決方法。

關鍵在 web.config 的這段連線字串,拿掉問題就解了!(前題:如果你沒有用到 ASP.NET 內建的使用者註冊及角色管理的話)

<connectionStrings>
  <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=aspnet-MYWEB-20120807153209;Integrated Security=SSPI" />

至於背後的故事,應該是 VS2010 或 VS2012 時代(依據問題專案的建立年代推敲)網站專案樣版預設加入的設定,供 ASP.NET Membership 機制使用(即使網站採 Windows 驗證)。VS2013/VS2015/VS2017 在開啟專案時一旦偵測到連線字串出現.\SQLEXPRESS 而主機沒裝,便會彈出警示。接著,我也想到為什麼大家不覺困擾,沒人處理-我重裝電腦後沒再安裝 VS2010/VS2012,同事們多半還有安裝較早 VS 版本,根本沒這問題,哈!

Windows 10 搜尋問題排除經驗二則

$
0
0

工作機由 Windows 8.1 升級至 Windows 10,過程挺順利,原本安裝的應用程式、環境設定幾乎都無痛移轉,午休時間升級完畢,下午打開 Visual Studio 就接著上工,算是一次良好體驗。(升級軟體有賺有賠,別人成功不保證你不會踩雷,升級前請詳閱公開說明書)

但搜尋上遇到一點小問題。

升級後手癢,想說新環境就該重新規劃一下磁碟分配,把 Windows 搜尋的索引位置從 C:\ProgramData\Microsoft 移到 X:\ProgramData\Microsoft,節省寶貴的 C: SSD 空間,又順手重建索引。

沒想索引重建後遇到狀況,Windows 搜索輸入關鍵字只能查到包含關鍵字的一般檔案,找不到已裝的應用程式(例如:Word、Excel…)。爬文得知,Windows 有個少為人知的資料夾,C:\ProgramData\Microsoft\Windows\Start Menu\ ,軟體安裝時會在其中建立資料夾跟程式捷徑,就是開始選單「程式集」項目的依據。而 Windows 搜尋可以找到已安裝的軟體,關鍵就在將 Start Menu 目錄納入索引範圍。但反覆調整索引項目、重建索引、停用再啟用 Windows 搜尋服務,甚至最後只留 Start Menu 索引,查不到就是查不到:

在某篇文章有人提到「重開機」… 是的,重開機後問題就消失了,白白浪費這麼多寶貴青春(如果我還有的話),筆記起來,下回處理索引問題要納入 SOP。

接著我遇到第二個問題,Windows 索引設定不認得 Outlook 2016 郵件,在選取位置上顯示為 mapi16://{S-1-5-21-32786… 格式的系統代碼:

爬文得知 mapi16 是 Outlook 自訂 Protocol,故問題應出在 Windows 不認得這個協定。網路上眾說紛云,重建索引、重裝 Outlook、重建郵件設定檔(Profile)、修復 Outlook、重裝 OS… 都有。我先選成本最低的修復 Office 2016 安裝,再「重開機」(這次有記住),重開機後在索引範圍看到的仍是 mapi16://{S-1-5-21-32786…,心灰意冷之餘開始亂試,取消勾選後再開啟設定 UI 發現 mapi16://{S-1-5-21-32786… 項目消失,又做了幾次,Microsoft Outlook 項目就莫名出現~

不知其所然,但還是留個記錄供遇到類似狀況的捧油參考。

最後補充一則發現,從 Outlook 2013 起移除直接使用 Windows Search 搜尋郵件的功能,Outlook 項目不會顯示在 Windows Shell 搜尋中 (例如,從 [開始] 功能表搜尋,或使用 Win+F),必須在 Outlook 中執行搜尋,覺得可惜但也只能接受。

關於 Shared Memory 的兩三事

$
0
0

參與古老系統的搬遷工程,其中使用 Shared Memory 實現跨 Process 溝通(例如:ASP.NET 呼叫 Window Service),也因而被迫了解這門對 .NET 開發者偏冷門的技術,特筆記備忘。

【Shared Memory 是什麼?】

跨 Process 溝通有個術語,Interprocess Communictaion(IPC),在 Windows 平台有以下選擇:參考

  • Clipboard
    程式 A 將内容貼進剪貼簿,程式 B 自剪貼簿取出内容。
  • COM
    OLE 複合文件(Compound Document)讓 Word 文件可以內嵌 Excel 工作表,點兩下還能叫出 Excel 進行編輯, OLE 的基礎為 COM 元件技術。
  • Data Copy
    程序 A 向程式 B 依約定的格式内容傳送 WM_COPYDATA 訊息
  • DDE
    DDE 是一種允許不同應用程式交換不同格式資料的通訊協定,可視為剪貼簿的沿伸,除了一次性抛轉,還能持續傳輸資料。(效能相對差,已不建議使用)
  • File Mapping
    File Mapping 意指將檔案模擬成 Process 中的一塊記憶體,當多個應用程式間透過共用 File Mapping 交換資料,稱之為 Named Shared Memory,在各種 IPC 方法中效能最佳,但必須透過 Mutex等同步機制防止讀寫衝突。
  • Mailslots
    單向溝通,Mailslot Client 送訊息給 Mailslot Server,訊息在 Server 讀取後删除,支援跨機器傳送,還可一對多廣播。(廣播訊息長度限制 400 bytes,一對一傳輸時訊息長度則由 Mailslot Server 建立時決定)
  • Pipes
    雙向傳輸,分為 Anonymous Pipe 及 Named Pipe。Anonymous Pipe 一般用於父程序與子程序間的標準輸入/輸出導向,雙向溝通要建兩條 Pipe,不能跨網路且限於有從屬關係的 Process;Named Pipe 則可用於任意 Process 間交換資料,並支援跨網路 Process 間傳輸。
  • RPC
    Remote Procedure Call(RPC) 允許應用程式呼叫其他應用程式提供的函式功能,並可跨網路呼叫。Windows RPC 符合 ISO DCE 標準,支援跨作業系統系統整合。
  • Windows Sockets
    基於 TCP/IP 或其他網路協定制訂的抽象通訊介面,底層透過網路連線進行資料交換。

Shared Memory 是 C/C++ 開發者常用的資料交換方式( Google 可以查到很多在 Linux 用 Shared Memory 實現 IPC 的範例),故 C/C++ 開發者在 Windows 平台也常選擇它做為溝通管道。

【Shared Memory 實作練習】

雖然用的人較少,但 .NET 內建 System.IO.MemoryMappedFiles 命名空間,要玩 Shared Memory 不是難事,幾乎跟操作檔案沒什麼兩樣,只要有 FileStream 相關操作經驗很快就上手,參考 MSDN 範例,我寫了小程式練習。

我寫了兩隻程式,ProcessA 透過 MemoryMappedFile.CreateNew() 建立大小為 1024 Bytes 的空間,與另一隻 ProcessB 練習傳接球。由於 1024 Bytes 兩隻程式共用,我將前 512 規劃為 ProcessA 寫入 ProcessB 讀取,後 512 則是 ProcessB 寫 ProcessA 讀,程式中使用 CreateViewStream 傳入起始位址及長度指向自己專屬的區域。為了避免 ProcessA 及 ProcessB 存取 MemoryMappedFile 時出現讀寫衝突,我使用 Mutex 鎖定控管單一時間只有一個 Process 可以存取 MemoryMappedFile。測試過程為 ProcessA 建立 MemoryMappedFile,寫入訊息字串 –> ProcessB 讀取訊息字串並寫入回應字串 –> ProcessA 讀取回應字串,結束。

ProcessA 程式如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace ProcessA
{
class Program
    {
staticvoid Main(string[] args)
        {
//REF: https://msdn.microsoft.com/en-us/library/dd267552(v=vs.110).aspx
using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("DARKTHREAD", 1024))
            {
bool mutexCreated;
                Mutex mutex = new Mutex(true, "DarkthreadSharedMem", out mutexCreated);
using (var stream = mmf.CreateViewStream()) {
byte[] msg = Encoding.UTF8.GetBytes("Hello, World!");
using (BinaryWriter bw = new BinaryWriter(stream))
                    {
                        bw.Write(msg.Length); //先寫Length
                        bw.Write(msg); //再寫byte[]
                    }
                }
                mutex.ReleaseMutex();
                Console.Write("操作 Process B 進行讀取及回應,完成後按Enter");
                Console.ReadLine();
 
                mutex.WaitOne();
using (MemoryMappedViewStream stream = mmf.CreateViewStream(512, 512))
                {
using (var br = new BinaryReader(stream))
                    {
//先讀取長度,再讀取内容
                        var len = br.ReadInt32();
                        var msg = Encoding.UTF8.GetString(br.ReadBytes(len), 0, len);
                        Console.WriteLine($"回應={msg}");
                    }
                }
                mutex.ReleaseMutex();
                Console.ReadLine();
            }
        }
    }
}

ProcessB 程式如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace ProcessB
{
class Program
    {
staticvoid Main(string[] args)
        {
try
            {
                Console.Write("按 Enter 開始讀取及回應…");
                Console.ReadLine();
using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("DARKTHREAD"))
                {
                    Mutex mutex = Mutex.OpenExisting("DarkthreadSharedMem");
                    mutex.WaitOne();                    
using (MemoryMappedViewStream stream = mmf.CreateViewStream(0, 0))
                    {
using (var br = new BinaryReader(stream))
                        {
//先讀取長度,再讀取内容
                            var len = br.ReadInt32();
                            var word = Encoding.UTF8.GetString(br.ReadBytes(len), 0, len);
                            Console.WriteLine($"訊息={word}");
                        }
                    }
using (MemoryMappedViewStream stream = mmf.CreateViewStream(512, 512))
                    {
using (var bw = new BinaryWriter(stream))
                        {
                            var msg = Encoding.UTF8.GetBytes("朕知道了");
                            bw.Write(msg.Length);
                            bw.Write(msg);
                        }
                    }
                    mutex.ReleaseMutex();
                }
                Console.ReadLine();
            }
catch (FileNotFoundException)
            {
                Console.WriteLine("Memory-mapped file does not exist.");
            }
        }
    }
}

測試成功!

【補充技巧】

  1. 如何檢視 Windows 目前已開啟的 MemoryMappedFile?
    SystemInternals 有個 AccessChk 工具能列出 Windows 所有可存取的檔案、資料夾、Registry、物件以及 Windows 服務。而 MemoryMappedFile 屬於一種 Windows 物件,使用以下指令可列出所有物件並存檔
    accesschk -osv > e:\objList.txt
    在其中尋找 MemoryMappedFile 名稱,若存在可看到類似以下記錄:
    \Sessions\1\BaseNamedObjects\DARKTHREAD
      Type: Section
      Medium Mandatory Level (Default) [No-Write-Up]
      RW NT AUTHORITY\SYSTEM
        SECTION_ALL_ACCESS
      RW DOMAIN\UserName
        SECTION_ALL_ACCESS
      RW DOMAIN\UserName-S-1-5-5-0-954410
        SECTION_ALL_ACCESS
  2. MemoryMappedFile 預設是開在使用者的 Session 中,預設無法跨 Session 使用。例如:兩個分屬不同 AppPool 的 ASP.NET 若執行身分不同,即使 MemoryMappedFile 名稱相同也是各自一份,故運用時需確認溝通雙方使用的執行身分相同。
  3. 若要跨不同執行身分溝通,MemoryMappedFile 可命名為 "Global\Filename"(注意 Global 大小寫有別,我踩到誤寫為GLOBAL 路徑無效的雷),如此可跨執行身分存取。
    但需要注意,Session 0 (Windows Service)以外的 Process 需要具有 SeCreateGlobalPrivilege 權限才能建立 Global\… MemoryMappedFile。(MSDN文件
    關於 Session 0,可參考對岸 MVP 的這篇文章 - 穿透Session 0 隔离(一)裡面有蠻詳細的介紹。

【茶包射手筆記】IIS 設定順序導致 500.19 錯誤

$
0
0

HTTP 500.19 錯誤多因 ASP.NET 父網站與子網站因繼承關係導致設定項目重複,過去曾經歷幾次(IIS 7限制IP存取的設定錯誤怪異的web.config HttpHandler重複錯誤),今天再遇到 IP 限制設定重複導致子網站掛點的狀況,同事發誓一切操作合情合理,想想上回 IIS 7 IP 限制設定打架案例沒有逆天亂搞照樣出錯,推測其中有雷,決定現場模擬還原真相。

我在 IIS 設定一獨立站台 Test,其下加入 Child 子網站。父網站的 index.html 以 IFrame 內嵌 Child/index.html 進行測試。

透過 IIS 管理介面設定 Test 站台拒絕未指定的 IP 用戶端。


接著在 Test 站台設定允許 IP ::1(IPv6 的 Localhost)

檢視 Child 子網站的 IP 位址及網域限制,可發現已自動加上 ::1,這是繼承自 Test 站台的設定。

接著在 Child 另外加入一筆 127.0.0.1,此時瀏覽 localhost:7611 仍正常。

如果我們在 Test 站台也加入 127.0.0.1,就會變成以下德行,重現今天遇到的狀況:

Child/index.html 詳細錯誤如下:

HTTP 錯誤 500.19 - Internal Server Error
無法存取要求的網頁,因為與該網頁相關的設定資料不正確。

詳細錯誤資訊:
模組       IpRestrictionModule
通知       BeginRequest
處理常式       ExtensionlessUrlHandler-Integrated-4.0
錯誤碼       0x800700b7
設定錯誤       在複合金鑰屬性 'ipAddress, subnetMask, domainName' 分別設為 '127.0.0.1, 255.255.255.255, ' 的情況下,無法新增類型 'add' 的重複集合項目
設定檔案       \\?\C:\inetpub\temp\apppools\Test\Test.config
要求的 URL      
http://localhost:7611/Child/
實體路徑       X:\WWW\Child\
登入方法       尚未判定
登入使用者       尚未判定

設定來源:
  835:         <ipSecurity>
  836:           <add ipAddress="127.0.0.1" allowed="true" />
  837:         </ipSecurity>

詳細資訊:
如果讀取網頁伺服器或 Web 應用程式的設定檔案發生問題,此時就會發生這種錯誤。在某些情況下,事件記錄可能會包含何種原因造成這項錯誤的詳細資訊。
檢視詳細資訊 »

由此可知:

先在子網站加入限制 IP,再到父網站加入相同限制 IP,就會因繼承關係造成子網站設定重複,觸發 500.19 錯誤!

發生問題後,子網站的「IP 位址及網域限制」已無法開啟,除了直接修改 IIS config 檔,必須先移除父網站的重複 IP 設定,才能使用 IIS 管理介面重設子網站的 IP 限制。

結論是 IIS 缺少防呆,無法防範或忽略繼承設定與子網站設定重複的狀況,只能操作時多加留意。

【茶包射手筆記】SQL 錯誤-Server 'XXX' is not configured for RPC

$
0
0

在測試台運作正常,程式部署到正式環境後出現 SQL 錯誤:

System.Data.SqlClient.SqlException (0x80131904): Server 'XXX' is not configured for RPC.
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at System.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
at System.Data.SqlClient.SqlDataReader.get_MetaData()
at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)

追查程式,錯誤點在以下這段呼叫 Linked Server 端 Stored Procedure 的 SQL 指令: (XXX 為 Linked Server 名稱)

EXECUTE ('CALL PKG_BLAH.SP_BLOO(?,?,?)', @P1, @P2, @Result OUTPUT) AT [XXX];

爬文查到此與 Linked Server 的 RPC Out 選項被設成 False有關。比對正式與測試 SQL Server,的確正式台 RPC Out 設 False,測試台設 True,將測試台設定改成 False 則可重現錯誤。

鐵證如山,調整正式台 RPC Out 設定為 True 後,問題排除。

最後補充,冷門選項 RPC 與 RPC Out 是什麼鬼?

依據 MSDN Blog What is the RPC and RPC Out option on a SQL Server linked-server- – Jason's Technical Topics

RPC 用於古老的 Remote Server 功能(Linked Server 的前身),SQL 2005 之後幾已絕跡,可直接放生。

至於 RPC Out,則與 Linked Server 有關,「呼叫 Linked Server 上的遠端 Stored Proceure」可視同 RPC (Remote Procedure Call),有兩種寫法:

  • EXEC [myserver].master.dbo.sp_helpdb
  • EXEC (‘master.dbo.sp_helpdb’) AT myserver

SQL 預設封鎖這類遠端 Stored Procedure 呼叫,必須將 RPC Out 設為 True 才放行,否則就如本案例,將產生 Msg 7411, Level 16, State 1, Line 1  Server ‘myserver’ is not configured for RPC. 錯誤。

猜想是基於安全考量,SQL 預設封閉未用到的管道以降低風險,故若系統包含呼叫 Linked Server 端 Stored Procedure 的情境,記得要解除封印。


【茶包射手筆記】詭異的 Word 字元疊疊樂

$
0
0

同事遇到一個 Word 鬼問題,某份使用者提供的 Word 文件,有段文字難以修改,輸入的文字會消失或重疊在一起!

做了一個範例重現問題。如下圖所示,文件原本有個字母 T,將游標移到 T 的後方,試著輸入 A、B、C、D、E 字元,看到的不是TABCDE,而是全部字元都疊在一起!

經過一番摸索,發現問題出在這段內容的「字型/字元間距/間距」設定被設成「緊縮」(預設為標準),並指定點數為 7.5 點,將其改回標準即一切正常。

順手測了緊縮不同點數的效果:

基本上 2 點已是極限,超過則文字重疊難以閱讀,還會導致後方即使輸入正常間距文字也出現字元被吃掉的詭異現象,已經不能算是合理使用。而究竟為什麼拿到的 Word 文件上會出現詭異整人陷阱,原因成謎。

總之,下回遇到 Word 輸入字元重疊或被吃掉的狀況,SOP 應納入字元間距檢查。

CODE-使用 C# 批次列印 PDF 檔案

$
0
0

專案遇到批次列印 PDF 檔需求。

Acrobat Reader 或 Foxit Reader 等常用 PDF 軟體本身就具備傳參數直接列印功能,例如 Acrobat Reader 直接列印 PDF 之語法為:AcroRd32.exe /p /h "pdf路徑" "印表機名稱"(印表機名稱省略時由預設印表機輸出)

基於以上資訊,最直覺的做法是找出 Acrobat Reader EXE 檔(AcroRd32.exe)路徑,在 .NET 程式透過 Process.Start() 傳入 PDF 路徑及 /p /h 參數呼叫 Acrobat Reader 列印檔案。但這個做法有個小缺點,它限制使用者必須安裝特定 PDF 閱讀軟體,再不然程式就得夠彈性,支援各種可列印 PDF 的軟體,如此尋找及識別 PDF 軟體邏輯將複雜化。

在 Stackoverflow 看到一個好方法,由於 Windows 多半會預設 PDF 開啟程式,並且還會註冊開啟、列印等動作,方便使用者透過檔案總管右鍵選單直接列印:

探索其背後原理,是 Acrobat Reader 先在 .pdf 副檔名註冊 UserChoice/ProgId = AcroExch.Document.11

而 AcroExch.Document.11 註冊了 Print/Command 對應到先前說過的列印指令: AcroRd32.exe /p /h "%1":

透過以上 Registry,當我們對 PDF 檔下達 Print Verb 時,Windows 便會找到對應程式並執行列印,不管它是 Acrobat Reader 還是 Foxit Reader,遠比指定並尋找特定軟體的做法更具彈性。以下為 Stackoverflow 找到的範例程式:

privatevoid SendToPrinter()
{
   ProcessStartInfo info = new ProcessStartInfo();
   info.Verb = "print";
   info.FileName = @"c:\output.pdf";
   info.CreateNoWindow = true;
   info.WindowStyle = ProcessWindowStyle.Hidden;
 
   Process p = new Process();
   p.StartInfo = info;
   p.Start();
 
   p.WaitForInputIdle();
   System.Threading.Thread.Sleep(3000);
if (false == p.CloseMainWindow())
      p.Kill();
}

仿照上述方法寫好第一版,丟給使用者測試後馬上被打槍-程式在列印多頁報表時會掉頁,例如 6 頁只印完 4 頁就沒了。

推敲其原因,由於 AcroRd32 非標準的命令列程式,無法等待程式執行結束,啟動程式後控制權即回到呼叫端,故範例程式的做法是等待三秒,假設文件已列印完畢即強制關閉 PDF 程式,造成 AcroRd32 6 頁只列了 4 頁就被關掉的狀況。(飄向北方才唱到咀嚼爆肚涮羊就被卡歌來著)

把 3 秒等待時間加長是種鋸箭做法,但魔術數字註定要糾結於「空等 vs 不足」的兩難。最後,我想出一個好方法-監測列印佇列(PrintQueue)。呼叫 AcroRd32 後先等待列印文件出現在 PrintQueue,再等待其列印完畢從佇列消失,最長等待時間則拉長到 180 秒,確保每個 PDF 都印好印滿,如此既沒有無謂等待,也沒有過早中止程式掉頁風險,新做法美妙到我想為自己起立鼓掌 XD(捻鬚而笑)

完整程式範例如下供大家參考:

 
 
//REF:https://stackoverflow.com/a/6106155/288936
publicstaticvoid Print(string filePath)
{
    Status = PrintJobStatus.Printing;
    Message = string.Empty;
try
    {
        logger.Debug($"Printing... {filePath}");
        ProcessStartInfo info = new ProcessStartInfo();
        info.Verb = "print";
        info.FileName = filePath;
        info.CreateNoWindow = true;
        info.WindowStyle = ProcessWindowStyle.Hidden;
 
        Process p = new Process();
        p.StartInfo = info;
        p.Start();
 
        p.WaitForInputIdle();
//以下邏輯克服無法得知Acrobat Reader或Foxit Reader是否列印完成的問題
//最多等待180秒(假設所有檔案可在3分鐘內印完)
        var timeOut = DateTime.Now.AddSeconds(180);
bool printing = false; //是否開始列印
bool done = false; //是否列印完成
//取純檔名部分,跟PrintQueue進行比對
string pureFileName = Path.GetFileName(filePath);
//限定最大等待時間
while (DateTime.Now.CompareTo(timeOut) < 0)
        {
if (!printing)
            {
//未開始列印前發現檔名相同的列印工作
if (CheckPrintQueue(pureFileName))
                {
                    printing = true;
                    Console.WriteLine($"[{pureFileName}]列印中...");
                }
            }
else
            {
//已開始列印後,同檔名列印工作消失表示列印完成
if (!CheckPrintQueue(pureFileName))
                {
                    done = true;
                    Console.WriteLine($"[{pureFileName}]列印完成");
break;
                }
            }
            System.Threading.Thread.Sleep(100);
        }
try
        {
//若程序尚未關閉,強制關閉之
if (false == p.CloseMainWindow())
                p.Kill();
        }
catch
        {
        }
if (!done)
        {
            Console.WriteLine($"無法確認報表[{pureFileName}]列印狀態!");
        }
    }
catch (Exception ex)
    {
        Console.WriteLine($"Error: {DateTime.Now:HH:mm:ss} {ex.Message}");
    }
}
 
//需查詢 WMI 記得加入參照及 using System.Management; 
privatestaticbool CheckPrintQueue(string file)
{
//尋找PrintQueue有沒有檔案相同的列印工作
string searchQuery =
"SELECT * FROM Win32_PrintJob";
    var printJobs =
new ManagementObjectSearcher(searchQuery).Get();
return printJobs.Any(o => (string)o.Properties["Document"].Value == file);
}

SQLite 批次 INSERT 的蝸牛陷阱

$
0
0

假日轉檯寫 Coding4Fun 專案,本週的 Scrum Sprint Planning Meeting 我認領的工作是將 13 萬英文單字轉入 SQLite 資料庫(謎:認領?快醒醒,這專案從頭到尾只有你一個人吧?)。

心想這有什麼難,涮涮涮寫好以下程式,沒想到其執行速度之慢,嚇得我屁滾尿流失了魂…

using (var cnSqlite = new SQLiteConnection(csSqlite))
{
    cnSqlite.Open();
    Stopwatch sw = new Stopwatch();
    sw.Start();
    var totalCount = list.Count;
    var index = 0;
foreach (var voc in list)
    {
        Console.WriteLine(
    $"{index++}/{totalCount}({index * 100.0 / totalCount:n1}%) {voc.Word}");
        cnSqlite.Execute(
"INSERT INTO Dictionary VALUES(@Word, @KKSymbol, @Explanation)", (object)voc);
    }
    sw.Stop();
    Console.Write($"Duration={sw.ElapsedMilliseconds:n0}ms");
}

事實上我沒耐心等到 sw.Stop() 觀察總耗時,花了 30 分鐘只 INSERT 完 10% 我就放棄了。換句話說,全部跑完要 5 個小時啊啊啊啊啊~ 這個年代這種速度?我有正在操作古董火砲對抗航母戰鬥群的無力感…

爬文找到文章(Make your SQLite bulk inserts very fast in C# .NET),才知這是 SQLite 的 FAQ

SQLite 一秒最快能完成 50,000 筆以上的 INSERT,但一秒只能完成幾十筆 Transation,依原本寫法,由於每筆 INSERT 動作預設自成一個 Transaction,速度要快也難。

解決方法很簡單,只需加個兩行,將整個迴圈包成一個 Transaction 就搞定。猜看看速度改進多少?

using (var cnSqlite = new SQLiteConnection(csSqlite))
{
    cnSqlite.Open();
    Stopwatch sw = new Stopwatch();
    sw.Start();
using (SQLiteTransaction tran = cnSqlite.BeginTransaction())
    {
        var totalCount = list.Count;
        var index = 0;
foreach (var voc in list)
        {
            Console.WriteLine(
                $"{index++}/{totalCount}({index * 100.0 / totalCount:n1}%) {voc.Word}");
            cnSqlite.Execute(
"INSERT INTO Dictionary VALUES(@Word, @KKSymbol, @Explanation)", (object)voc);
        }
        tran.Commit();
    }
    sw.Stop();
    Console.Write($"Duration={sw.ElapsedMilliseconds:n0}ms");
}

實測結果,132,319 筆 14.887 秒塞完,平均 8,888 筆/秒(這數字巧合也太神奇惹)!跟原本 7 筆/秒相比,速度提升 1200 倍!已筆記。

COALESCE 發生字元設定不符合錯誤

$
0
0

同事報案,Dapper 查詢 ORACLE 時使用 COALESCE()遇到 ORA-12704: character set mismatch(字元設定不符) 錯誤。

我用以下程式成功重現問題:(jefftest2.t 欄位為 NVARCHAR2)

staticvoid Main(string[] args)
        {
using (var cn = new OracleConnection(csStr))
            {
                var list = cn.Query(
"select 1 from jefftest2 where coalesce(t, :text) like '%'",
new { text = "ABC" });
                Console.Write(list.Count());
                Console.Read();
            }
        }

執行結果如下:

有趣的是,將 COALESCE(t, :text) 改成 NVL(t, :text) 就不會出錯。爬文查到應是 COALESCE(NVARCHAR2, VARCHAR2) 前後字串型別不一致,有人想出 N'' || :text 鋸箭小密技。試了一下,還真的有效!

問題來了,為什麼 :text 會變成 VARCHAR2?想起以前遇過類似問題-Dapper+ODP.NET無法寫入Unicode問題,莫非是相同原因?改用 ODP.NET OracleCommand 測試,指定 OracleDbType.NVarchar2,執行正常:

由此推測,八九不離十又是 Dapper 踩中 ODP.NET Bug 的老問題,搬出上回的 Hacking 修補大法

staticvoid FixOdpNetDbTypeStringMapping()
{
    Assembly asm = typeof(OracleConnection).Assembly;
    Type tOraDb_DbTypeTable = asm.GetType("Oracle.ManagedDataAccess.Client.OraDb_DbTypeTable");
    var fldDbTypeMapping = tOraDb_DbTypeTable.GetField("dbTypeToOracleDbTypeMapping",
        BindingFlags.Static | BindingFlags.NonPublic);
int[] mappings = (int[])fldDbTypeMapping.GetValue(null);
    mappings[(int)System.Data.DbType.String] = (int)OracleDbType.NVarchar2;
    fldDbTypeMapping.SetValue(null, mappings);
}
 
staticvoid Main(string[] args)
{
    FixOdpNetDbTypeStringMapping();
using (var cn = new OracleConnection(csStr))
    {
        var list = cn.Query(
"select 1 from jefftest2 where coalesce(t, :text) like '%'",
new { text = "ABC" });
        Console.Write(list.Count());
        Console.Read();
    }
}

問題排除~

依上次研究中,原以為要集滿「Oracle資料庫未採AL32UTF8編碼 + OracleParameter參數型別指定DbType + 內容剛好有ANSI/BIG5難字」三項條件才會踩坑,但依這回經驗,參數用於 COALESCE() 不一定要有難字也會出錯,看起來,未來應該將這個 Hacking 修補納入 Dapper + ODP.NET 開發的 SOP 比較保險。

KB-當 WHERE AND/OR 條件遇上 NULL

$
0
0

在 SQL 世界裡 NULL 性質特殊,行為獨特,過去就曾討論過:

自以為至此對 NULL 認識已足,不料前兩天在 WHERE LIKE AND/OR 情境中遇上 NULL,一時意志動搖陷入迷惘,想必是認知還不夠深刻,再補篇 KB 吧!

先別急著看答案,大家猜猜以下 SQL 指令會得到什麼結果?

select'T1','MATCH'from dual wherenulllike'78'
union
select'T2','MATCH'from dual wherenot(nulllike'78')
union
select'T3','MATCH'from dual wherenot(nulllike'78') and 1 = 1
union
select'T4','MATCH'from dual wherenulllike'78'or 1 = 1

答案只有 T4 符合!

理由是 NULL 不管 LIKE 任何字串,結果不是 True 也不是 False,因此 T1 不成立;加上 NOT,一樣不是 True 也不是 False,故 T2 也不成立。這個無法論斷 True 或 False 的狀態與 1=1(True) 做 AND 運算結果不會成立,與 1=1 (True) 進行 OR 比較則會成立。

爬文找到專業解說,如下表所示,NULL LIKE 所產生既非 True 也不是 False 的狀態術語為 Unknown,Unknown AND True 為 Unknown(T3 的例子),Unknown AND False 為 False;Unknown OR True 為 True(T4 的例子),Unkown OR False 則為 Unknown。

未來遇 AND/OR 配 NULL 情境如信心動搖,速查此表堅定信念。

【茶包射手日記】ORACLE JOIN GROUP BY 子查詢爆慢疑案

$
0
0

接獲同事報案,某段 Oracle 查詢偶發嚴重效能問題。查詢時資料表經由 JOIN 自身的 GROUP BY 子查詢挑出某日期前客戶最新的一筆資料:

SELECT
    ccb.Key1,
    ccb.Key2,
    ccb.Key3,
    ccb.Key4,
    ccb.Key5,
    ccb.Col1,
    ccb.Col2,
    ccb.Col3,
    ccb.Col4,
    --...略...
    ccb.Col20
FROM MyTable ccb
INNERJOIN ( 
SELECT Key0,Key1,Key2, Key3, MAX(Key4) as Key4,Key5
FROM MyTable
WHERE Key4 <= :TDay
GROUPBY Key0,Key2,Key1, Key3, Key5
)  maxb 
ON   ccb.Key0 = maxb.Key0
AND  ccb.Key1 = maxb.Key1
AND  ccb.Key2 = maxb.Key2
AND  ccb.Key3 = maxb.Key3
AND  ccb.Key4 = maxb.Key4
AND  ccb.Key5 = maxb.Key5

MyTable 約 90 萬筆,GROUP BY 子查詢結果約 8000 筆,JOIN 後筆數與子查詢筆數相同。

Key0 到 Key5 有設 Index,先查出 8000 筆,再透過 Index 從 90 萬筆中找出 8000 筆感覺效能不致於離譜,測試正常的執行時間不超過 12 秒,但卻不時會發生數分鐘跑不完導致程式逾時的狀況。

一早接獲報案,優先想到的偵辦方向是檢視執行計劃試著找出瓶頸,立即實測,此時執行時間約十來秒正常,而執行計劃如下:

我對 Oracle 執行計劃沒啥研究,但先 GROUP BY 再 JOIN 的順序很符合我對「JOIN 一個 GROUP BY 子查詢」的理解。

不到一多小時後再測一次,問題出現了!執行時間超過數分鐘,檢視執行計劃,Oracle 給了一個匪夷所思的結果:

先 JOIN 再 GROUP BY 再 GROUP BY 是什麼鬼啦?且執行成本爆增三倍,應是查詢爆慢的原因。

進一步調查,這一小時間資料有些變化,但筆數差異不大,應不致產生巨大差異。但同事提到一點,在兩次測試間曾執行過 Analyze Estimate Statistics 動作試著更新統計改善 Index 效能。在模擬環境測試,竟意外重現 Analyze 之後執行計劃崩壞查詢爆慢的現象,真是出乎意料的劇情發展!

官方文件則提到:Do not use the COMPUTE and ESTIMATE clauses of ANALYZE to collect optimizer statistics. These clauses are supported for backward compatibility. Instead, use the DBMS_STATS package, which lets you collect statistics in parallel, collect global statistics for partitioned objects, and fine tune your statistics collection in other ways. The cost-based optimizer, which depends upon statistics, will eventually use only statistics that have been collected by DBMS_STATS. 意思是 Analyze 是舊指令,不宜再用,而且它所蒐集資料無助於 Cost-Based Optimizer (只對 VALIDATE or LISTCHAINEDROWS clauses 或 Freelist Blocks 資訊蒐集有效),要更新統計資料改善查詢效能,應一律改用 DBMS_STATS。不過,無效是一回事,我沒有找到任何執行 Analyze 會傷害效能的說法可以佐證,Oracle 為什麼會出現難以理解的執行計劃及執行速度,對我來說是謎。

無法解釋執行計劃崩壞的原因,但我的想法是只要阻止 Oracle 將 JOIN 拉進子查詢瞎攪和,我就不會踩到雷。用 Temp Table 是一招,而我懷念起 SQL 的 CTE (Common Table Expression),爬文後有意外發現,原來 Oracle  9i 起就已支援 WITH … AS 這種寫法(學名叫 Subquery Factoring),早期沒有遞迴功能,但到 11g 時已加上向 SQL 看齊了。

試著將查詢修改如下:

WITH maxb AS (
SELECT Key0,Key1,Key2, Key3, MAX(Key4) as Key4,Key5
FROM MyTable
WHERE Key4 <= :TDay
GROUPBY Key0,Key1,Key2,Key3,Key5
)
SELECT
    ccb.Key1,
    ccb.Key2,
    ccb.Key3,
    ccb.Key4,
    ccb.Key5,
    ccb.Col1,
    ccb.Col2,
    ccb.Col3,
    ccb.Col4,
    --...略...
    ccb.Col20
FROM MyTable ccb INNERJOIN maxb 
ON   ccb.Key0 = maxb.Key0
AND  ccb.Key1 = maxb.Key1
AND  ccb.Key2 = maxb.Key2
AND  ccb.Key3 = maxb.Key3
AND  ccb.Key4 = maxb.Key4
AND  ccb.Key5 = maxb.Key5

經實測,改用這種 CTE 寫法,就能避免 Oracle 惡搞執行計劃~

本次辦案心得:

  • Oracle 從 9i 起就可以寫 CTE(11g 起支援遞迴)是一大發現,有不少複雜查詢可以因此簡化
  • Analyze 指令已是歷史,勿再使用 ,請改用 DBMS_STATS
  • 在某些情境下,Oracle 可能讓 JOIN ( GROUP BY 子查詢)先 JOIN 再 GROUP BY,導致可怕的龜速… (我的老天鵝)

在單元測試專案使用 dynamic 出現 CSharpArgumentInfo.Create 錯誤

$
0
0

在自動測試專案加入使用 dynamic 型別的測試方法後,Visual Studio 2017 傳回編譯錯誤:

Missing compiler required member 'Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create'

在 Microsoft Connect 查到相似錯誤回報,案例集中在微軟單位測試及 NUnit 測試專案(NUnit 可透過更新版本解決),推測為單元測試專案預設未參照 Microsoft.CSharp.dll,而它是使用 dynamic 的必要參照。

問題在為單元測試專案手動加入參照後排除(如下圖),特此筆記。


【茶包射手日記】Visual Studio 編譯自動帶入相依 DLL 問題

$
0
0

同事報案,在 Visual Studio 從私有 NuGet 伺服器安裝我寫的共用元件,該元件參照了 Managed ODP.NET 但沒在 NuGet Package 宣告相依性,理論上不會一併安裝 Managed ODP.NET NuGet Package,但同事發現建置後 bin 目錄卻神奇地出現 Oracle.ManagedDataAccess.dll。試著在我的電腦演練相同操作,bin 目錄並不會出現 Oracle.ManagedDataAccess.dll!很明顯這又是我不了解的「魔法」,啟動調查,試著找出 Visual Studio 自動帶出相關 DLL 的原理。

起初我懐疑有某個聰明的外掛套件從中幫忙,自動補齊該共用元件需要的第三方程式庫,Visual Studio 改用安全模式也是同樣結果,不在場證明 GET,無保請回。

接著我懐疑跟註冊 GAC 有關,推測同事的 Managed ODP.NET 有註冊 GAC,Visual Studio 能成功找到 DLL 寫入 bin,我的沒註冊 GAC 不知去哪裡找 DLL,補不了檔案。

不料,檢查 GAC C:\Windows\assembly\GAC_MSIL\Oracle.ManagedDataAccess 資料夾的結果出乎意料,我的機器有同事沒有,意味我有註冊 GAC 而同事沒有,跟前面的推論完全相反啊啊啊~(啪!我聽到清脆的打臉聲,Orz)

別怕,拎杯機靈勝過尚書大人,立刻找到合理新解釋:因為我的 Managed ODP.NET 已註冊 GAC,所以不需要複製到 bin!(咳,「事前信心十足大膽預測,事後又能娓娓道來為何失準」是「偽專家」的必要技能,我真不愧是見過大風大浪的老屁股呀)

但這裡有個問題:如果同事沒有註冊 Managed ODP.NET,Visual Studio 是怎麼找到 Oracle.ManagedDataAccess.dll 放進 bin 裡?

這類疑難雜症,交給茶包一哥 Process Monitor就對了!

開啟 Process Monitor 監控專案建置過程的 Registry 與檔案存取,我學到一件事:建置作業由 MSBuild.exe 負責,它會先搜尋共用元件所在目錄看 Oracle.ManagedDataAccess.dll 有沒有跟它放在一起(在本例為 NuGet Packages 目錄,但我沒有包進去),若沒找到 MSBuild 會接著尋找 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v4.0.30319\AssemblyFoldersEx\ Regitry,同事在該機碼下有個 Oracle.ManagedDataAccess 指向 Oracle.ManagedDataAccess.dll,MSBuild 就靠著它找到 DLL 並複製到 bin 目錄下。

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v4.0.30319\AssemblyFoldersEx\Oracle.ManagedDataAccess]
@="E:\\Oracle\\odp.net\\managed\\common\\Oracle.ManagedDataAccess.dll"

(我的電腦在 AssemblyFoldersEx 也有機碼指向 Managed ODP.NET,但名稱不同,叫 odp.net.managed,如下圖)

補充:AssemblyFoldersEx 是 Visual Studio/MSBuild 尋找第三方元件的依據,參考:如何在32/64bit環境讓Visual Studio加入參考時可在.NET頁籤瀏覽自己的元件

AssemblyFoldersEx 機碼可解釋為何同事沒註冊 GAC 也可以找到 Managed ODP.NET DLL,下一步是驗證 GAC 是否與 DLL 會不會複製到 bin 有關?用 gacutil /u Oracle.ManagedDataAccess.dll 將其從 GAC 移除,再測試我的電腦 bin 還是沒出現 Oracle.ManagedDataAccess.dll,代表「有註冊 GAC 所以不用複製到 bin」的推測又錯了…(啪!啪!)

最後,老老實實比對同事跟我的 Process Monitor 記錄,同事的狀況是從 AssemblyFoldersEx 查到路徑找到檔案就複製到 bin 下(如下圖),我則是 AssemblyFoldersEx 找到路徑尋獲 DLL 檔,又繼續去 GAC \Windows\assembly\GAC_MSIL\Oracle.ManagedDataAccess 尋找 DLL 檔,重點是都有找到檔案,但就是沒將檔案複製到 bin。

這讓我有個想法:莫非問題出在版本不符?檢查共用元件參照的 Managed ODP.NET 版本為 4.121.2.0,同事 AssemblyFoldersEx\Oracle.ManagedDataAccess Registry 指向的版本也是 4.121.2.0,
而我的 AssemblyFoldersEx 與 GAC 指向的版本則是 4.121.1.0,並不是共用元件要求的版本。

BINGO!

將 AssemblyFoldersEx 指向位置的版本換成 4.121.2.0,我的電腦編譯後 bin 就出現 Oracle.ManagedDataAccess.dll 了,全案宣告偵破!

歸納本案調查心得:

  1. Visual Studio 建置背後靠 MSBuild 完成
  2. MSBuild 與 Visual Studio 會藉由 SOFTWARE\Microsoft\.NETFramework\v4.0.30319\AssemblyFoldersEx\ Registry 尋找第三方程式庫
  3. 若發現某 DLL 參照其他程式庫,MSBuild 會試著依 DLL 所在目錄、AssemblyFoldersEx、GAC 順序找尋所參照 DLL 並複製到 bin 目錄下
  4. 找尋 DLL 時版本必須完全一致,否則視同沒找到,既不複製也不會有錯誤訊息

【後話】

MSBuild 解析參照組件過程感覺有很多學問,想找篇深入探討文件解惑。在 MSDN 部落格上看到一篇序文如獲至寶,作者提到計劃寫完一系列共六篇文章深入剖析 MSBuild 及 Visual Studio 如何解析尋找 Assembly,讓我滿心期待一探究竟,很遺憾,作者 2010 年 5 月發願後就沒了下文,徒留幾篇敲碗留言… Orz

筆記:C# 6.0 自動實作屬性初始化與運算式主體定義

$
0
0

專案裡有個在父類別宣告 virtual List<string> MyProp { get; } = new List<string>(); ,接著在子類別 override MyProp, Visual Studio 2017 自動帶出 List<string> MyProp => base.MyProp; 。(術語為 Expression Body Definition 運算式主體定義)

子類別要傳回 "Prod1","Prod2" ,我差一點就接改成 List<string> MyProp  => "Prod1,Prod2".Split(',').ToList();,仔細想想不對,我的本意是要給 MyProp 屬性初始值,沿用 => 寫法變成每次產生新字串陣列,意義不同,差點搞錯。為此趕緊寫篇筆記壓壓驚!

來個範例:

using System;
 
publicclass Blah
{
    private Guid _prop1 = Guid.NewGuid();
public Guid Prop1
    {
get
        {
return _prop1;
        }
    }
public Guid Prop2 { get; } = Guid.NewGuid();
 
public Guid Prop3
    {
get
        {
return Guid.NewGuid();
        }
    }
public Guid Prop4 => Guid.NewGuid();
 
}
 
static void Main(string[] args)
{
    var blah = new Blah();
    Console.WriteLine($"Prop1={blah.Prop1},Prop2={blah.Prop2}");
    Console.WriteLine($"Prop1={blah.Prop1},Prop2={blah.Prop2}");
    Console.WriteLine($"Prop3={blah.Prop3},Prop4={blah.Prop4}");
    Console.WriteLine($"Prop3={blah.Prop3},Prop4={blah.Prop4}");
    Console.Read();
}

在以上範例中,Prop1 與 Prop2 意義相同,都是物件建立時給予屬性初始值,屬性值固定不變;Prop3 與 Prop4 則偏向動態性質,每次呼叫時都重新產生新值。其中 Prop2 與 Prop4 是 C# 6.0 起支援的新寫法,較傳統寫法簡潔。

實測結果如下,Prop1/Prop2 兩次呼叫結果相同,Prop3/Prop3 每次讀取都會拿到新的 GUID。

Prop1=9a818bfa-7789-4dc3-8eab-a1c526cdf6c3,Prop2=d09ae5a4-01e8-41fa-a888-b508c974e463
Prop1=9a818bfa-7789-4dc3-8eab-a1c526cdf6c3,Prop2=d09ae5a4-01e8-41fa-a888-b508c974e463
Prop3=76e8b7ac-0830-4715-9d78-6840e7d59b3e,Prop4=922858c2-294c-4178-8a74-bd7bef385eb2
Prop3=1af010c8-a926-4e17-ad77-83d481227171,Prop4=03a8ea07-cc09-42fc-b37d-77c9d6c087d6

【延伸閱讀】

擴充方法參數傳入 dynamic 型別出錯

$
0
0

呼叫擴充方法時傳入 dynamic 型別參數,發生以下錯誤:

'Blah' has no applicable method named 'ExtMethod' but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched. Consider casting the dynamic arguments or calling the extension method without the extension method syntax.

使用以下範例可重現錯誤,ExtMethod 為擴充方法,傳入字串參數時正常,改傳 dynamic 型別即出現上述錯誤:

想起先前遇過類似狀況:【茶包射手日記】CSHTML ViewBag無法使用擴充方法,因 ViewBag 為 dynamic 型別無法使用擴充方法,解法很簡單,將 dynamic 轉型就好。在本案例,也是改寫成 b.ExtMethod((string)d) 就能解決問題。

這讓我聯想到另一個狀況,撇開擴充方法不論,多載(Overloading)方法會依參數型別呼叫不同方法,遇上參數型別是 dynamic 會發生什麼事?

依據 C# 程式規格,dynamic 本質上可視為多了部分特異功能的 object:

dynamic is considered identical to object except in the following respects:

  • Operations on expressions of type dynamic can be dynamically bound (Dynamic binding).
  • Type inference (Type inference) will prefer dynamic over object if both are candidates.

Because of this equivalence, the following holds:

  • There is an implicit identity conversion between object and dynamic, and between constructed types that are the same when replacing dynamic with object
  • Implicit and explicit conversions to and from object also apply to and from dynamic.
  • Method signatures that are the same when replacing dynamic with object are considered the same signature
  • The type dynamic is indistinguishable from object at run-time.
  • An expression of the type dynamic is referred to as a dynamic expression.

簡單來說,面多載抉擇時,把 dynamic 想成 object 就對了~

程式範例-使用 Json.NET 將 Key/Value 陣列轉為物件屬性

$
0
0

專案遇到的需求:程式接收來自外界的 JSON 資料,物件之各屬性內容以 KeyValuePair<string, string> 陣列儲存,序列化結果如下:

{
"modType": [
    {
"Key": "I",
"Value": "獨立模組"
    },
    {
"Key": "J",
"Value": "聯合模組"
    }
  ],
 
"source": [
    {
"Key": "I",
"Value": "內部"
    },
    {
"Key": "E",
"Value": "外部"
    }
  ],
 
"statusOption": [
    {
"Key": "0",
"Value": "停止"
    },
    {
"Key": "1",
"Value": "運轉"
    },
    {
"Key": "2",
"Value": "暫停"
    }
  ]
}

若依此資料結構,JavaScript 前端 MVVM 繫結特定項目時要寫成 model.source[1].Value 不夠直覺,希望改成 model.source.E。JavaScript 要將 Key/Value 陣列轉成物件屬性不是難事,jQuery.each() 一行可以搞定:
var obj={};$.map(model.source,function(item){ obj[item.Key]=item.Value;});

不過,傳送繁瑣 JSON 資料到前端再簡化,自然不如在 C# 端直接轉換優雅,而這對 Json.NET 來說是小菜一碟。

程式範例附於下方,簡單說明原理:先將 JSON 字串用 JObject.Parse() 反序列化成 JObject 物件,透過 JObject.Properties() 可逐一取得象徵各屬性的 JProperty 物件,JProperty.Name 為屬性名稱,JProperty.Value 則為屬性值,在本案例為 Key/Value 物件組成的陣列。透過 p.Value as JArray 轉為 JArray 後可 foreach 取得陣列元素。陣列元件可視為包含 Key/Value 兩個屬性的 JObject,可透過 item["Key"]/item["Value"] 取值。我們為每個屬性建立一顆專屬 JObject,將 Key/Value 陣列以 propObj.Add(propName, propValue) 轉成 propObj 的一個個屬性值,藉以取代原有的 JArray,就完成了置換。其中有個小眉角,由於 Key 可能包含不合法的屬性名稱字元或格式(例如「.」字元或以數字起首),因此要藉由 Regex 取代及修正(數字起首時在前方加上「_」)。

staticvoid Main(string[] args)
{
    var json = System.IO.File.ReadAllText("data.json");
//將JSON轉為JObject
    JObject jo = JObject.Parse(json);
//逐一轉換各屬性
foreach (var p in jo.Properties())
    {
//原本Key/Value陣列方式表達選項?容
        JArray a = p.Value as JArray;
//準備一個新物件以屬性儲放選項
        JObject propObj = new JObject();
//將{ "Key":"..", "Value":"..."}視為JObject
foreach (JObject item in a)
        {
string propName = (string)item["Key"]; //取出Key
//將.換成_,數字起首時前方加_,避免產生無效屬性名
            propName =
                Regex.Replace(
//TODO:如有其他字元再擴充
                    Regex.Replace(propName, "[-.]", "_"),
"^[0-9]", //若以數字起始前方加_
                    m => "_" + m.Value);
//取出Value
string propValue = (string)item["Value"];
//新增成屬性
            propObj.Add(propName, propValue);
        }
 
        jo[p.Name] = propObj;
    }
 
    Console.WriteLine(JsonConvert.SerializeObject(jo, Formatting.Indented));
    Console.Read();
}

轉換結果如下,成功!

{
"modType": {
"I": "獨立模組",
"J": "聯合模組"
  },
"source": {
"I": "內部",
"E": "外部"
  },
"statusOption": {
"_0": "停止",
"_1": "運轉",
"_2": "暫停"
  }
}

以上寫法展示完 JObject/JProperty/JArray 的概念與應用方式,接著我們來抄捷徑:

JObject jo = JObject.Parse(json);
foreach (var p in jo.Properties())
{
    p.Value =
        JObject.FromObject(
        (p.Value as JArray).Cast<JObject>()
        .ToDictionary(
            o => {
string propName = (string)o["Key"];
                propName =
                    Regex.Replace(
                        Regex.Replace(propName, "[-.]", "_"),
"^[0-9]",
                        m => "_" + m.Value);
return propName;
            }, 
            o => (string)o["Value"]));
}

JArray 經由 ToDictionary() 轉成 Dictionary<string, string>,呼叫 JObject.FromObject() 就直接轉成 JObject,收工!

後話:Json.NET 並不是轉換效能最好的 JSON 程式庫,但其完整性、成熟度與應用彈性實在沒話說,只想學一套 JSON 程式庫,選它就對了!

貼文後經網友提醒,補上之前寫過的另一篇介紹:使用dynamic簡化Json.NET JObject操作兩帖併服,藥效加倍~ :P

方法多載(Method Overloading)與 dynamic

$
0
0

方法多載(Overloading)是指多個名稱相同但參數個數或型別不同的方法,編譯器依傳入參數的個數、型別與順序決定使用哪一個方法。概念上多載讓方法變得更彈性,能接受不同參數組合,符合更多應用情境。舉個常見的例子,Convert.ToByte() 可傳入 int, short, string, float, double, decimal, char… 等輸入值,將其轉成 byte,傳入 string 時還能指定 16 進位(fromBase)或 IFormatProvider。

我有個根深蒂固的觀念-多載解析都發生在編譯期間,編譯器依參數將函式指標指向同名方法的其中一個。上回在談擴充方法參數傳入 dynamic 型別出錯時提到:「當 dynamic 遇上多載介面解析,一律視為 object 就對了」,但事後想想覺得怪,「多載解析都是在編譯期間完成」不適用 dynamic 型別吧?dynamic 要到執行期間才知確實型別,編譯期間要如何判斷該套用哪個多載實作?廢話不多說,做個實驗便知:有個 OverloadingMethod 共有接收 int 或 string 兩個多載版本,測試時第一次傳入數字、第二次傳入字串、第三次將數字轉型成 dynamic。

class Program
{
staticvoid Main(string[] args)
    {
        OverloadingMethod(12345);
        OverloadingMethod("ABCDEF");
        OverloadingMethod((dynamic)12345);
        Console.Read();
    }
 
staticvoid OverloadingMethod(int i)
    {
        Console.WriteLine($"int version: {i}");
    }
 
staticvoid OverloadingMethod(string s)
    {
        Console.WriteLine($"string version: {s}");
    }
 
}

編譯後以 ildasm 解譯回 MSIL,答案揭曉!如下圖所示,前兩次分別呼叫 int 及 string 版多載方法(黃底部分),第三次則又臭又長,透過 System.Runtime.CompilerServices、System.CSharp.RuntimeBinder 命名空間的物件與方法隔水加熱完成。

改用 LINQPad 的 IL 檢視比較簡潔,原則上類似 Reflection,第三次呼叫,方法名稱"OverloadingMethod"變成字串變數,以 Invoke 方式執行。

所以,結論是多載原則上是在編譯期間決定實際呼叫方法沒錯,但遇上 dynamic 只能留到執行期間再決定。最後,以 C# in Depth- Overloading的這段說明為本議題劃上句點:

At compile time, the compiler works out which one it's going to call, based on the compile time types of the arguments and the target of the method call. (I'm assuming you're not using dynamic here, which complicates things somewhat.)

Viewing all 428 articles
Browse latest View live