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

試駕體驗-小工具程式 .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 的開發經驗,依直覺行事,遇到問題就爬文求解,就是傳說中的 GDD(Google Driven Development)/ SDD(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 模式中嚴重傷害效能

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

  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 檔。

又偷學到一招~

實例分析-彈出式視窗被瀏覽器封鎖

$
0
0

昨天剛在公司解完案例,今天又在日常生活遇到實例,老天爺這暗示明顯無比,趕緊來篇筆記,以防出門雷劈!

最近迷上蝦皮拍賣。能跟 LINE 一樣跟賣家溝通超方便,尤其問完問題馬上接到賣家的實體照片真令人感動,不用出門與人面對面又保有臨櫃交談的即時性,真是阿宅的救星。遇到一個賣家很妙,凌晨四點多發訊息通知我補寄商品今天會到(不知對方有沒有早起的拎杯秒回「謝謝」嚇到? XD)。

在中華郵政網站輸入包裹號碼,按下「運輸資料」查詢鈕… 登楞!

JavaScript 試圖彈出視窗被 Chrome 瀏覽器封鎖了!

簡單來說,這是踩中「瀏覽器會封鎖不是使用者點擊直接觸發的 window.open」地雷。(詳細說明可參考: showModalDialog與IE快顯封鎖

無聊追進郵局包裹查詢程式幫忙 Debug 順便練功。網頁是用 Angular 寫的:

<a class="css_btn_class_gray ng-scope" ng-click="showInfo('Base64編碼');">運輸資料</a>

按鈕時會觸發 API 查詢

$scope.showInfo = function(b64Str){ $scope.queryAPI(b64Str); };

$scope.queryAPI 呼叫 AJAX 查詢,接收結果後呼叫 setTimeout(function() { $scope.createTRSView(); }, 500); 開啟結果視窗:

    $scope.queryAPI = function(b64Str){
        $scope.sendRecv("EB500100", "queryAPI", API_Url, vo,
function(tota, isError) {
//查詢處理(省略)
//延遲0.5秒
                setTimeout(function() {
                    $scope.createTRSView();
                }, 500);
            }
        });
    };
    $scope.createTRSView = function(){
var template = "<html><head><title>郵袋籃車查詢</title>…略…";
var mw = window.open("", "_blank");
        $scope.mw = mw;
if(mw) {
            mw.document.write(template.format($("#trs_template").html()));
        } else {
            alert("無法開啟查詢視窗");
        }
    };

由以上邏輯可發現,window.open 與 onclick 事件間隔了兩層非同步,第一層是呼叫 API 回傳結果(背後使用的是 $http Promise 機制),之後透過 setTimeout 又是另一層非同步,window.open 動作怎麼都不可能算成 onclick 的直接觸發行為,註定要被瀏覽器攔截!

遇到這種情境要怎麼處理呢?我想到幾種做法:

  1. 改為直接操作 XmlHttpRequest 以同步方式呼叫(網頁卡住等待伺服器傳回結果再繼續執行),收到結果後直接 window.open,避開 XHR 非同步執行、$http Promise 與 setTimeout 三層非同步,以符合 window.open 被包在 onclick 事件中的條件。 不過,這做法與 AJAX 精神背道而馳,我歸類為餿主意。
  2. 在 onclick 事件就 window.open 將結果視窗先開好(得先顯示「查詢中…」之類的動畫,不然會很乾),待 AJAX 呼叫完成後再將結果填入,但無法視 AJAX 呼叫結果決定要不要開視窗是一大缺點。
  3. 避用 window.open(),改以 IFrame 內嵌或直接在 DOM 建立 div 放入結果。

我個人偏好第 3 種做法,練功完畢。


初試 Bash on Ubuntu on Windows 10

$
0
0

心血來潮,想試試 Windows 10 的新玩意兒 – Bash on Ubuntu on Windows,依我個人看法,Bash on Windows 最重要的意義不是用 Linux Shell 換掉 DOS Shell,而在於用 Windows 10 直接跑 Ubuntu 原生程式,就像 iOS 可以跑 Android App 一樣,是令人雀躍的一大突破!

安裝 Bash on Windows 10 前要先確認 Windows 10 組建版本大於 14393,且必須為 64 位元版本。(這年頭應該沒人裝 32 位元了吧?)

由於 Bash on Windows 10 仍在 Beta 階段,使用前要進「設定/開發人員專用/使用開發人員功能」切換成「開發人員模式」:

到 Windows 功能安裝介面選取「適用於 Linux 的 Windows 子系統(搶鮮版(Beta))」

安裝完成後,開啟 DOS 視窗執行 bash 即開始下載及安裝。過程需設定 UNIX 使用者名稱與密碼,安裝完畢就直接進入 Bash Sell 環境:

平時要開啟 Bash Shell,有兩種做法:使用捷徑 Windows 上 Ubuntu 的 Bash:(若有需要可以釘選在開始畫面或工具列)

或是從 Windows 開始或 DOS 執行 bash:

簡單整理我的初步試用心得:

  1. 跟 Ubuntu 一樣,軟體或程式庫用 apt-get 就能安裝或更新,能在 Windows 直接跑 Ubuntu 原生程式,感覺超讚!
  2. 磁碟 C、D 在 Bash 被對映成 /mnt/c、/mnt/d,要注意 Bash 區分大小寫,輸入路徑時要改一下習慣。
  3. 可以顯示中文但無法輸入,中文顯示也有點問題,會吃字。(字串有幾個中文字,尾端就少幾個字元)
    以下圖為例,檔名「八個中文長度少八987654321」有 8 個中文,會顯示為「八個中文長度少八9」,最後 8 個字元消失。

    爬文找到一些關於 Bash on Windows 亞洲語系支援問題的討論,看來要等未來版本修正。

【茶包射手筆記】NUnit 發生 OutOfMemoryException

$
0
0

從 Github 取得 ServiceStack.Text想幫忙修 Bug。專案使用 NUnit 跑單元測試,為方便測試,在 Visual Studio 2017 安裝 NUnit 3 Test Adapter,安裝後可由 Test Explorer 直接執行測試。

不料,編譯後 Test Explorer 只找到一項測試,Output / Tests 則出現大量 OutOfMemoryException:

Exception System.OutOfMemoryException, Exception converting ServiceStack.Text.Tests.XmlSerializerTests.serialize_Territory
已發生類型 'System.OutOfMemoryException' 的例外狀況。

爬文找到解法,原因出在 NUnit 還不支援 .NET Core 使用的新版 Portable PDB 格式(未來的新標準),而 ServiceStack.Text.Tests 是一個 .NET Core 專案(同時支援 net45 與 netcoreapp1.0),.NET Core 版本的 PDB 導致問題。解決方法是在 csproj 中加入以下 PropertyGroup,指定當 Target 不等於 netcoreapp1.0 時 DebugType = Full 以產生 Windows 版 PDB:(預設 DebugType 為 Portable,Full 應源自 Full Framework,不是指完整版 PDB,參考

  <PropertyGroup>
    <DebugType Condition="'$(TargetFramework)' != '' AND '$(TargetFramework)' != 'netcoreapp1.0'">Full</DebugType>
  </PropertyGroup>

加入設定後,Test Explorer 順利找到測試項目並成功跑完,綠燈!

筆記-使用 Dns.GetHostEntry 解析 IP 位址

$
0
0

某排程使用以下程式碼產生 IEndPoint 以建立 Socket:

IPEndPoint pEndPoint = new IPEndPoint(Dns.GetHostEntry(remoteHost).AddressList[0], remotePort);

其中用了 Dns.GetHostEntry(),好處是不管 remoteHost 傳入的是主機名稱還是 IP,一律可轉成 IPAddress。

排程在正式及測試環境運作多時,今天將程式移到另一網段機器上執行,remoteHost 為 IP 位址(假設為 192.168.1.1),與原本設定相同,確認新主機與 192.168.1.1 間網路暢通,甚至用 telnet 192.168.1.1 portNo 建立連線也成功,但程式一執行就出現以下錯誤:

[SocketException (0x2af9): No such host is known]
   System.Net.Dns.InternalGetHostByAddress(IPAddress address, Boolean includeIPv6) +2221072
   System.Net.Dns.GetHostEntry(String hostNameOrAddress) +6671028

認真看了 MSDN 文件,搞懂 GetHostEntry() 邏輯才恍然大悟。

GetHostEntry 的介面為:

public static IPHostEntry GetHostEntry(
    string hostNameOrAddress
)

其中 hostNameOrAddress 參數可以是主機名稱也可以是 IP 位址。當傳入 IP 時 GetHostEntry 假設程式想取得 IPHostEntry 的完整資訊-包含 AddressList, Aliases, 與 HostName,因此將執行以下動作:

  1. 嘗試解析 IP 位址,若 hostNameOrAddress 傳入的是有效 IP 位址字串,轉成 IP 位址物件不是問題
  2. 利用 IP 反查取得主機名稱存入 HostName
  3. 利用主機名稱查詢該主機的所有 IP 位址,存入 AddressList

今天出錯的關鍵在於程式原本所在的正式與測試主機與 192.168.1.1 隸屬同一網域,而問題主機則屬於另一個網域,兩個網域雖有信任關係,但 WINS 及 DNS 反查未打通,故在進行第 2 步以 IP 反查 HostName 時踏到鐵板,產生 No such host is known 錯誤。

知道原因就好辦,有幾種解決方法:

  1. 設定 system32/drivers/etc/hosts 讓 192.168.1.1 能反查到主機名稱
  2. 改用 FQDN,GetHostEntry() 改成用 FQDN 解析 IP(DNS 解析的功能是好的)
  3. 修改程式,當 remoteHost 為 IP 位址時不走 GetHostEntry(),改用 IPAddress.Parse()

最後採行方案 2,問題排除。

VS2017 Git SSL 憑證無效問題

$
0
0

少數人會遇到的冷門問題,使用 Visual Studio 連上 Github 與私有 Git 伺服器時發生 SSL 憑證錯誤:

Git failed with a fatal error. unable to access '…': SSL certificate problem: unable to get local issuer certificate

可能原因有二:

  1. 網站連線時 SSL 憑證遭網管設備置換,Windows 已設定信任網管設備的 CA 根憑證,但 Git 因屬不同體系,拒絕承認置換憑證有效性
  2. 私有 Git 伺服器使用自己簽發的 SSL 憑證,其根憑證未被信任

針對這類情況,解決方法也有二種,第一種是停用憑證檢查(省事但不安全)、第二種則是讓 VS2007 Git 信任該憑證。

方法1 編輯 c:\Users\你的帳號\.gitconfig,加入

[http]
    sslVerify = false

如此 Git 工具將一律忽略憑證無效的問題,風險是萬一網路被惡意人士攔截竊聽,你也不會發現。

方法2 指定 Git 信任特定憑證

先將要信任的 CA 憑證匯出成 CER,格式請選「Base-64 編碼 X.509」

匯出的 CER 檔是個文字檔,格式為 -----BEGIN CERTIFICATE----- 與 -----END CERTIFICATE----- 間夾著一段 Base64 編碼碼。

找到 C:\Program Files\Git\usr\ssl\certs\ca-bundle.crt,將它複製到 c:\Users\你的帳號 目錄下,將 CER 裡的文字加在最後面:

最後,修改 c:\Users\你的帳號\.gitconfig,加上 sslCAInfo 指向我們修改過的 ca-bundle.crt。

大功告成!

【延伸閱讀】

關閉 VS2017 Chrome 偵錯整合

$
0
0

使用 Visual Studio 2017 偵錯網頁,馬上發現不同:VS2017 增加了對 Chrome 的整合度。當選擇使用 Chrome 檢視網站,按下 F5 偵錯網站,VS2017 將另起一個獨立 Chrome 程序(過去會在既有 Chrome 程序開啟新分頁),歷經短暫的等待(畫面如下),VS2017 會經由 URL 注入程式,透過 Chrome DevTools Protocol與 F12 開發工具整合,允許在 VS2017 IDE 設定 JavaScript/TypeScript 中斷點、觀察變數、執行即時指令以及逐行偵錯;當 VS2017 停止偵錯,Chrome 也將自動關閉,整合度比照 IE。

不過,新功能跟我不投緣。一來是對 Chrome F12 開發者工具己經上手,其 JavaScript/CSS/AJAX 偵察除錯功能也比 VS 強大完整;二則是啟動整合偵錯後,每次啟動時間會拖長,累積的等待時間嚴重消耗中年程序員所剩不多的寶貴青春,好令人心焦… 最後,偵察 JavaScript 問題我習慣保留網頁,改完程式存檔或編譯後重新整理網頁,馬上就可接著測試,比起「結束偵錯關閉瀏覽器,下回再重新啟動」有效率。VS2017 的新功能導致 Chrome 反覆關閉重啟,有時還造成如上圖的「Chrome 未正確關閉」問題,有些惱人。

總之,試用一陣子後,決定把它關掉。

找到 Options / Debugging / General / Enable JavaScript debugging for ASP.NET (Chrome and IE),取消勾選,偵錯行為就回到以前的做法囉~

【延伸閱讀】

Viewing all 428 articles
Browse latest View live