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

ODP.NET 無法讀取 Oracle 欄位計算結果

$
0
0

同事報案,使用 Dapper + ODP.NET 呼叫某 Procedure,以 Ref Cursor 取資料時出現型別轉換錯誤,一路深入追查,發現問題跟是否用了 Procedure、Ref Cursor、 Dapper 都沒有關係,錯誤發生在 ODP.NET 層。

有問題的查詢涉及幾個高精確度的欄位運動,經過一番簡化,我先找出用下列查詢可重現問題。

使用 PL/SQL Developer 查詢不會出錯,但使用 ODP.NET OracleDataReader dr["N"]、dr.GetDecimal(0)、dr.GetValue() 都會出現「指定的轉換無效」(Invalid Cast Exception)錯誤,dr.GetDouble(0) 則能讀出結果:

由於出錯案例的爆點在幾個高精確度數字(NUMBER(14)、NUMBER(10,7))的乘除結果,一度以為跟運算成員的欄位精確位數大高造成的。

經過一番調查,我才搞懂背後發生什麼事,並有了新結論:

  1. Oracle NUMBER的最高精確位數高達 38 位,C# decimal只有 28-29 位,當 Oracle 欄位值高於 28 位,ODP.NET 祭出 .NET 世界精準度最高的 decimal 也裝不下,引發轉型別轉換失敗錯誤。(延伸閱讀:ODP.NET 如何決定數值欄位的 .NET 對應型別?
  2. 其實不必動用超大位數數字進行運算,只要一個簡單的除法搞出無窮小數就能重現 Oracle 位數超出 decimal 負荷的狀況。
  3. 當未指定型別,Oracle 會以 NUMBER(預設 38 位最高精確度)傳回運算結果,換言之,只要遇到無窮小數就會傳回滿滿 38 位數字讓 decimal 難看。
  4. PL/SQL Developer(或其他 Oracle 查詢軟體)顯示的結果小數位數是經過處理的,並非 Oracle 傳回的原始內容。

讓我用一個例子證明 1、2 兩點:

class Program
{
staticvoid TryGetValue(OracleDataReader dr, int n)
    {
try
        {
            Console.WriteLine($"N{n}={dr.GetValue(n)}");
        }
catch (Exception ex)
        {
            Console.WriteLine($"N{n} GetValue Error = {ex.Message}");
        }
    }
staticvoid DumpBinData(OracleDataReader dr, int n)
    {
string binData = BitConverter.ToString(dr.GetOracleDecimal(n).BinData);
        Console.WriteLine($"N{n} .BinData = {binData}");
    }
 
staticvoid Main(string[] args)
    {
using (var cn = new OracleConnection(Helper.GetCnnStr()))
        {
 
            cn.Open();
            var cmd = cn.CreateCommand();
            cmd.CommandText = @"
SELECT         
100/17 AS N0,
CAST(100/17 AS NUMBER(*,28)) AS N1,
CAST(100/17 AS NUMBER(*,29)) AS N2,
CAST(100/17 AS NUMBER) AS N4
FROM DUAL";
            var dr = cmd.ExecuteReader();
            dr.Read();
for(var i = 0; i < 4; i++)
            {
                Console.WriteLine($"---N{i}---");
                TryGetValue(dr, i);
                DumpBinData(dr, i);
            }
        }
        Console.Read();
    }
}

我用 100/17 製造出包含無窮小數的數值,再分別 CAST 換成 NUMBER(*, 28)、NUMBER(*,  29) 及 NUMBER 三種型別。結果只有轉成 NUMBER(*, 28) 的欄位能被正確讀取,其餘的都發生轉型失敗。另外,由 OracleDataReader.GetOracleDecimal().BinData 可以檢視 Oracle 傳回的原始資料,N0(未指定型別)及 N4(轉型為 NUMBER)的 BinData 是相同的,足以證明當未指定型別時,Oracle 採用最大精確度的 NUMBER 型別。

所以,當查詢結果出現除不盡狀況的狀況,其真實數字位數會高達 38 位,由 Oracle 工具軟體看到的結果是經過四捨五入的結果,如第一張圖例的 PL/SQL Developer 取 16 位,不同的工具結果不同,例如改用 SQLPLUS,位數就只有 9 位。

所以,不要拿工具軟體查詢到的數字當成標準答案!

最後,好奇在 SQL Server 上會不會遇到同樣的狀況?答案是不會:

SQL 整數相除結果預設為整數,非整數相除預設精準度預設為 Single,除非刻意轉型讓位數破錶,否則不會出錯,而錯誤訊息為「轉換溢位」較明確。

結論:

當在 Oracle 查詢使用除法運算,請記得轉型讓精準度夠用就好,否則一旦出現除不盡的狀況,你就有得忙了。


LINE 反斜線變日圓符號(¥)之謎

$
0
0

電腦版 LINE 輸入與顯示時老將磁碟路徑的反斜線符號「\」改成日圓符號「¥」,挺困擾的:

推測可能與 Meiryo 明瞭體字型有關。如果你的 Windows 有安裝日文語言選項,以下網頁可證明在 Meiryo 字型中反斜線符號會變成 ¥。

原因出在並不是每個國都使用 \ 作為路徑分隔字元,日本是用 ¥,而韓國則是用 ₩。參考 

維基百科找到日韓鍵盤配置圖範例,右上角 Backspace 左側我們習慣的反斜線鍵位置,在日本為 ¥,在韓國為 ₩,也可證明這點:

知道是字型問題就有了方向,試著修改聊天字體顯示,由預設字體改為 Arial Unicode MS 或其他我們常用的中文字型:

實驗發現,聊天內容中的反斜線變正常了,但輸入文字區仍然不對。

將介面語系改成英文,文字輸入區的反斜線顯示就正常了。

再查深一點,在 C:\Program Files (x86)\Naver\LINE\res\skin\basic\css\common.cs 可以找到 LINE 借用 CSS 指定不同國別語系的 UI 字型,在 Chinese-Taiwan 部分 font-family 優先指定 Meiryo,而 English 部分則為 Tahoma,這就解釋了為什麼切成英文一切正常。

既然有 CSS,我們可以自己動手修正它嗎?本來可以,但後來 LINE 新版本似乎改了架構,將這些樣式設定打包成單一檔案(Qt Resource?),無法直接更改,既然不開放使用者客製調整,這問題就只能靠 LINE 自已修正了。

使用 WebClient FTP 上傳檔案發生 553 錯誤

$
0
0

在 .NET 要 FTP 上傳檔案,最精簡有效的做法莫過於使用 WebClient,例如:

using System;
using System.IO;
using System.Net;
 
publicclass CSharpLab
{
publicstaticvoid Test()
    {
string userName = "ftpAccount";
string password = "ftpPassword";
string uploadUrl = "ftp://myFtpServerHost/someFolder/test.txt";
byte[] data = newbyte[] { 0x31, 0x32, 0x33 };
        WebClient wc = new WebClient();
        wc.Credentials = new NetworkCredential(userName, password);
        wc.UploadData(uploadUrl, data);        
    }
}

這個寫法在專案中廣泛運用多年都沒遇到什麼問題。近日同事在 FTP 上傳某一主機時,卻發生 553 File name not allowed 錯誤:System.Net.WebException: The remote server return an error: (553) File name not allowed

手動 FTP 登入上傳,確定路徑、權限都沒有問題。爬文發現,原來 Linux FTP Server 與 IIS FTP Server 存在行為差異

Linux FTP Server 不像 IIS 有共用的 FTP 根目錄,登入後會處於該帳號的使用者根目錄(例如:/home/username),因此對Linux FTP Server,ftp://myFtpServerHost/someFolder/test.txt 將指向 /user/home/someFoler/test.txt,如果要指向絕對路徑,需多加一根「/」,寫成 ftp://myFtpServerHost//someFolder/test.txt 。

過去面對的 FTP 主機都是 Windows 故沒發現,第一次遇上 Linux FTP 主機才學到這點。問題在修改 URL 多加 / 後排除,結案。

閒聊:用 LINQ 還是自己寫 SQL?

$
0
0

前陣子在網路看到「該靠 EF(LINQ)還是自幹 SQL 語法(甚至一律轉成 SP)」 的討論,我的「個人偏好」挺明確-CRUD 可靠 EF/ORM 省工,複雜查詢或操作則走 Dapper自己寫 SQL。不過它只算是「偏好」,其效益因客觀條件劇烈變動,若無視開發者背景、人力資源配置、系統需求等各種因素,無限期支持 OOO 一定比 XXX 好,肯定會在特定情境踩坑。既然沒有一體適用的「建議」,我就只從開發老人的角度聊聊決策理由及優劣分析,不陷入追求「最佳解」的迷思。(充其量只會有符合某種情境的「最適解」)

依據看過的專案實踐,我把在 .NET 執行 SQL 邏輯的策略分成三種:EF(LINQ)、自幹 SQL 語法、將邏輯都放入 SP,先分析一下優缺點:

EF(LINQ)

優點:

  1. 開發人員不需要太多太深的 SQL 語法與知識,只需學好 LINQ,由 EF 負責翻譯成具備專業水準的 SQL 語法
    (EF 甚至已考慮許多連中鳥也忽略的細節,例如:讀取資料再更新前先比對欄位是否已被第三方異動,避免更新衝突。)
  2. 支援強型別及 Intellisense,不必擔心敲錯欄位名稱
  3. 相較於傳統 ADO.NET 新増與更新,寫法簡潔許多(例如:一行更新
  4. 完全杜絕 SQL Injection 風險
  5. 支援跨資料庫開發,實現「可抽換不同廠牌資料庫」的夢想

缺點:

  1. 難以應用 SQL 語法專屬特性簡化問題或提升效能(例如:CTE、Cursor、Index Hint…)
  2. 將複雜查詢邏輯轉成單一 LINQ 查詢很耗腦力
    (造成有些開發者會選擇將資料分批拉回再用 .NET 處理,其效率不如在 DB 端直接完成)
  3. 在某些狀況下 LINQ 可能被轉成較無效率的語法,效能低於預期

自幹 SQL 語法

優點:

  1. 得以完全發揮 SQL 語法特性,展現極致效能
  2. 直接提供 SQL 可省去由 LINQ 轉換的過程,效率略好
  3. 開發者充分掌握最終 SQL 指令,較能精準控制執行效能、鎖定範圍等細節
    例如:一次更新多筆資料的某個欄位
  4. 必要時可依 SQL 與 C# 強項拆解運算邏輯,由 SQL 查詢取得半成品再以 C# 加工轉為最終結果,靈活分工可節省可觀開發時間

缺點:

  1. 可能徹底誤用 SQL 語法特性,引發驚人災難(水能載舟,亦能覆舟呀)
  2. 有寫出 SQL Injection 的可能 Orz
  3. 遇到 INSERT/UPDATE 時要逐一列舉欄位名稱、宣告欄位值變數,超級囉嗦
  4. 綁死資料庫廠牌甚至版本,大幅提高抽換資料庫難度

一律寫成 SP

優點:

  1. SP 經過事先編譯,效率優於 .NET 動態傳入 SQL
  2. 採此模式時,一般多由專屬有經驗人員撰寫,與放任開發者自由發揮相比,較易管控品質
  3. 邏輯集中於 SP,有利於跨平台共用(例如:從 Java、PHP 也可直接引用)

缺點:

  1. 所有邏輯限定只能用 SQL 語言(T-SQL、PL/SQL…)實現,難度較高,需要更多相關知識才能勝任
  2. 某些情境將邏輯拆分成兩段,.NET 與 SQL 分別處理自己擅長部分是最省時省力的做法,限定在 SP 實現一切時就喪失此優勢
    (SQLCLR 算是例外,但與純 .NET 相比仍有限制)
  3. SP 的開發偵錯環境遠不如 .NET 程式便利(尤其 Visual Studio 一出,誰與爭峰?) 
  4. 所有運算負擔集中在資料庫端,無法靠增加 .NET 中台、前台主機數量提高系統產量

 

當優缺點已知,依據開發者背景、團隊人力配置、專案需求,要做出抉擇便非難事:

  • 如果你有一批經驗豐富,有能力用 PL/SQL 或 T-SQL 刷 LeetCode 面試題庫的專屬開發人員,就全部寫 SP 吧!不要為難前後端的 .NET 開發人員。
  • 如果系統要求將來資料庫平台可以抽換,EF 是較省力選擇,避免日後換掉 DB 痛到像剝皮。
  • 如果你的開發團隊 SQL 知識背景不深,使用 LINQ 可以確保大家完成水準之上的 SQL 相關系統,還不必擔心豬隊友搞出 SQL Injection 讓系統裸奔。
  • 如果你的系統一秒鐘幾十萬上下、資料量龐大,對資料庫一丁點效能提升也錙銖必較,那麼別猶豫,SQL 絕對要自己寫。

回到我的個人觀點,明知 .NET 端有一堆神兵利器還限定自己只用 T-SQL、PL/SQL 解決問題,是信仰堅定的苦行僧才走的修行之路,專案團隊傾向全端開發沒有專門研究 DB 的人員,全面 SP 化對我來說太苦太難成本太高,優先排除。 那我為什麼偏好「查詢用 Dapper,CRUD 用 EF/LINQ」?

我愛死在 C# 輸入屬性名詞前幾個字,Visual Studio 帶出完整名稱,不用擔心奶油桂花手敲錯字 Bug 半天。而 new EntityObject,Add 後 SubmitChanges 就完成新増,跟自己組 INSERT INTO TXX (C1,C2,C3…) VALUES (@C1,@C2,@C3…),再 AddParameter("@C1")、AddParameter("@C2")、AddParameter("@C3") 相比,根本是跟人火併要帶手槍還是揮球棒的問題呀!

做 CRUD 借助 EF/ORM 省時又省力,是明智之舉。至於查詢,則有 Dapper 自己寫 SQL vs 寫 LINQ 轉 SQL 兩種選擇,要怎麼選?依我看法,SQL vs LINQ 就像手排車 vs 自排車的差異。

自排固然方便,但要能充分發揮車輛性能極致,手排才是王道!在今天,馬路上跑的幾乎全是自排車,在 F1 賽車界依舊是手排的天下:來源

在分秒必爭的賽道中,搭載扭力轉換器傳輸動力、透過排檔電腦決定檔位的傳統自排變速箱,雖然便利、讓車手可以專注在賽道路線的攻略,但是檔位的切換與動力的傳輸卻顯得遲緩且不直接,無法確實將檔位與引擎轉速維持在車手所需的範圍,結果就是影響實際的單圈速度。
即使傳統手排在主流賽事中已不常見,但追根究柢,序列式變速箱、自手排變速箱仍舊是以手排變速箱為基礎所發展而來,若以廣義而論,手排仍是賽車運動中的主流。

手排更能貫徹車手的意志,是好事也是壞事。手排讓舒馬克在賽道上風馳電掣,也讓菜鳥上坡熄火倒滑害你驚呼What The Fxxk。

EF 查詢轉換 SQL 的過程有許多眉角不易掌控,而只有自己寫 SQL 才能善用 CTE、Index Hint、WITH (NOLOCK) 等技巧讓查詢效能最佳化,我面對的系統需求甚少有抽換 DB 的可能,但對系統效能有較高要求,故只能假裝自己是專業賽車手,不妄想開自排一路打 D 檔拿下名次。(資安宣導:自己寫 SQL 請務必使用參數化查詢,提醒愛用參數組 SQL 字串的同學,閻羅王最近針對 SQL Injection 開發者研發了一批專用刑具在等著你們…)

你不會因為換手排車就會開比較快(也有可能是死比較快),走這條路必須投資時間學習 T-SQL、PL/SQL 語法、查詢技巧、效能考量,搶了原本 EF 代勞的重責大任,知識與經驗不足很容易砸鍋,這也是選擇自己寫 SQL 前必要的認知。

EF 與自寫 SQL 併用的組合,像是用高階語言快速開發,但易成瓶頸的重度運算改用 C/C++ 寫成程式庫從外部呼叫,試著各取其長處。這種做法比起純用高階語言複雜,需要多懂 C 並存在沒管好 Unmanaged 記憶體當機的風險,但如果目標是要省時省力又兼顧效能,這是很值得考慮的做法。

CSV 轉換利器-ServiceStack.Text

$
0
0

做專案免不了遇到匯出或讀取 CSV 的需求,將物件轉成逗號分隔字串看似小菜一碟,用 C# 串字串也能搞定,但魔鬼在細節裡:字串值如包含逗號就要用雙引號包夾,遇到雙引號要置換成兩個雙引號,如果字串內容有換行符號更是讀取識別時的一大挑戰… 不管是匯出或解析 CSV 都得費不少力氣。最近發現一個處理 CSV 的強大元件-ServiceStack.Text 的 CsvSerializer!

ServiceStack是一套用於快速打造 SOA 服務的 Framework 工具組(可取代 WCF、WebAPI),強調輕巧、快速。ServiceStack.Text則是其中處理 JSON、CSV、JSV 序列化與反序列化的程式庫(在 ServiceStack 自家評測中 JSON 處理速度比 Json.NET 快三倍),而 CsvSerializer 正好可解決專案中的 CSV 需求。

以下簡單示範如何利用 ServiceStack.Text 匯出及解析 CSV。

首先使用 NuGet 安裝 ServiceStack.Text:

我用一小段程式做示範,測試對象是自訂物件陣列。

  • Test1() 用 CsvSerializer.SerializeToCsv<T>() 將陣列轉為 CSV 字串。
  • Test2() 用擴充方法 .FromCsv<List<T>>() 將 CSV 字串再轉回物件陣列。
  • Test3() 則嘗試不定義強型別物件,將 CSV 還原回字串陣列進行客製化應用。

為了增加趣味挑戰性,當然要刻意在物件字串屬性穿插逗號、雙引號及換行,試試 ServiceStack.Text 的能耐。

using ServiceStack.Text;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ServiceStack;
using Newtonsoft.Json;
using System.Dynamic;
 
namespace CsvTest
{
class Program
    {
publicclass Entity
        {
publicint Num { get; set; }
public DateTime Date { get; set; }
publicstring Text { get; set; }
publicbool Flag { get; set; }
 
public Entity(int num, DateTime date, string text, bool flag)
            {
                Num = num;
                Date = date;
                Text = text;
                Flag = flag;
            }
        }
 
static Entity[] TestData = new Entity[]
        {
new Entity(1, new DateTime(2012,12,21), "Normal", true),
new Entity(16, new DateTime(2012,12,21), "Taipei, Taiwan", true),
new Entity(32, new DateTime(2012,12,21), $@"""雙引號""跟,都來一下
換行當然不可少
", false)
        };
 
 
staticvoid Main(string[] args)
        {
//Test1();
//Test2();
            Test3();
        }
 
//物件陣列轉成CSV
staticvoid Test1()
        {
            File.WriteAllText("E:\\CSVLab\\Test1.csv", 
                CsvSerializer.SerializeToCsv<Entity>(TestData),
//指定new UTF8Encoding(true)產生包含BOM標記的UTF8檔案
//不然Excel直接開啟會有亂碼
new UTF8Encoding(true));
        }
 
staticvoid Test2()
        {
            var csv = File.ReadAllText("E:\\CSVLab\\Test1.csv");
//記得using ServiceStack啟用擴充方法
            var data = csv.FromCsv<List<Entity>>();
            Console.WriteLine(JsonConvert.SerializeObject(data, Formatting.Indented));
            Console.Read();
        }
 
staticvoid Test3()
        {
            var csv = File.ReadAllText("E:\\CSVLab\\Test1.csv");
string[] propNames = null;
            List<string[]> rows = new List<string[]>();
foreach (var line in CsvReader.ParseLines(csv))
            {
string[] strArray = CsvReader.ParseFields(line).ToArray();
if (propNames == null)
                    propNames = strArray;
else
                    rows.Add(strArray);
            }
            Console.WriteLine($"PropNames={string.Join(",", propNames)}");
for (int r = 0; r < rows.Count; r++)
            {
                var cells = rows[r];
for (int c = 0; c < cells.Length; c++)
                {
                    Console.WriteLine($"[{r},{c}]={cells[c]}");
                }
            }
            Console.Read();
        }
 
    }
}

Test1() 順利地輸出 CSV,由結果驗證 CsvSerializer 遇到逗號時會自動加雙引號,遇到內含雙引號也懂得置換。

測試以 Excel 開啟匯出的 CSV,欄位分隔解析完全正確。(注意:匯出中文 CSV 時記得要傳 Encoding.UTF8 或 new UTF8Encoding(true) 參數避免亂碼。參考

Test2() 將 CSV 還原物件陣列也成功!

Test3() 使用 CsvReader.ParseLine()、CsvReader.ParseFileds() 將 CSV 拆解成多筆資料字串,再逐筆依欄位分解成字串陣列,有自己土砲過的人就知道要判斷 逗號 vs 夾在雙引號中的逗號、換行 vs 夾在雙引號中的換行 有多煩人,有了 ServiceStack.Text,一切簡單多了!

工具箱再添順手兵刃一件!

陸續接獲網友回饋:補充其他處理 CSV 的好選擇:

花 8.29 英鎊拯救世界,WannaCrypt 勒索病毒中場休息

$
0
0

好戲劇化的發展,震驚全球的 WannaCrypt 勒索病毒(嚴格來說是蠕蟲),在一位英國資安研究員註冊某個網域名稱後,中止了第一波攻擊。(讓我想起電影世界大戰裡莫名烙賽停擺的外星人)


照片來源:http://thestagblog.com/tuesdayapocalypse-waroftheworlds/

相信大家應該都從各大媒體看到報導了,WannaCrypt 勒索病毒參考先前美國國安局流出的攻擊程式,利用 Windows 的一個資安漏洞(微軟於今年 3 月已釋出安全更新),能主動攻擊感染同一區域網路中的未做 Windows Update 的 Windows Vista/7/8.1/2008R2 主機,將該主機的所有文件檔加密上鎖,要求美金 300 元的贖金。先在英國健保署、西班牙電信公司傳出嚴重災情,而俄羅斯、烏克蘭與台灣也受害嚴重,依據防毒公司 avast 的統計,全球共有 104 國家受害,超過 12 萬 6 千台機器被感染。而微軟為原本已停止支援多時的 Windows 2003/XP 破例緊急出了安全更新,可見事態之嚴重。

但 WannaCrypt 的這波猛烈攻擊在英國一位資安研究員註冊了某個網域後意外停止了。

經營 MalwareTech 部落格的一位英國 22 歲年輕資安研究員分享他意外拯救世界的經過

在 WannaCrypt 災情傳出後,MalwareTech 取得在英國健保署肆虐的 WannaCrypt 勒索軟體樣本準備進行研究,丟進隔離環境執行時,發現 WannaCrypt 會一直嘗試存取某個未註冊的網域名稱。

MalwareTech 在研究惡意程式時習慣會註冊拿下這類網域名稱,目的在於蒐集感染數據及研究破解之道,於是這回先花 8.29 英鎊註冊再說。

有趣的是,註冊網域名稱生效後,他接到其他研究員詢問,表示樣本似乎出了問題,已無法重現感染行為。

之後經過反組譯勒索軟體以及模擬未註冊網域情況,驗證了「勒索軟體只要檢測到該網域存在就會停止執行」,而網域名稱寫死在程式裡(有些惡意軟體會使用演算法動態改變使用的網域名稱,或同時檢查多個網域名稱才決定),換言之,這個網域名稱是當初設計用來停止活動的開關(概念是在研究室的沙箱環境中,連不該存在的網域也會有回應,此時惡意軟體會停止活動避免行跡敗露),除非勒索軟體改版,這波所散佈的 WannaCrypt 勒索病毒,都將因為 MalwareTech 註冊網域而中止活動,這波的攻擊應該已告平息。

不過,可以預見勒索病毒作者一定會很快改版,試圖捲土重來,大家快利用這個天下掉下來的機會,趕快檢查自己的 Windows Vista/7/8/2008 R2 是否已安裝好 MS17-010安全更新!(Windows 10 有強迫安全更新,風險較低)

試駕體驗-小工具程式 .NET Core 1.1 版試寫

$
0
0

.NET Core版本演進到 1.1,2.0 也已進入 Preview 階段,輕巧、高效能與跨平台是 .NET Core 最大的優勢,預估未來將成主流,雖然現階段用在工作上的機率不大,找到機會還是該提早練習體驗,以免時間到了來不及上車。最近用 .NET Core 1.1 寫了一支小程式,順手分享實作心得。

先說小工具程式的用途,需求很簡單:

已有兩個很小的 Web API 服務(就當是 Microservice 概念吧)。其中一個姑且稱之 WebStatus Service,負責監控多台 Web 主機,以 JSON 格式回報主機狀態如下:

[
    {
"Name": "Web1",
"Status": "PASS",
"UpdateTime": "10:00:00",
"Message": ""
    }, 
    {
"Name": "Web2",
"Status": "FAIL",
"UpdateTime": "10:00:02",
"Message": "No Response"
    }
]

另外一個 LineNotify Service 則可透過 LINE Notify 發送通知給相關人員。(就是上回介紹過用 LINE Notify / LINE Login 技術開發的實驗專案)

小工具的任務很單純,依固定間隔執行,從 WebStatus Service 取得主機狀態,若發現有主機異常,就將主機名稱、時間與錯誤訊息以 LINE Notify 通知維運人員。這種長期執行排程,丟到低耗電低成本 Linux 小電腦跑是個好主意,改用 .NET Core 開發就可以具備此一優勢,加上程式很小,拿來體驗新技術試水溫正好,就像盲目約會約看電影最好是一樣道理:苗頭不對默默看完快閃,感覺良好再約晚餐… (謎之聲:啊不就很會?)

先聲明,我對 .NET Core 還沒做過深入研究,主要憑藉過去使用 .NET Framework 的開發經驗,依直覺行事,遇到問題就爬文求解,就是傳說中的 GOD(Google Driven Development)/ SOD(Stackoverflow Driven Development),不是良好示範,但猜想是實務上蠻多人踏進新領域應該都是採取此種入水姿勢,就當成一次探險好了,測試「有經驗的 .NET 開發者是否不需訓練就能上手 .NET Core 1.1?」 :P

我使用 Visual Studio 2017 開發,新増專案時選擇 Visual C# / .NET Core / Console App (.NET Core)

乍看程式寫起來跟 .NET Console App 差不多,但深入到細節就會開始體驗到 .NET Core 與 .NET Framework 的細微差異。

WebStatus 與 LINE Notify WebAPI URL 當然不該寫死在程式裡,放進 config 才是王道。我遇到的第一個問題是 .NET 不再使用 app.config 與 appSettings,改採開放政策,允許開發者依需求透過不同 Provider 使用 INI、JSON、XML、環境變數,甚至 Azure Key Valut 取得設定。對我而言,JSON 是最親切的選項,做法是先安裝 Microsoft.Extensions.Configuration 與 Microsoft.Extensions.Configuration.Json,以 new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build() 讀入 JSON 檔並建立 IConfiguration 物件,再以 Configuration["WebStatusUrl"] 讀取設定。官方文件有蠻淺顯但詳細的介紹。

接下來馬上遇到第二個問題:我呼叫 Web API 最常用的 WebClient 不支援 .NET Core!爬文找到替代品 HttpClient,與 WebClient 相比有不少優點:支援DNS解析快取、Cookie/身分驗證設定,可同時發送多個Request、基於 HttpWebRequest/HttpWebResponse(易於測試)、IO相關方法均採非同步,缺點是不支援FTP,對本專案無影響。延伸閱讀

我寫了一個函式模擬 WebClient.DownloadString(),傳入 URL,取得傳回結果:

static async Task<string> HttpGet(string url)
        {
//WebClient 無法跨平台,改用HttpClient http://stackoverflow.com/a/30286173/288936 
            var hc = new HttpClient();
            var resp = await hc.GetAsync(url);
            resp.EnsureSuccessStatusCode(); //未傳回HTTP 200即拋出例外
return await Task.Run(() => resp.Content.ReadAsStringAsync());
        }

要傳送 Url 參數時發現 .NET Core 沒有 HttpUtility 可用,要換一下元件,改用 System.Net.WebUtility.UrlEncode() 即可。

除此之外,其餘部分倒是挺順利。取得 JSON 後利用 Json.NET 轉回物件陣列(Yes,Json.NET 有 .NET Core 版本!),再用 LINQ .Where(o => o.Status == "FAIL") 挑出異常項目,為了避免故障期間狂發通知轟炸,我用了點技巧,將取得結果以 System.IO.File.WriteAllText() 寫檔保存,每次發出通知前與上次狀況比對,若上回就已異常就不發通知,因此只有在正常變異常,或異常變正常時才會接到訊息。這段邏輯用 LINQ 寫輕鬆愉快又簡潔,我愛死 LINQ 跟 C# 了!

//取得上次執行結果
string lastJsonFile = Path.Combine(Directory.GetCurrentDirectory(), "LastStatus.json");
if (!File.Exists(lastJsonFile)) File.WriteAllText(lastJsonFile, "[]");
 
conststring failStatus = "FAIL";
//讀取上次結果,篩選異常項目轉成主機名稱字串陣列
            var lastFailedNames = 
                JsonConvert.DeserializeObject<List<Result>>(File.ReadAllText(lastJsonFile))
                .Where(o => o.Status == failStatus).Select(o => o.Name).ToArray();
//從本次結果挑出異常項目集合
            var failedResults = JsonConvert.DeserializeObject<List<Result>>(json)
                .Where(o => o.Status == failStatus);
//用.Where()挑出上次正常本次異常的項目
            var newFailed = failedResults.Where(o => !lastFailedNames.Contains(o.Name));
if (newFailed.Any())
            {
string msg =     
$"【服務異常通報】\n{string.Join("\n", 
newFailed.Select(o => $"{o.Name}/{o.UpdateTime}/{o.Message}").ToArray())}";
//UrlEncode 在 System.Net.WebUtility
                var notifyRes = HttpGet(lineNotifierUrl + System.Net.WebUtility.UrlEncode(msg)).Result;
            }
//利用.Except()濾掉上次與這次都異常的項目,留下來的就是本次已恢復正常的項目
            var recovered = lastFailedNames.Except(newFailed.Select(o => o.Name));
if (recovered.Any())
            {
string msg = $"【服務恢復通報】\n{string.Join("", recovered.ToArray())}";
//UrlEncode 在 System.Net.WebUtility
                var notifyRes = HttpGet(lineNotifierUrl + System.Net.WebUtility.UrlEncode(msg)).Result;
            }
 
            File.WriteAllText(lastJsonFile, json);

就這樣程式寫完了,專案結構如下,在 Visual Studio 可直接執行也能逐行偵錯,開發體驗跟一般的 .NET 專案沒啥兩樣。

我打算直接將編譯結果部署到 Linux 直接執行,此時可使用 Publish 功能:

Publish 設定用預設值即可:

Publish 會將用到的 NuGet 程式庫 dll 以及 Console App 的 dll/pdb (.NET Core Console App 的編譯結果為 dll,不是 exe)集中到 PublishOutput 目錄下:

將這些檔案部署到 Ubuntu 主機,執行 dotnet WebStatusAlarm.dll 即可執行,再配合使用 crontab 設定排程,我的第一支實用級 .NET Core 程式就在 Ubuntu 主機正式商業運轉囉~

綜合來說,本次改用 .NET Core 1.1 寫小工具程式的經驗挺順利的,遇到的問題主要集中在慣用的 .NET Framework 程式庫元件在 .NET Core 不支援,但爬文都蠻快找到答案(Stackoverflow 是大寶庫),熱門的第三方程式庫如 Json.NET、NLog 都已經有 .NET Core 版本,而部署到 Linux 與執行的方法也算簡便。雖然試寫小工具程式的體驗與移轉大型專案不能相提並論,但 .NET 1.1 的成熟度與方便性比我原本預期為高,下回找機會再來玩 ASP.NET Core。

補充兩個 .NET Core 開發資源:

【茶包射手日記】WebControl Render() 發生 ArgumentNullException

$
0
0

遇到詭異茶包一枚。

同事 O 要新加入同事 D 與我共同開發的一個 Web Site 專案。同事 O 使用 Visual Studio 由 TFS 取得最新版本原始碼,編譯正常,卻在執行偵錯時發生錯誤:

[ArgumentNullException: 值不能為 null。參數名稱: key(英文:Value cannot be null. Parameter name: key)] System.Collections.Generic.Dictionary`2.FindEntry(TKey key) +11772221 System.Collections.Generic.Dictionary`2.TryGetValue(TKey key, TValue& value) +13 Microsoft.VisualStudio.Web.PageInspector.Runtime.WebForms.SelectionMappingRenderTraceListener.GetLiteralTraceData(LiteralControl literal, TraceData& data) +47 Microsoft.VisualStudio.Web.PageInspector.Runtime.WebForms.SelectionMappingRenderTraceListener.GetTraceData(Object renderedObject) +259 Microsoft.VisualStudio.Web.PageInspector.Runtime.WebForms.SelectionMappingRenderTraceListener.EndRendering(TextWriter writer, Object renderedObject) +35 System.Web.UI.RenderTraceListenerList.EndRendering(TextWriter writer, Object renderedObject) +66 System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +170 System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100 System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +25 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +128 System.Web.UI.Control.RenderChildren(HtmlTextWriter writer) +13 System.Web.UI.HtmlControls.HtmlContainerControl.Render(HtmlTextWriter writer) +32 System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +66 System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100 System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +25 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +128 System.Web.UI.HtmlControls.HtmlTableRow.RenderChildren(HtmlTextWriter writer) +47 System.Web.UI.HtmlControls.HtmlContainerControl.Render(HtmlTextWriter writer) +32 System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +66 System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100 System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +25 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +128 System.Web.UI.HtmlControls.HtmlTable.RenderChildren(HtmlTextWriter writer) +47 System.Web.UI.HtmlControls.HtmlContainerControl.Render(HtmlTextWriter writer) +32 System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +66 System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100 System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +25 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +128 System.Web.UI.Control.RenderChildren(HtmlTextWriter writer) +13 System.Web.UI.WebControls.WebControl.RenderContents(HtmlTextWriter writer) +12 System.Web.UI.WebControls.WebControl.Render(HtmlTextWriter writer) +32 Afa.WebControl.ListAssistant.Render(HtmlTextWriter output) in X:\MySource\ListAssistant.cs:1269

爆炸點在某顆古老的自製 Web Control 元件:

有幾點很可疑:

  1. 同一專案在同事 O 加入前,在同事 D 與我的電腦執行完全正常。
  2. 該 Web Control 元件已在正式環境運轉多年,未遇過類似錯誤。
  3. 爆炸發生在父類別 System.Web.UI.WebControls.WebControl 的 Render(),不是自製程式邏輯,莫非是 .NET Framework 內部的 Bug?

摸不著頭緒之際,為滿足另一系統要求試著將網站掛在 IIS 執行,結果就是正常的!我馬上聯想到:莫非是 Visual Studio 偵錯機制作祟?

BINGO! 在取消 Browser Link 功能後,錯誤瞬間消失!

由以上現象,我大約猜到錯誤原因:當啟用 Browser Link 時,Visual Studio 變成 SignalR 伺服器,並透過 HTTP 模組在每個網頁插入一段 JavaScript 與 SignalR 持續連線,而錯誤 Callstack 出現的 Microsoft.VisualStudio.Web.PageInspector.Runtime.WebForms.SelectionMappingRenderTraceListener 應是負責在 WebForm 控制項加上標記,以便將瀏覽器網頁上的元素關到 Visual Studio 設計檢視對應的控制項,而這段邏輯與自訂元件的某些行為表現衝突而肇禍。

事後用上述關鍵字爬文,查到不少文章,錯誤訊息都是 Value cannot be null. Parameter name: key,都建議關閉 Brower Link 排除。

結案收工!


NuGet 小技巧-NLog 套件 .NET Core 支援

$
0
0

前陣子開始體驗 .NET Core 開發後,最常面臨的問題多是某個慣用 .NET 基本元件、第三方程式庫是否在 .NET Core 能繼續使用。此時就能明顯看出西瓜偎大邊效應,常用、熱門、活躍的程式庫,跟隨新平台、新技術的腳步會比較快,某些冷門或開發社群已不再投入的程式庫,平台切換之際可能就是說再見的時刻,將留在港邊目送你航向大海。(所以選擇第三方程式庫時採取「拿香跟著拜」策略是道理滴)

上次提到,我常用的 Json.NET、NLog 都已支援 .NET Core。Json.NET 隨裝隨用沒遇到什麼問題;而 NLog 官網列舉的支援平台已包含 .NET Core,但 NuGet 下載最新版 NLog 4.4.9 時卻出現不相容 netcoreapp1.1 的錯誤訊息:

Restoring packages for X:\MySrc\WebStatusAlarm\WebStatusAlarm.csproj...
Package NLog 4.4.9 is not compatible with netcoreapp1.1 (.NETCoreApp,Version=v1.1). Package NLog 4.4.9 supports:
  - monoandroid10 (MonoAndroid,Version=v1.0)
  - net35 (.NETFramework,Version=v3.5)
  - net40 (.NETFramework,Version=v4.0)
  - net45 (.NETFramework,Version=v4.5)
  - sl4 (Silverlight,Version=v4.0)
  - sl5 (Silverlight,Version=v5.0)
  - wp8 (WindowsPhone,Version=v8.0)
  - xamarinios10 (Xamarin.iOS,Version=v1.0)
One or more packages are incompatible with .NETCoreApp,Version=v1.1.
Package restore failed. Rolling back package changes for 'WebStatusAlarm'.

研究後發現,NLog 從 5.0 起才加入 .NET Core 支援,而 5.0 仍在 Beta 階段。要安裝非正式版(Alpha、Beta、Preview…)的 NuGet 套件,需勾選下圖中的「Include prerelease」選項,勾選後在下拉清單即可選取 5.0.0 測試版安裝:

NLog .NET Core 的使用方式與 .NET Framework 版完全相同,原來的程式寫法與 NLog.config 設定可以一行不改直接沿用,讚!

由於 .NET Core 仍在起步階段,部分 NuGet 套件支援 .NET Core 的版本仍未正式釋出,有此經驗下回就懂得查詢測試版,必要時先裝測試版搶先。

【後記】下圖是安裝 NLog 時,NuGet 列舉需一併安裝的 CoreFx NuGet 套件,傳統上直覺屬於 Framework 內建的 System.Collections.Sepcialized、System.ComponentModel.Primitives、System.Data.Common 變成獨立 NuGet 套件,突顯 .NET Core 將 Framework 模組化套件化的設計哲學,發揮用多少裝多少的特性,也是它得以輕量化提高效能的關鍵吧?

值得一提的是,上述的每一個 System.* 程式庫,你都可以在 Github上找到原始程式,找到 Bug 或想到改進的點子,還可以提交修改建議給開發團隊,十多年前學習 C# 時,完全料想不到 .NET 會發展成今天的模樣,哈!

使用 Visual Studio 2017 開發 RDLC 報表

$
0
0

很久沒用 RDLC 報表跟 Report Viewer,這幾天有機會試著在 VS 2017 編輯 RDLC 報表,發現做法跟以往不同,做個筆記。

首先,Visual Studio 2015 時代 Report Service 報表被包含於 Microsoft SQL Servers Data Tools 安裝選項, VS2017 改為要額外下載安裝:Microsoft Rdlc Report Designer for Visual Studio - Visual Studio Marketplace

安裝過程遇到小插曲-安裝程式回報找不到可支援的 Visual Studio 版本!直到 VS2017 安裝 Update後才告排除。

安裝 Report Designer 後即可在專案中新増或編輯 RDLC 報表。要新増專案項目時則發現另一事:猜猜 Report 屬於哪個子分類,Data?SQL Server?還是 General?都不是,它被歸於 Visual C# 的直屬清單,不屬於任何分類。平時使用時,直接在右上角「Search Installed Temlates (Ctrl+E)」欄位輸入"report" 篩選比較快。

接著來看如何在網頁顯示 RDLC 報表。目前為止,使用 WebForm 配合 ReportViewer Control 是唯一做法,若是 ASP.NET MVC 專案,也建議加一個專屬 WebForm 顯示報表比較省時省力。使用純 JavaScript / HTML5 呈現 RDLC 報表理論上可行,但目前我沒發現可用的元件或程式庫。

這個年代,ReporViewer 元件當然也該改由 NuGet 下載,不再走下載程式跑安裝程序註冊元件的老路。不過,NuGet 裡的 ReportViewer 套件版本挺亂,輸入 "reportviewer" 關鍵字你會看到一堆版本各異,由網友(套件名稱後方不是 by Microsoft)整理上傳的安裝套件:

試了幾個網友打包的版本,用起來倒也沒什麼問題。如果你要找由微軟官方維護的最新版,關鍵字請輸入 "reportviewercontrol":

目前最新版是 14.0.0.0,安裝 NuGet 套件的同時,web.config 會自動加上 buildProviders、httpHandlers 等必要設定:

<system.web>
  <compilation debug="true" targetFramework="4.5.2">
    <buildProviders>
      <add extension=".rdlc" type="Microsoft.Reporting.RdlBuildProvider, Microsoft.ReportViewer.WebForms, Version=14.0.0.0, Culture=neutral, PublicKeyToken=89845DCD8080CC91" />
    </buildProviders>
    <assemblies>
      <add assembly="Microsoft.ReportViewer.Common, Version=14.0.0.0, Culture=neutral, PublicKeyToken=89845DCD8080CC91" />
      <add assembly="Microsoft.ReportViewer.WebForms, Version=14.0.0.0, Culture=neutral, PublicKeyToken=89845DCD8080CC91" />
    </assemblies>
  </compilation>
  <httpRuntime targetFramework="4.5.2" />
  <httpHandlers>
    <add path="Reserved.ReportViewerWebControl.axd" verb="*" type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=14.0.0.0, Culture=neutral, PublicKeyToken=89845DCD8080CC91" validate="false" />
  </httpHandlers>
</system.web>

依據官方部落格教學,在 WebForm 加上組件註冊宣告:

<%@ Register Assembly="Microsoft.ReportViewer.WebForms, Version=14.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" Namespace="Microsoft.Reporting.WebForms" TagPrefix="rsweb" %>

在 WebForm 中加上 ScriptManager 跟 rsweb:ReportViewer 控制項,

<asp:ScriptManager runat="server"></asp:ScriptManager>
<rsweb:ReportViewer ID="rptViewer" runat="server" Width="850px" Height="680px">
</rsweb:ReportViewer>

在 Server 端設好 LocalReport 屬性,就能在網頁顯示 RDLC 報表了:

但新版 ReportViewer 的工具列讓我大吃一驚,完全走 RWD、Bootstrape 風的大圖示、寬間隔,與我手邊現有專案慣用的緊密小字樣式格格不入!所幸,工具列樣式不難透過 CSS 調整,開 F12 研究後加了幾行 <style>,簡單調整緊緻拉提一番,先減少突兀感,至於要更美觀的任務就要靠負責 UI 設計的同事換圖示整型了。

順手附上我的拉皮設定:

<style>
span.glyphui {
    margin: 1px;
}
.ToolbarPageNav input {
    margin: 1px;
}
.ToolbarRefresh.WidgetSet,
.ToolbarPrint.WidgetSet,
.ToolbarBack.WidgetSet,
.ToolbarPowerBI.WidgetSet {
    height: 32px;
}
.WidgetSet {
    height: 32px;
}
.HoverButton {
    height:32px;
}
.NormalButton {
    height:32px;
}
.NormalButton table,
.HoverButton table,
.aspNetDisabled table {
    width: 56px;
}
.DisabledButton {
    height:32px;
}
.ToolbarFind,
.ToolbarZoom {
    padding-top: 3px;
}
.ToolBarBackground {
    background-color: #bdbdbd!important;
}
</style>

2017 海山馬

$
0
0

邁入第五年,海山馬是我唯一年年參加從未缺席過的賽事。(當然,這場命運的糾纏多少源自意外落馬復仇雪恥的情節)

今年第一次不再獨跑,莫名組了亂跑團,打算邊跑邊喇笛賽,隨意跑完就好。

一樣是熟悉的起跑拱門布條,但今年把6塗掉改成7的痕跡好明顯,完全不需揣測琢磨,是去年那塊沒錯 XD

六點起跑,天氣還不錯,多雲沒什麼太陽,比照往年的記錄算好。下面這張照片拍出像飛碟一樣的東西,估計是鳥吧?:P

過去全馬的跑法都是先往公館方向跑到中正橋折返,回到起點完成 28K,再往三峽跑到河濱哨所站來回 14K。今年路線大改,改成先往三峽跑 5K 折返,回到起點 10K,再往南跑大漢橋來回再 10K,總共跑兩趟累積到 42K。路線重複較無趣不說,重點是這様我就無法重訪「黑大落馬紀念碑預定地」了啊啊啊啊~

浮洲濕地附近的荷花,由於今年路線調整,經過時間已晚,不如清晨美。

跑著跑著,雲層漸散,太陽出來惹~起跑前為了要不要擦防曬乳原本還有點猶豫,嘿,押對寶了。

往大漢橋段看到三棵好大的樹,連跑多年,第一次發現。應該是因為之前經過這裡還處於前半馬的亢奮期,專心催油門,無心觀景吧?今年前後經過四次,後面兩次全在散步(雖然有太陽,但風好大好舒服呀,當然要散步放鬆身心囉),想不注意也難,呵。

就這樣恥力全開,沿路大啖西瓜、狂飲豆漿,慢慢地晃回終點,以 SUB6 再下一馬~

不免俗要跟會場附近的山羊打聲招呼。

  
  

獎牌還是維持一貫傳統,公版獎牌,不標全半馬,但正面印了賽事名稱還畫了匹馬,也算突破,呵。

使用物件陣列作為 RDLC 報表資料來源

$
0
0

我喜歡 RDLC勝過 Report Server 報表的原因之一是報表資料來源不限定來自資料庫,可以是自己組裝的 DataTable, 甚至是自訂資料物件,具有無比的應用彈性。這篇文章用一個極簡的範例,展示如何使用 List<T> 當成 RDLC 資料來源。

我用中國重大歷史事件一覽表當素材:

先設計一個包含 Year 與 Description 屬性的 Event 類別代表每個歷史事件,寫一個 MyHistoryStore 透過 GetAllEvents() 解析文字檔吐回 List<Event>。這裡有個重點,傳回結果型別必須是 IEnumerable(T[]、List<T> 都算),RDLC 才會視為可用的資料來源。(參考:To be accessible as a data source, a class must expose a method or property that returns an IEnumerable. You can add a class or a reference to the assembly for a class to your project.)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Web.Hosting;
 
namespace RDLCTest
{
publicclass Event
    {
publicstring Year { get; set; }
publicstring Description { get; set; }
    }
 
publicclass MyHistoryStore
    {
publicstatic List<Event> GetAllEvents()
        {
using (var sr = new StreamReader(
                HostingEnvironment.MapPath("~/App_Data/history.txt")))
            {
string line;
                List<Event> list = new List<Event>();
while ((line = sr.ReadLine()) != null)
                {
                    var i = line.IndexOf(",");
if (i > 0)
                    {
                        list.Add(new Event()
                        {
                            Year = line.Substring(0, i),
                            Description = line.Substring(i + 1)
                        });
                    }
                }
return list.ToList();
            }
        }
 
    }
}

上述物件寫好記得先編譯一次,報表設計精靈才能從 DLL 中找出可用的資料屬性或方法。接著我們在專案中新増「Report Wizard」(開 Report 純手工改 RDLC XML 硬幹也成,但非鐵血硬漢勿試):

使用 Visual Studio 2017 新増 Report Wizard 時可能跳出下警告,只要你的 Report Designer 不是來路不明,就信任吧!

進入 Report Wizard 的 DataSet 屬性設定介面,先輸入 DataSet 名稱(本例用 HistoryDataSource,這個名字要記下來等等會用到),在 Data Source 下拉選單選取專案 NameSpace(本例為RDLCTest),接著 Available datasets 下拉選單應可找到剛才寫好的 MyHistoryStore(GetAllEvents) (記得在此步驟前專案要先編譯過,不然會找不到),選擇後右方會出現 Year、Description 欄位資料,代表 Wizard 已正確找到方法並識別出資料物件屬性。

接下來的操作與一般 RDLC 報表設計相同,在此不多贅述。

提示幾個設計報表要注意的小地方。

下圖白色部分是 Body 的範圍,要設定整張報表設定,要在白色範圍之外按右鍵,選「Report Properties」。

如果報表打算列印出來,就要留意紙張的尺寸、邊距。

欄位寬度總和記得不要超出紙張寬度減去邊距,不然每頁會印成兩張,實務上我習慣直接敲尺寸數字,比憑直覺拖拉精準。

Server 端的寫法很簡單,指定 ReportPath、DataSources.Add() 一個 ReportDataSource,建構時傳入 DataSet 名稱與 IEnumerable 就搞定了。這裡的 DataSet 名稱要輸入我們在 Report Wizard 一開始設定 DataSource 時給的名字-HistoryDataSource,若名字不符會產生「尚未提供資料來源 'HistoryDataSource' 的資料來源執行個體。」錯誤。(關於 ReportViewer 使用說明請參考前文

publicpartialclass Report : System.Web.UI.Page
    {
protectedvoid Page_Load(object sender, EventArgs e)
        {
if (!IsPostBack)
            {
                rptViewer.ProcessingMode = Microsoft.Reporting.WebForms.ProcessingMode.Local;
                rptViewer.PageCountMode = Microsoft.Reporting.WebForms.PageCountMode.Actual;
                rptViewer.LocalReport.ReportPath = Server.MapPath("~/ObjDataSrcReport.rdlc");
                rptViewer.LocalReport.DataSources.Add(
new Microsoft.Reporting.WebForms.ReportDataSource("HistoryDataSource", 
                    MyHistoryStore.GetAllEvents()));
            }
        }
    }

薑!薑!薑!薑~ 搞定收工。

最後補充,PageCountMode預設為 Estimate,頁數會顯示成 1 of 2?、2 of 3?,看不出實際總頁數。要設定為 Actual 才會顯示正確總頁數,Estimate 模式能避免某些情境下為取得總頁數拖累效能,本案例為記憶體中的資料物件無此疑慮,可放心設成 Actual。

【笨問題】在 Chrome 如何檢視 SSL 憑證?

$
0
0

一直以來, 遇到 Chrome 提示安全連線問題,我的第一個動作是在網址前方按右鍵查看問題並檢視憑證資訊:(如下圖)

不知從哪一版 Chrome 起,在不安全警示的右鍵選單不再顯示憑證問題詳細資訊,也無法檢視憑證資訊,只有一個「瞭解詳情」連結指向一篇 FAQ 說明。

不得其門而入,迫不得已我只好改用 IE 開啟查詢憑證問題。

鄉愿了好一陣子,今天痛下決心,認真爬文,才知道這是 Chrome 56 版做的調整。憑證資訊搬家了-按 F12 開發者工具,在 Security 頁籤下,有比以前詳細的問題說明,而檢視憑證資訊按鈕也在這兒。

終於,不用為了看憑證開 IE 囉~

SQLite 資料庫 C# 程式範例-使用 Dapper

$
0
0

最近想在 Coding4Fun 專案使用資料庫管理英文單字及測驗結果。情境本身有些小尷尬,評估規模與複雜度,若用資料物件配合序列化存檔實作有點吃力,搬出 SQL Express 又顯殺雞用牛刀,於是我想起免安裝又超級輕巧的 In-Process 資料庫首選-SQLite

完全沒有 SQLite 使用經驗,開啟 Visual Studio 寫個極簡範例當入門吧!

資料物件故意安排了 string、DateTime、int、byte[] 四種屬性,想測驗 SQLite 是否能滿足不同資料型別需求。

class Player
        {
publicstring Id { get; set; }
publicstring Name { get; set; }
public DateTime RegDate { get; set; }
publicint Score { get; set; }
publicbyte[] BinData { get; set; }
public Player(string id, string name, DateTime regDate, int score)
            {
                Id = id;
                Name = name;
                RegDate = regDate;
                Score = score;
                BinData = Guid.NewGuid().ToByteArray().Take(4).ToArray();
            }
        }
static Player[] TestData = new Player[]
        {
new Player("P01", "Jeffrey", DateTime.Now, 32767),
new Player("P02", "Darkthread", DateTime.Now, 65535),
        };

這年頭只要該應用夠熱門夠普及,通常不需要特別去搜尋元件或程式庫,往往在 NuGet 敲敲關鍵字試手氣就搞定,SQLite 就是個成功案例。

在 NuGet 搜尋 sqlite,前幾名全是我們要的結果-由 SQLite Development Team 提供的官方元件。System.Data.SQLite 是完整版,支援 LINQ、EF;既然 SQLite 標榜輕薄短小,資料庫存取方式我選擇用 Dapper,一路輕巧簡便到底。如果要走 Dapper,安裝 System.Data.SQLite.Core 就夠了:

SQLite 建立連線,執行指令的做法也是依循 ADO.NET 標準,跟 SQL 或 ORACLE 沒什麼兩樣,只是將 SqlConnection、OracleConnection 換成 SQLiteConnection,cn.Query、cn.Execute 等細節都一樣。

完整程式範例如下:

staticvoid Main(string[] args)
        {
            InitSQLiteDb();
            TestInsert();
            TestSelect();
            Console.Read();
        }
 
staticstring dbPath = @".\Test.sqlite";
staticstring cnStr = "data source=" + dbPath;
 
staticvoid InitSQLiteDb()
        {
if (File.Exists(dbPath)) return;
using (var cn = new SQLiteConnection(cnStr))
            {
                cn.Execute(@"
CREATE TABLE Player (
    Id VARCHAR(16),
    Name VARCHAR(32),
    RegDate DATETIME,
    Score INTEGER,
    BinData BLOB,
    CONSTRAINT Player_PK PRIMARY KEY (Id)
)");
            }
        }
 
staticvoid TestInsert()
        {
using (var cn = new SQLiteConnection(cnStr))
            {
                cn.Execute("DELETE FROM Player");
//參數是用@paramName
                var insertScript = 
"INSERT INTO Player VALUES (@Id, @Name, @RegDate, @Score, @BinData)";
                cn.Execute(insertScript, TestData);
//測試Primary Key
try
                {
//故意塞入錯誤資料
                    cn.Execute(insertScript, TestData[0]);
thrownew ApplicationException("失敗:未阻止資料重複");
                }
catch (Exception ex)
                {
                    Console.WriteLine($"測試成功:{ex.Message}");
                }
            }
        }
 
staticvoid TestSelect()
        {
using (var cn = new SQLiteConnection(cnStr))
            {
                var list = cn.Query("SELECT * FROM Player");
                Console.WriteLine(
                    JsonConvert.SerializeObject(list, Formatting.Indented));
            }
        }

執行結果如下:

測試成功:constraint failed
UNIQUE constraint failed: Player.Id
[
  {
    "Id": "P01",
    "Name": "Jeffrey",
    "RegDate": "2017-06-04T21:03:07.2899539",
    "Score": 32767,
    "BinData": "ZIYfzw=="
  },
  {
    "Id": "P02",
    "Name": "Darkthread",
    "RegDate": "2017-06-04T21:03:07.305952",
    "Score": 65535,
    "BinData": "PeOUJA=="
  }
]

補充幾則注意事項:

  1. 與 SQL、ORACLE 不同,SQLite 不需預先安裝伺服器及建立資料庫,不少應用情境是在程式執行時從無到有產生 .sqlite 檔案,建立資料表後寫入資料。所以我也體驗了一下這種玩法:先檢查 .sqlite 檔案是否存在,若不存在代表尚未初始化。建立 SQLiteConnection 時 SQLite 會自動建立 Test.sqlite 檔案,接著執行 CREATE TABLE 建立資料表。
  2. SQLite 的資料型別與 SQL 語法跟 MSSQL、ORACLE 有些出入,需花點時間熟悉。但關聯式資料庫的觀念大同小異,若已有相關開發經驗不難上手。(我在 tutorialspoint 找到蠻淺顯完整的教學,比官方文件易讀,是不錯的入門教材)
  3. 程式測試了基本的 INSERT、SELECT、Primary Key 重複檢核、int/string/DateTime/byte[] 型別的資料庫對應,全部順利過關。
  4. SQLite 參數比照 MSSQL 以 @paramName 表示(ORACLE 則用 :paramName)

最後強力推薦好用工具一枚-SQLite 界的 SSMS(或是 PLSQL Developer、Toad)-Firefox SQLite Manager Add-On,對於初接觸 SQLite 的新手來說,在不熟 SQLite 語法的情況下,能有 GUI 工具輔助建立資料庫、管理資料表、Index,測試 SQL 指令,猶如身處茫茫大海發現燈塔,自此不再徬徨無措,作者功德無量啊~ (關於更詳細的 SQLite Manager 的介紹,可參考梅干桑的文章

祝大家 SQLite 輕鬆上手~

檔案總管右鍵選單開啟免安裝版Notepad++

$
0
0

使用安裝版 Notepad++ 的同學請忽略本文,祝你有美好的一天。(同場加映萬用檔案總管右鍵開啟技巧一則,繼續讀下去也無妨。)

如果你選擇下載 Notepad++ 免安裝版(zip package、7z package、minimalist package),有個困擾是沒法在檔案按右鍵用「Edit with Notepad++」直接編輯檔案。

為此 Notepad++ 提供一顆元件(NppShell.dll,下載網址:http://notepad-plus.sourceforge.net/commun/misc/NppShell.New.zip),讓免安裝版也能使用「Edit with Notepad++」選單。

操作說明如下:(官方文件

將 NppShell.New.zip 解壓縮到 Notepad++.exe 所在目錄,使用管理者身分開啟 Command Prompt 視窗,輸入指令:regsvr32 /s /i NppShell64.dll ( 32 位元版 Windows 則用 NppShell.dll)

註冊完畢,在任何檔案按右鍵就會出現可愛的「Edit with Notepad++」選項囉~

且慢!用管理者身分跑 regsvr32 註冊元件?這不是免安裝!這不是免安裝!這不是免安裝!(在地上耍賴打滾)

好吧!如果閣下為深綠人士-追求純正綠色,講求絕對可攜,這裡有同事分享給我的替代方案一枚。

在檔案總管地址列輸入「shell:sentto」:

使用右鍵拖拉技巧在 AppData\Roaming\Microsoft\Windows\SendTo 資料夾建立 notepad++.exe 捷徑:

建好捷徑後,在檔案按右鍵,展開 Send to(傳送到)再選 Notepad++ 捷徑(實務上會將捷徑名稱「notepad++.exe – Shrotcut」更名成 Notepad++ 較美觀),就能用 Notepad++ 直接開啟該檔案,選單操作步驟多一步,但己經夠方便了。而這招不限定 Notepad++,也可搭配其他程式使用。

以上兩則小訣竅提供大家參考。


AJAX 網頁踩雷記:ASP.NET MVC 一秒變蝸牛

$
0
0

來看一個有趣實驗。

以下是個簡單的 ASP.NET MVC Controller,在 Index View 透過 AJAX 呼叫向 Server 讀取資料,SimuAjaxCall 則模擬 AJAX 呼叫動作,使用 Thread.Delay() 延遲指定秒數後傳回字串結果:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace LabWeb.Controllers
{
publicclass HomeController : Controller
    {
public ActionResult Index()
        {
return View();
        }
public ActionResult SimuAjaxCall(int seqNo, int delay)
        {
            System.Threading.Thread.Sleep(delay * 1000);
return Content($"AjaxCall-{seqNo}");
        }
    }
}

Index.cshtml 網頁內容如下。有個測試按鈕觸發同步發出 7 個 AJAX 呼叫 SimuAjaxCall,並將每次呼叫取得內容顯示在網頁上。先聲明,這並非良好的設計方式,依據 HTTP 規範,瀏覽器對同一網站來源的同時連線數有其上限,預設為 6 條,故第 7 個 AJAX 請求必須等待前 6 個請求有人執行完畢後才會送出,故設計時應盡可能透過合併或其他技巧,減少 AJAX Request 數目。(關於連線數上限議題,可參考這篇文章)網頁上還有另一顆「變蝸牛」按鈕,背後呼叫 /Magic/Snail 取得字串顯示,至於它背後做了什麼事,在此先賣個關子。

@{
    Layout = null;
}
 
<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Multiple AJAX Call Test</title>
</head>
<body>
<div>
<buttonid="btnAjax">測試 AJAX 呼叫</button>
<buttonid="btnSnail">變蝸牛</button>
<ul>
</ul>
</div>
<scriptsrc="https://code.jquery.com/jquery-3.2.1.js"></script>
<script>
        $("#btnAjax").click(function () {
//發出7個耗時1秒的AJAX呼叫
for (var i = 0; i < 7; i++) {
                $.post("/Home/SimuAjaxCall", { seqNo: i, delay: 1 })
                    .done(function (res) {
                        $("ul").append("<li>" + res + "</li>");
                    });
            }
        });
        $("#btnSnail").click(function () {
            $.post("/Magic/Snail")
                .done(function (res) {
                    $("ul").append("<li>" + res + "</li>");
                });
        });
</script>
</body>
</html>

我們的測試方法是先按「測試AJAX呼叫」,用 F12 開發者工具觀察 7 個 AJAX Request 的執行時間,接著使用「變蝸牛」魔法捲軸,之後再按一次「測試AJAX呼叫」觀察結果差異。實測結果如下:

第一次 7 個 AJAX Request 齊發測試(黃色部分)一如預期,前六個同步執行,第七個等了 1 秒才執行(1 秒綠色長條前方有 1 秒的灰色細長條為等待時間),驗證了瀏覽器對同一站台同時連線上限數為 6。

呼叫 /Magic/Snail 後再做一次相同測試,結果卻截然不同,七個 AJAX Request 分別花了 1 到 8 秒才執行完(紅色部分)!若使用者必須等待全部 AJAX Request 完成,等待時間也由 2 秒拉長到 8 秒。

這情境似曾相識,對吧?(感覺陌生的同學可參考 再探ASP.NET大排長龍問題

是的,揭曉 /Magic/Snail 裡的魔法,就是 Session!

using System.Web.Mvc;
 
namespace LabWeb.Controllers
{
publicclass MagicController : Controller
    {
public ActionResult Snail()
        {
            Session["A"] = 123;
return Content("變蝸牛!");
        }
    }
}

有趣的實驗,但發生在真實環境我可笑不出來… (補聲暗)

當時我遇到的狀況是網頁同時發出十多個 AJAX Request,前六個 AJAX 呼叫每個耗時 5-12 秒,但個別執行明明只要 1-3 秒。尤其某個應該瞬間完成的 Action,我在 Action 第一行跟最後一行寫 Log 記錄執行時間,發現 Action 從開始到結束花不到 0.1 秒,但 IIS Log 記錄跟瀏覽器端觀察到執行時間都在 4 秒以上,推測時間耗消在呼叫 MVC Action 之前或 Action 完成之後,卻又無從調查。同事 J 提醒可能與 Session 有關,這才恍然大悟。萬萬沒想到,原本以為不用 WebForm 就再也不用擔心 Session 阻塞交通,但事實不然…

一旦你在網站應用程式的某個角落用了 Session,MVC Action 也會大排長龍,一秒變蝸牛!

追究原因,程式用了某個 WebForm 時代的古老元件,其中使用 Session 保存狀態。傳統 WebForm 以 PostBack 為主,Session 的鎖定行為影響有限,當應用在會同時發出多個 AJAX Request 的場景,便導致了可怕的後遺症。 

解決方法很簡單,有同步 AJAX 執行需求且要避免被 Session 摧毁效能的 Controller 上請加註[SessionSate()],設成 Disabled 或 ReadOnly:(前題是這個 Controller 未使用 Session 或對 Session 只讀不寫)

    [SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)]
publicclass HomeController : Controller

修正後,Action 同步呼叫不再變蝸牛。

狠狠地被上了一課!如果你的網站採取 AJAX 方式設計,Session 這種活化石,就別再用了。

Session 有毒,所以呢?

$
0
0

上週我才意外發現:古老的 Session 不只會害 ASP.NET WebForm 大排長龍,就連 ASP.NET MVC Controller 也難逃魔掌,對 AJAX 網站效能的殺傷力直逼 BOSS 等級!

Session 是 ASP 時代就存在的活化石,允許每個工作階段有自己專屬的資料存放空間,不必費心規劃參數傳遞方式,在任一 ASPX 塞入資料,中間不管使用者歷經多少網頁做過多少事,只要有需要,在任何網頁呼叫 Session["…"],資料就回來了。由於它無腦直覺又好,故深受開發新手喜歡,成為許多 ASP/ASP.NET 開發人員鍾愛並廣泛使用的資料傳遞管道,因此也就不難理解,十多年來歷經 ASP、ASP.NET 1.1/2.0/3.5/4.0/4.5 一路演進到 ASP.NET MVC,它一直都在。

雖然到處可見,從程式架構的角度,Session 卻不是好東西,至少存在以下缺點:(我的觀察啦,歡迎 Session 系同學補充)

  • Session 具備全域變數性質,生命周期與使用範圍難以管理,常常使用完畢仍繼續佔用記憶體,另外也不利單元測試
  • Session["…"] 非強型別,無法靠 Visual Studio 快速追蹤讀取及寫入來源,追查問題不易,更名重構的難度也較高
  • In-Process Session 被保存於特定主機的記憶體,即便 WebFarm 有多台主機,限定該工作階段後續 Request 要由同一主機處理,不利於負載平衡最佳化。(負載平衡的最高境界要做到由 Web Server A 取得網頁 UI,按鈕送出時改由 Web Server B 處理也 OK)
  • 最後且最致命的一點,就是先前文章點出的 Session 預設互斥鎖定行為,導致所有用到 Session 的 ASPX 或 MVC Action 必須排成一列逐一執行,在 AJAX 模式中嚴重傷害效能

那麼,如果不用 Sesion,有什麼替代方案能避開上述缺點,達到類似效果?

  1. 要避免全域變數難以管理、易浪費記憶體,最簡單的做法是將狀態資訊透過呼叫函式、方法參數傳遞。如此,變數及物件的生命周期與範圍明確,傳送軌跡清晰,易於偵錯,單元測試也好寫許多。
  2. 在一些流程動線複雜的情境裡,要貫徹只用參數傳遞資訊往往需要堅定信仰與強大心理素質,並不容易。例如,A呼叫B、B呼叫C、C呼叫D、D再呼叫E,A要將資訊交給D,就得在 B、C、D、E 呼叫介面都加上該狀態參數並層層傳遞,程式碼光想就覺得噁心。因此很多時候,適度依賴「具有全域性質的狀態保存機制」可讓程式架構簡化,在 Web 開發領域,Cookie 是首選!
    但 Cookie 只適合儲存單純字串,由於會每個 Request Header 都會夾帶,長度愈短愈好。實務上,常見做法是為工作階段產生唯一的識別字串,真正的狀態資訊則保存在伺服器端(MemoryCache、資料庫… 等),當需要更新或讀取狀態,以識別字串為憑取出資料物件(存入資料庫的話還需序列化及反序列化), ASP.NET Session 就是用同樣原理實作而成。
  3. 要實作「以Cookie 為憑存取伺服器端物件」,MemoryCache 是最簡便的選擇,MemoryCache 可以指定到期時間或多久沒存取自動清除,能大幅減少耗用不必要的記憶體佔用,其本質跟 Session 一樣,可以用來保存各式狀態資訊或物件。(稍後的實作範例有更多細節)
  4. 將狀態資訊轉為類別的屬性值保存於 MemoryCache,有助於改善 Session["…"] 非強型別難以追蹤的缺點,例如以下程式示意:
    publicclass SessionInfo
        {
    publicstatic UserProfile Profile
            {
                get
                {
    return資料保存機制.Read<UserProfile>();
                }
                set
                {
    資料保存機制.Save<UserProfile>(value);
                }
            }
        }
  5. MemoryCache 固然簡便,但保存在記憶體將侷限 WebFarm 主機的調度彈性,遇到當機重開將導致工作階段資料遺失,要克服這點,得改用資料庫或獨立伺服器保存資料。Session 的強大之處也在於它已考慮到這一層,提供將 Session 資料保存在 SQL Server或是 StateServer的選項。依此要領,要自幹類似機制並非不可能,但複雜度不低,且需留意效能,超出本文討論範圍甚多,就此打住。
  6. 據我了解,有不少開發者使用 Session 從頭到尾只用於保存使用者身分,為此忍受 Session 獨佔鎖定的副作用有點不值得。如為此種情境,可考慮改用前述的 Cookie + MemoryCache 概念、ASP.NET Membership 機制,甚至最新的 ASP.NET Identity。為了 Session 改換認證底層工程是浩大了點,但導入新機制可獲得額外整合彈性與安全強化,應一併納入投資報酬率評估。

說了這麼多,相信不少開發者心中不免犯滴咕:「這堆有的沒的我懂,但我只想避免 Session 獨佔鎖定讓 ASP.NET 變蝸牛,完全不想為此大興土木啊啊啊啊」

如果線上網站已運轉十多年,雖然把 Session 當全域變數用架構很醜,但它頭好壯壯日進斗金。Session 鎖定是問題,但為此異動架構帶來風險,未必是明智之舉。

有沒有不用開腸破肚,只鎖定 Session 切除的微創手術?

這也是我在工作專案中遇到的實際挑戰-如何用最小幅度修改避免 Session 帶來的效能衝擊?

下是我的解法-UnobstrusiveSession(低調風 Session),用法與 Session 幾乎完全一樣,差別在於它的鎖定僅限於資料讀寫的短暫期間,不影響 ASP.NET 程式的並行性。

UnobstrusiveSession 核心程式如下,不到 80 行,其原理如先前所提,用 Cookie 保存工作階段識別碼(用 GUID 保證不重複),以 Cookie 為憑存取實際儲存於 MemoryCache 的資料,Cache 保存政策則比照 Session 設為 20 分鐘不存取自動清除:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Web;
 
publicstaticclass UnobtrusiveSession
{
static HttpContext CurrContext
    {
        get
        {
if (HttpContext.Current == null)
thrownew ApplicationException("HttpContext.Current is null");
return HttpContext.Current;
        }
    }
conststring COOKIE_KEY = "UnobtrusiveSessionId";
publicstaticstring SessionId
    {
        get
        {
            var cookie = CurrContext.Request.Cookies[COOKIE_KEY];
if (cookie != null) return cookie.Value;
//set session id cookie
            var sessId = Guid.NewGuid().ToString();
            CurrContext.Response.SetCookie(new HttpCookie(COOKIE_KEY, sessId));
return sessId;
        }
    }
publicstatic SessionObject Session
    {
        get
        {
            var cache = MemoryCache.Default;
            var sessId = SessionId;
if (!cache.Contains(sessId))
            {
                cache.Add(sessId, new SessionObject(sessId), new CacheItemPolicy()
                {
                    SlidingExpiration = TimeSpan.FromMinutes(20)
                });
            }
return (SessionObject)cache[sessId];
        }
    }
 
publicclass SessionObject
    {
publicstring SessionId;
        Dictionary<string, object> items =
new Dictionary<string, object>();
public SessionObject(string sessId)
        {
            SessionId = sessId;
        }
publicobjectthis[string key]
        {
            get
            {
lock (items)
                {
if (items.ContainsKey(key)) return items[key];
returnnull;
                }
            }
            set
            {
lock (items)
                {
                    items[key] = value;
                }
            }
        }
 
    }
}

使用時,只需將 Session["…"] 改寫成 UnobstrusiveSession.Session["…"] 即可,其餘都不用修改。我寫了一支測試網頁:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm.aspx.cs" Inherits="WebNoSession.WebForm" %>
 
<!DOCTYPEhtml>
 
<htmlxmlns="http://www.w3.org/1999/xhtml">
<headrunat="server">
<title>Session Lab</title>
<style>
        div {
            font-size: 9pt;
            margin: 6px;
        }
</style>
</head>
<body>
<formid="form1"runat="server">
<div>
            SessionId=<%= UnobtrusiveSession.Session.SessionId %>
</div>
<div>
            Session["Data"]=<%=UnobtrusiveSession.Session["Data"] %>
<br/>
</div>
<div>
            Session["Data"]: <asp:TextBoxrunat="server"ID="txtData"Width="80px"></asp:TextBox>
<asp:Buttonrunat="server"ID="btnSet"Text="Save"OnClick="btnSet_Click"/>
<asp:Buttonrunat="server"ID="btnRefresh"Text="Refresh"/>
</div>
</form>
</body>
</html>

Server 端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
 
namespace WebNoSession
{
publicpartialclass WebForm : System.Web.UI.Page
    {
protectedvoid Page_Load(object sender, EventArgs e)
        {
if (!IsPostBack)
            {
                txtData.Text = (string)UnobtrusiveSession.Session["Data"];
            }
        }
 
protectedvoid btnSet_Click(object sender, EventArgs e)
        {
            UnobtrusiveSession.Session["Data"] = txtData.Text;
        }
    }
}

如下圖所示,我開了 Chrome、Chrome 無痕視窗、IE,形成三個獨立的工作階段,各自擁有自己的 Session["Data"]。

補充幾點:

UnobstrusiveSession 使用 MemoryCache 保存資料,特性與 In-Process Session 相同(重啟或切換 Web Server 會遺失工作階段資料),Cache 部分採取 20 分鐘沒存取任何 Session 內資料就將所有 Session 資料清空的策略,與 ASP.NET Session 只要存取 ASPX 不一定要讀寫 Session 都會保留的策略不同。如果沒有每支 ASPX 都讀取使用 Session,20 分鐘後資料就會遺失,如要改善,可設定 MasterPage 或 Application_BeginRequest 持續讀寫 Session 避免資料被移除。

另外要強調一點,除了不會因鎖定機制重創 AJAX ASPX 效能,Session 的其他缺點 UnobtrusiveSession 一個都不少(全域變數、非強型別、不利負載平衡最佳化),其目的在力求以最低成本換掉 Session 解決鎖定問題,如果環境允許,建議調整系統架構避免使用 Session 這類機制才是上策。

將參照 DLL 併入單一 WPF 執行檔

$
0
0

將 .NET 執行檔跟所參照 DLL 合併成單一 EXE 檔的做法之前介紹過(Visual Studio編譯小技巧:工具程式一檔搞定 ),在專案用 NuGet 安裝 MSBuild.ILMerge.Task 就能輕鬆搞定。之前在 Console Application 用得挺順利,今天用在 WPF 卻卡在一個錯誤無法編譯:

The item "X:\TFS\RptBatchPrint\packages\Hardcodet.NotifyIcon.Wpf.1.0.8\lib\net451\Hardcodet.Wpf.TaskbarNotification.dll" in item list "ReferencePath" does not define a value for metadata "CopyLocal".  In order to use this metadata, either qualify it by specifying %(ReferencePath.CopyLocal), or ensure that all items in this list define a value for this metadata.  

用關鍵字爬文竟連回自己的文章,網友 agrozyme 留言提到相似問題,在 WPF 專案遇過一模一樣的錯誤稍後還留言分享了解決方法,但不幸地文件連結年久毁損,解法失傳…(搥牆)再爬文找到一兩則相似問題回報,但無人提出解決方案。

最後,換了關鍵字幸運找到另一種解法:Combining multiple assemblies into a single EXE for a WPF application – DigitallyCreated

原理是在 csproj 加入一段 AfterResolveReference 編譯作業指令(位置可放在 Microsoft.CSharp.targets 下方)

 

<TargetName="AfterResolveReferences">
<ItemGroup>
<EmbeddedResourceInclude="@(ReferenceCopyLocalPaths)"Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
<LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Target>

這段設定會在編譯時將專案參照到的 DLL 轉為 EmbeddedResource,之就可發現 EXE 變肥許多,用 JustDecompile 反組譯可看到專案參照的 Nancy.Hosting.Self、Json.NET、NLog、Hardcodet.Wpf.TaskbarNotification 等 DLL 已被轉成 EXE 內嵌資源:

接著在專案新増一個 Program.cs 當作啟動物件,先註冊自訂 AppDomain.CurrentDomain.AssemblyResolve 事件再呼叫原本的 WPF Application 啟動方法(App.Main())。在 AssemblyResolve 事件中,依組件名稱從 EmbeddedResource 中取出組件 DLL,再以 Reflection 方式載入傳回:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
 
namespace RptBatchPrintAgent
{
publicclass Program
    {
        [STAThreadAttribute]
publicstaticvoid Main()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
            App.Main();
        }
 
privatestatic Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
        {
            Assembly executingAssembly = Assembly.GetExecutingAssembly();
            AssemblyName assemblyName = new AssemblyName(args.Name);
 
string path = assemblyName.Name + ".dll";
if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
            {
                path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);
            }
 
using (Stream stream = executingAssembly.GetManifestResourceStream(path))
            {
if (stream == null)
returnnull;
 
byte[] assemblyRawBytes = newbyte[stream.Length];
                stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
return Assembly.Load(assemblyRawBytes);
            }
        }
    }
}

最後修改 Startup Object 指向 Program,大功告成!

就這樣,在 ILMerge 之外又學會一招  DLL 合併技巧~

註:原始文章下方有一些讀者回應提及這個做法在某些情境可能遇到的問題,如遇狀況可供參考。

RDLC 報表無法設定每頁顯示標題列

$
0
0

RDLC 呈現多頁報表時,預設並不會每頁重新顯示標題列。關於標題列要不要重複,Tablix Properties 有相關選項:

如下圖所示,Row Headers 跟 Column Header 都有 Repeat headers rows/columns on each page 選項可勾選。

經實測,這選項根本沒用啊,就算勾選也只有第一頁會出現標題列。

爬文發現當報表為表格式配置(另一種是矩陣式 Matrix)時,設定每頁顯示標題列需要一點小技巧。

如下圖,報表設計畫面的 Column Groups 右側有個小三角圖示,可開啟 Advanced Mode:

開啟後,Row Groups 與 Column Groups 會出現 (static) 項目。(1) 點選 Row Groups 的 (static),開啟屬性視窗完成以下設定:

RepeatOnNewPage = True (2)
KeepWithGroup = After (3)
FixedData = True (4)

設定完成,標題列就會出現在每一頁囉。

至於 Tablix 屬性頁設定為什麼無效則大有玄機,簡單來說,Tablix 屬性設定所指的 Row Headers/Column Headers 適用於矩陣式配置,並不是單純表格式配置裡一般認知的標題列,因此設定不適用,嚴格來說不是 Bug 而是不易理解的行為設計,想深入了解的同學可以參考這篇 MSDN 部落格文章

使用非 IE 瀏覽器列印 Reporting Service 報表

$
0
0

曾被 Reporting Service 報表列印 ActiveX 元件版本問題惡整過,在羚羊簇擁之下一路初探二探三探,記憶猶新。造成我有個根深蒂固觀念-ReportViewer 藉由 ActiveX 元件解決列印需求,所以在非 IE 瀏覽器上不能直接從網頁印報表是天經地義的事,遇到同事詢問,我的回答都是「無解」。但前陣子研究使用 Visual Studio 2017 開發 RDLC 報表我才赫然發現-ReportViewer 14.0 已經支援非 IE 瀏覽器線上列印囉!

對照舊版 ReportViewer(以11.0為例),下圖由上到下分為 IE、Chrome及Firefox,可以看到雖然三種瀏覽器都能開啟報表,但只有 IE 的工具列會顯示列印按鈕。

換成 ReportViewer 14.0 再試一次,現在三種瀏覽器都有列印按鈕可用囉!(灑花)

實測觀察,ReportViewer 的新做法是先將報表匯出成 PDF 檔再列印。好奇追進程式碼,找到一個 PdfPrint() 函式:

在 PdfPrint() 找到以下邏輯,解開 ReportViewer 跨瀏覽器列印 PDF 的奧祕:

var HasActiveXAdobe = function()
        {
try
            {
var adobePdf = new ActiveXObject("AcroPDF.PDF");
returntrue;
            }
catch (e)
            {
returnfalse;
            }
        }
 
var HasActiveXFoxit = function ()
        {
try
            {
var foxitPdf = new ActiveXObject("FoxitReader.FoxitReaderCtl");
returntrue;
            }
catch (e)
            {
returnfalse;
            }
        }
 
var GetPdfPluginName = function () {
var browserInfo = GetBrowser();
if (browserInfo.browser == "IE")
            {
if (HasActiveXAdobe()) return"AdobePDF";
if (HasActiveXFoxit()) return"FoxitPDF";
            }
else
            {
if (null == navigator.mimeTypes)
                {
return"NoPDFPlugin";
                }
var pdfPlugin = navigator.mimeTypes["application/pdf"];
if (pdfPlugin && pdfPlugin.enabledPlugin)
                {
return pdfPlugin.enabledPlugin.name;
                }
            }
return"NoPDFPlugin";
        }

在 IE 瀏覽器需借助 Adobe Reader 或 Foxit 外掛,走的仍是 ActiveX 的老路;至於其他瀏覽器,則透過 naviagtor.mimeType["application/pdf"] 取得 PDF 對應的套件(在 Chrome 預設為內建 Chrome PDF Viewer),接著透過 PDF 外掛 API,便能直接列印 PDF 檔。

又偷學到一招~

Viewing all 2311 articles
Browse latest View live