How to resolve the issue of Quasar V2 project lacking Intellisense in VS Code?

Install the following VS Code extensions.

Open the Quasar project and create a new file named quasar.d.ts in the src directory.

If you are working with Quasar CLI with Vite, add the following content into the quasar.d.ts file.

/// <reference types="@quasar/app-vite" />

If you are working with Quasar CLI with Webpack, add the following content into the quasar.d.ts file.

/// <reference types="@quasar/app-webpack" />

Restart VS Code and the Intellisense should work fine.

.NET Framework 組件版本衝突的問題與解決方法

相依性組件版本衝突

在 .NET Framework 裡,我們將程式碼編譯建置後的結果稱為「組件」,是共同運作及構成一個功能邏輯單位的型別與資源集合。組件可以是「可執行檔(.exe)」或是「動態連結程式庫(.dll)」的型式。

現代化的 .NET 應用程式通常依賴於一至多個 NuGet 函式庫套件,透過安裝並使用這些套件,節省了大量重複造輪子的時間,提高了開發人員的生產力。

另外,在開發規模較大的應用程式時,我們通常會將應用程式切割為多個專案來進行,並將專案間共用的程式邏輯抽取出來成為另一個類別庫,來避免程式邏輯的重複。

那麼,問題來了,如果我們的應用程式和共用類別庫相依於同一個 NuGet 函式庫套件的不同版本時,會發生什麼事呢?來看看以下一個簡化過的例子:

相依性組件版本衝突

我們可以看到,應用程式加入了共用類別庫的參考,並相依於「Newtonsoft.Json 版本 11」,而共用類別庫則相依於「Newtonsoft.Json 版本 12」,這造成了相依性組件的版本衝突,讓我們的應用程式無法正確地載入 Newtonsoft.Json 組件。

問題重現

以 ASP.NET MVC 5 應用程式為例,打開 Visual Studio,新增一個名稱為「VersionConflicts」的 ASP.NET MVC 5 專案,作為我們的應用程式。

從選單裡找到「工具 > NuGet 套件管理員 > 套件管理器主控台」打開「套件管理器主控台」,輸入指令「Install-Package Newtonsoft.Json -Version 11.0.2 -Project VersionConflicts」,用來在我們的應用程式安裝及建立對 Newtonsoft.Json 版本 11 組件的相依性。

在專案的 Controllers 目錄下新增一個控制器「HomeController.cs」,內容如下:

using System.Web.Mvc;
namespace VersionConflicts.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
Response.Write(Newtonsoft.Json.JsonConvert.SerializeObject(new
{
msg = "序列化自「應用程式」"
}));
return Content("");
}
}
}

在方案中新增一個類別庫專案,,命名為「SharedLibrary」,用來模擬我們的共用類別庫。

再次打開「套件管理器主控台」,這次要在我們的共用類別庫安裝及建立對 Newtonsoft.Json 版本 12 組件的相依性,不同於應用程式相依於版本 11,這將造成相依性組件的版本衝突。在套件管理器主控台中輸入指令「Install-Package Newtonsoft.Json -Version 12.0.3 -Project SharedLibrary」。

將共用類別庫中的 Class1.cs 取代為以下內容:

namespace SharedLibrary
{
public class Class1
{
public static string SerializeObject()
{
return Newtonsoft.Json.JsonConvert.SerializeObject(new
{
msg = "序列化自「共用類別庫」"
});
}
}
}

回到應用程式專案 VersionConflicts,加入對共用類別庫 SharedLibrary 專案的參考。

將 HomeController.cs 中的 Index() 動作方法修改如下:

public ActionResult Index()
{
Response.Write(SharedLibrary.Class1.SerializeObject());
Response.Write("<br>");
Response.Write(Newtonsoft.Json.JsonConvert.SerializeObject(new
{
msg = "序列化自「應用程式」"
}));
return Content("");
}

確定 VersionConflicts 為啟設專案後,按下「Ctrl + F5」啟動但不偵錯應用程式。由於我們的應用程式有相依性組件的版本衝突,以致於應用程式無法正確地載入相依性組件。

我們也能在 Visual Studio 的錯誤清單視窗中,看到相依性組件版本衝突的警告。

可能的解決方法

1. 使相依性組件的版本一致

解決組件版本衝突的基本策略,就是消除組件版本的不一致,如果能夠修改相依性,讓衝突的組件都採用同一個版本,那麼這是最簡單的解決方法。例如在我們的例子中,「應用程式」以及「共用類別庫」對 Newtonsoft.Json 組件的相依性都是我們能控制的,我們可以對這兩個專案進行 NuGet 套件的更新,讓它們都參考至最新的,或一致的版本組件,例如 Newtonsoft.Json 版本 12。

打開「套件管理器主控台」,輸入以下的指令可以更新方案中所有專案的 Newtonsoft.Json 套件至最新版本:

Update-Package Newtonsoft.Json

更新方案中所有專案的相依性套件版本後,應「重建方案」(選單:建置 > 重建方案),以刪除所有已編譯的組件並重新建置方案下的所有專案。

2. 重新導向相依性組件版本

如果沒辦法從專案來修改相依性,例如共用類別庫是已編譯過的DLL,那麼我們可以強制要求 .NET Framework,當執行應用程式發生組件版本衝突時,要載入的組件版本,這樣的動作稱為「重新導向組件版本」。以我們的例子來說,在應用程式 VersionConflicts 專案,編輯根目錄下的「Web.config」檔案,在 /configuration/runtime 區段,加上 assemblyBinding 組態設定,編輯後的組態類似於底下的樣子:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!– 省略… –>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="11.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<!– 省略… –>
</configuration>

如果應用程式有多個相依性組件版本衝突,必須在 assemblyBinding 組態下為每一個衝突的相依性組件增加一個 dependentAssembly 節點。

我們來詳細看看 dependentAssembly 下的 assemblyIdentity 元素,屬性 name 為相依性組件的名稱,也是 NuGet 套件的 Id,屬性 publicKeyToken 為組件強式名稱的雜湊值,culture 屬性則是組件的文化特性。

<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />

這些組件相關的資訊都能使用 PowerShell 取得,在 PowerShell 下執行以下 Script (記得修改 dll 檔的路徑):

[System.Reflection.Assembly]::LoadFile(“C:\your\dll\path\Newtonsoft.Json.dll").FullName

執行後 PowerShell 將印出組件的顯示名稱,包含組件的名稱、版本、文化特性以及組件強式名稱的雜湊值,範例如下:

Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed

再來看看 bindingRedirect 元素,oldVersion 為要載入的組件版本範圍,newVersion 則是要重新導向的組件版本。

<bindingRedirect oldVersion="0.0.0.0-12.0.0.0″ newVersion="11.0.0.0″ />

我們所加入這整個 dependentAssembly 節點的意思為,當應用程式要載入 Newtonsoft.Json 組件時,不論版本為 oldVersion 範圍裡的那一個版本(0.0.0.0-12.0.0.0),都載入 newVersion 所指定的版本(11.0.0.0)。

3. 同時載入不同版本的相依性組件

首先我們必須認識到,讓應用程式同時載入不同版本的相同組件並不是一個好的作法,因為相同組件的不同版本間僅有部分的差異,我們將載入其他相同部分重複的程式邏輯。另外,組件的設計者在設計組計時,多數不會料想到組件會被重複載入,這將造成許多不可預期的例外。

如果已經充分了解風險,我們還是可以強制 .NET Framework 執行應用程式時,同時載入不同版本的相同組件。方法包括了:

1. 處理 AssemblyResolve 事件,動態載入不同版本組件 (不需要是強式名稱簽署組件)
2. 在 Web.config 裡,加上 assemblyBinding 組態,並在 dependentAssembly 節點使用 <codeBase> 元素指定組件版本 (必須是強式名稱簽署組件)
3. 將不同版本的相同組件安裝到全域組件快取 (GAC)

3.1. 處理 AssemblyResolve 事件,動態載入不同版本組件 (不需要是強式名稱簽署組件)

首先將範例中「應用程式」(VersionConflicts 專案),以及「共用類別庫」(SharedLibrary 專案),兩者對 Newtonsoft.Json 的參考,屬性「複製到本機」都設為 False。

以 SharedLibrary 專案為例,先在方案總管中,展開專案的參考清單,選取 Newtonsoft.Json 項目,按下 F4 編輯參考屬性,將屬性「複製到本機」設為 False。

編輯參考屬性,將屬性「複製到本機」設為 False。

接著,手動將不同版本的 Newtonsoft.Json.dll 複製到應用程式(VersionConflicts 專案) bin 目錄下的不同版本名稱資料夾裡面,目錄結構如下圖:

回到 Visual Studio,打開方案總管,在應用程式 VersionConflicts 專案中,將 bin 目錄下手動加入的資料夾,如V11、V12,開啟右鍵選單「加入至專案」,這樣在 Web 應用程式發佈時,才會將這些手動加入的資料夾一併複製到發佈目錄。

將 bin 目錄下手動加入的資料夾「加入至專案」

在 VersionConflicts 專案下,打開 Global.asax.cs,在 Application_Start() 方法中,註冊 AssemblyResolve 事件處理函式,以動態載入不同版本組件。範例如下:

using System;
using System.IO;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace VersionConflicts
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) =>
{
string assemblyInfo = resolveArgs.Name; // e.g "Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed"
var parts = assemblyInfo.Split(',');
string name = parts[0];
var version = Version.Parse(parts[1].Split('=')[1]);
string fullName;
if (name == "Newtonsoft.Json" && version.Major == 11)
{
// 如果參考的組件為版本11,則載入V11資料夾下的組件
fullName = Path.Combine(HttpRuntime.AppDomainAppPath, @"bin\V11\Newtonsoft.Json.dll");
}
else if (name == "Newtonsoft.Json" && version.Major == 12)
{
// 如果參考的組件為版本12,則載入V12資料夾下的組件
fullName = Path.Combine(HttpRuntime.AppDomainAppPath, @"bin\V12\Newtonsoft.Json.dll");
}
else
{
return null;
}
return Assembly.LoadFile(fullName);
};
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
}
}

如果有多個組件版本衝突,我們必須在 AssemblyResolve 事件函式中增加判斷式分別處理。

3.2. 在 Web.config 裡,加上 assemblyBinding 組態,並在 dependentAssembly 節點使用 <codeBase> 元素指定組件版本 (必須是強式名稱簽署組件)

如果版本衝突的組件是強式名稱簽署組件,那麼我們可以不必撰寫程式碼處理 AssemblyResolve 事件,可以用更簡單的組態設定方式,來同時載入相同組件的不同版本。請先參考上一小節,將組件參考屬性「複製到本機」都設為 False、手動複製組件到 bin 目錄下的不同版本資料夾,以及將手動複製的組件資料夾「加入至專案」。

接著只要在 Web.config 裡,加上 assemblyBinding 組態,並在 dependentAssembly 節點使用 <codeBase> 元素指定組件版本即可,修改後的 Web.config 類似於以下的樣子:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!– 省略… –>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
<codeBase version="11.0.0.0" href="bin/V11/Newtonsoft.Json.dll"/>
<codeBase version="12.0.0.0" href="bin/V12/Newtonsoft.Json.dll"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
<!– 省略… –>
</configuration>

相同地,如果有多個組件版本衝突,必須在 assemblyBinding 組態下為每一個衝突的相依性組件增加一個 dependentAssembly 節點。

3.3. 將不同版本的相同組件安裝到全域組件快取 (GAC)

將不同版本的相同組件安裝到全域組件快取 (GAC),.Net Framework 在執行應用程式時,就會依據組件參考的不同版本,同時載入對應版本的組件。我們可以利用 gacutil.exe 安裝組件至 GAC,首先在 Visual Studio 中按下「Ctrl + `」打開「開發人員提示字元」(或選單:檢視 > 終端機),執行以下指令將 SharedLibrary 專案所參考的 Newtonsoft.Json 版本 12,以及 VersionConflicts 專案所參考的 Newtonsoft.Json 版本 11,都安裝到 GAC 即可。

gacutil -i SharedLibrary\bin\Debug\Newtonsoft.Json.dll

gacutil -i VersionConflicts\bin\Newtonsoft.Json.dll

將不同版本的相同組件安裝到全域組件快取 (GAC)

將組件安裝至 GAC 是一個簡單解決組件版本衝突的好方法,條件是組件必須是強式名稱簽署組件,另外較不適合 WebFarm 負載平衡式伺服器陣列的架構,因為必須在伺服器陣列下的每一台主機安裝組件。

使用以上可能的解決方法解決相依性組件版本衝突後,在 Visual Studio 中確後目前啟始專案為 VersionConflicts,按下「Ctrl + F5」啟動但不偵錯應用程式,可以看到即使相依性組件參考至不同版本,應用程式仍能正確執行。

總結

現代化的 .NET 應用程式通常依賴於一至多個 NuGet 函式庫套件,而隨著應用程式規模的增加,我們通常會將應用程式切割為多個專案來進行,專案間時常會發生相依性組件的版本衝突,本篇文章介紹了相依性組件版本衝突的原因,以及可能的解決方法。

參考資料

https://blog.alantsai.net/posts/2017/10/donet-dll-hell-problem-how-to-use-bindingredirect
https://blog.darkthread.net/blog/assemblyinformationversion
https://michaelscodingspot.com/dotnet-dll-hell/

ASP.NET MVC 5 如何從 Razor 檢視產生 HTML 字串

ASP.NET MVC 框架將應用程式切割為 Model (模型)、View (檢視)以及 Controller (控制器)三個部分,Model 主要用來作資料的存取和業務邏輯的實作,View 的功能為使用者介面的呈現,Controller 則是用來處理 HTTP 的請求及回應。MVC 的核心精神為「關注點分離」(Separation of Concerns),這樣的架構能幫助我們降低程式碼的耦合,讓我們較不會寫出業務邏輯、使用者介面混在一起的義大利麵程式碼(Spaghetti code)。

ASP.NET MVC 使用 Razor 視圖引擎搭配檢視樣板檔案(.cshtml)來動態產生 HTML 網頁回應,Razor 視圖引擎的功能非常強大,可以在樣板中穿插靜態的 HTML 標籤或文字,以及以 Razor 語法、C# 程式語言,所撰寫的使用者介面相關程式邏輯,利如使用「@if」來作條件式渲染,或是使用「@for」、「@foreach」進行迴圈式渲染…等。

Razor 視圖引擎已經被完全地整合在 ASP.NET MVC 框架之中,一般情況下,我們都是在 Controller 的動作方法中,直接呼叫 View() 輔助方法建立並回傳 ViewResult 實體,就能以 Razor 視圖引擎處理樣板,並動態回應 HTML 結果。在這個過程中,我們並不能夠直接存取到 Razor 視圖引擎所動態組成的 HTML 字串。

但有時候我們會希望能夠取得動態組成的 HTML 字串,來作為其他用途,舉例來說,以 HTML 格式寄送 E-mail 時的信件內文。這個情況下,如果沒有樣板引擎的幫助,我們就必須以串接字串的方法,來組成 HTML。

以下的工具函數 RenderViewToString() 能讓我們傳入 ControllerContext 物件、檢視名稱,以及檢視的資料模型物件後,取得 Razor 視圖引擎處理樣板後的 HTML 字串:

using System.IO;
using System.Web.Mvc;
namespace JZLib
{
public static class JZUtil
{
public static string RenderViewToString(ControllerContext controllerContext, string viewName, object viewData = null)
{
using (var writer = new StringWriter())
{
var razorViewEngine = new RazorViewEngine();
var razorViewResult = razorViewEngine.FindView(controllerContext, viewName, "", false);
var viewContext = new ViewContext(controllerContext, razorViewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer);
razorViewResult.View.Render(viewContext, writer);
return writer.ToString();
}
}
}
}
view raw JZUtil.cs hosted with ❤ by GitHub

讓我們來看一個簡單的例子,首先,在專案的 Models/ 目錄下,新增名稱為「Order.cs」的 C# 程式碼檔案,其中包含自訂的訂單類別「Order」、訂單明細類別「OrderDetail」,以及用來取得測試訂單資料的工具類別「OderUtil」,檔案內容如下,OrderUtil 的 GetTestOrder() 方法能夠取得測試用的訂單資料:

using System.Collections.Generic;
namespace Models
{
public class Order
{
public int order_id { get; set; }
public string customer_email { get; set; }
public IEnumerable<OrderDetail> details { get; set; }
}
public class OrderDetail
{
public string product_name { get; set; }
public int quantity { get; set; }
}
public static class OrderUtil
{
public static Order GetTestOrder()
{
return new Order
{
order_id = 1,
customer_email = "jason@gms.ndhu.edu.tw",
details = new List<OrderDetail> {
new OrderDetail{ product_name = "basketball", quantity = 100 },
new OrderDetail { product_name = "soccer", quantity = 50 }
}
};
}
}
}
view raw Order.cs hosted with ❤ by GitHub

接著在控制器中撰寫 SendOrderEmail() 動作方法,用來發送包含訂單資料的 E-mail 給訂購顧客,在沒有 Razor 引擎的協助下,必須以字串串接的方式,來動態組成信件內文的 HTML:

[HttpPost]
public ActionResult SendOrderEmail()
{
var order = Models.OrderUtil.GetTestOrder();
string subject = "測試主旨";
string body = $@"訂單編號:{order.order_id}<br>
顧客E-mail:{order.customer_email}<br>
訂單明細:";
body += @"<table border=""1"" cellpadding=""5"">
<tr>
<th>產品</th>
<th>數量</th>
</tr>";
foreach (var detail in order.details)
{
body += $@"<tr>
<td>{detail.product_name}</td>
<td>{detail.quantity}</td>
</tr>";
}
body += "</table>";
using (SmtpClient smtp = new SmtpClient("your.smtp.server"))
{
smtp.Send(new MailMessage("noreply@jzcorp.com", order.customer_email)
{
Subject = subject,
Body = body,
IsBodyHtml = true
});
}
return View("MailSent");
}

可以看得出來,為了結合訂單資料和 HTML 標籤,字串串接的方式讓程式碼略顯複雜,在串接的過程中也很容易出錯,而造成最終產出的 HTML 無法被正確解析。

我們可以改用 RenderViewToString() 工具函數來取得 HTML 字串,首先,在專案的 Views/Shared/ 目錄下,新增一個樣板檔案「_OrderEmailTemplate.cshtml」,並以 @model 指示詞指定檢視的資料模型為自訂的訂單類別「Order」,檔案內容如下:

@model Models.Order
@{
Layout = null;
}
訂單編號:@Model.order_id<br>
顧客E-mail:@Model.customer_email<br>
訂單明細<br>
<table border="1" cellpadding="5">
<tr>
<th>產品</th>
<th>數量</th>
</tr>
@foreach(var detail in Model.details)
{
<tr>
<td>@detail.product_name</td>
<td>@detail.quantity</td>
</tr>
}
</table>
@ViewContext.Controller.ViewBag.remark

Razor 檢視樣板可以輕鬆地結合動態資料以及靜態的 HTML 標籤及文字,除了強型別的資料模型外,也能使用並輸出 ViewBag 的資料,例如第 23 行的「@ViewContext.Controller.ViewBag.remark」,不過特別要注意的是,這裡不能使用一般的「@ViewBag.remark」,否則會取不到 ViewBag 裡的資料。

接著,將控制器中用來發送訂單 E-mail 的動作方法 SendOrderEmail() 修改如下即可,一樣需要注意的是,如果樣板中有用到 ViewBag 的話,必須在呼叫 RenderViewToString() 工具函數前指派 ViewBag 的值(第 8 行處):

[HttpPost]
public ActionResult SendOrderEmail()
{
var order = Models.OrderUtil.GetTestOrder();
string subject = "測試主旨";
ViewBag.remark = "寄送時間:" + DateTime.Now;
string body = JZLib.JZUtil.RenderViewToString(ControllerContext, "_OrderEmailTemplate", order);
using (SmtpClient smtp = new SmtpClient("your.smtp.server"))
{
smtp.Send(new MailMessage("noreply@jzcorp.com", order.customer_email)
{
Subject = subject,
Body = body,
IsBodyHtml = true
});
}
return View("MailSent");
}

使用 Razor 引擎搭配樣板檔案讓動態產生 HTML 字串的過程變得容易許多,去除掉字串串接的部分,也適度地提升了程式碼的可讀性,最重要的是,避免了字串處理時的錯誤,讓產出的 HTML 能夠正確地被解析。最後,範例中 HTML 格式 E-mail 的結果示意圖如下:

如果你的 ASP.NET MVC 專案也有結合資料動態產生 HTML,又難以處理字串串接問題的話,不妨試著使用 Razor 引擎搭配樣版檔案,來簡化產生 HTML 字串的過程!

下載 RenderViewToString() 工具函數範例程式碼

HTML表格對角線

對於類似課表這種欄標題和列標題不同的表格,我們通常會在表頭的地方畫上對角線以區分欄、列的標題。

利用表頭對角線區分欄標題「星期」與列標題「節次」。

網頁並沒有標準作法來處理儲存格裡的對角線,一種簡單的作法是,先使用繪圖軟體製作一個對角線的圖片,再利用CSS將製作好的對角線圖片,設定為表頭的背景。

例如,我使用繪圖軟體畫了一張對角線圖片,解析度為150像素 * 150像素,背景色設為「#F3F3F3」,對角線線段顏色設為「#CCCCCC」,結果如下。

將先行製作好的對角線圖片放置於網頁相同目錄下,接著使用CSS定義「th.diagonal」選擇器,包含以下樣式規則,用來將對角線圖片,設為表頭的背景。

th.diagonal {
width: 150px;
height: 50px;
box-sizing: border-box;
background: url(diagonal.jpg) no-repeat center;
background-size: 150px 50px;
}

接著只要在表格的表頭 th 元素上加上「diagonal」樣式類別就可以呈現出表頭的對角線了。雖然這個方法簡單直覺,但缺點是對角線的圖片是預先製作的,如果表格的背景顏色、邊框顏色需要改變的時候,就必須重新使用繪圖軟體編輯圖片,相當地不方便。

更好的解決方法是,直接使用CSS在表頭繪製對角線,這樣我們就可以動態地改變背景及邊框的顏色。繪製的方法是利用linear-gradient()函數來為表頭建立漸層色背景。這個漸層色應包含4個漸層顏色終止點(linear color stop),只要將中間兩個終止點的位置設為非常靠近,就能模擬出線段的呈現。其中,第1、3個終止點用來設定表頭的背景顏色,第2、4個終止點用來設定對角線的線段顏色。

table.diagonal th.top-left {
background: linear-gradient(to top right, /*漸層色的方向,應與要繪製的對角線相交*/
#f3f3f3 49.5%, /*表頭的背景顏色*/
#cccccc 49.5%, #cccccc 50.5%, /*對角線的線段顏色*/
#f3f3f3 50.5%); /*表頭的背景顏色*/
}

關於第二種方法的實作,我用Vue.js寫了一個HTML表格標頭對角線產生器:
https://jzwang-dev.github.io/TableDiagonal/

C# 密碼複雜度檢查

我們有時候會希望使用者所輸入的密碼,能符合一定強度的規則。例如,密碼長度至少有8碼以上、一定要包含英文字母和數字,以及必須混用大小寫字母…等。

一個基本的實現方法是,將檢查的程式碼包裝成一個函數,在其中使用連續多個判斷式,來逐項測試規則是否通過檢查,在逐項測試的過程中,只要一遇到不通過的清況,就直接回傳false,表示密碼複雜度不夠,如果最後能通過所有的測試,則回傳true,表示密碼的複雜度符合規則。

我實作了一個工具函數,來做基本的密碼複雜度檢查:

/// <summary>
/// 檢查密碼強度
/// </summary>
/// <param name="password">密碼</param>
/// <param name="minLength">最小長度</param>
/// <param name="maxLength">最大長度(null則略過檢查)</param>
/// <param name="hasNumber">必須至少有一個數字</param>
/// <param name="hasLetter">必須至少有一個英文字母(不區分大小寫)</param>
/// <param name="hasDiffCaseLetter">必須至少有一個大寫字母及一個小寫字母</param>
/// <param name="hasSpecialChar">必須至少有一個特殊符號</param>
/// <returns>是否通過檢查</returns>
public static bool CheckPasswordComplexity(string password, int minLength = 8,
int? maxLength = null, bool hasNumber = true, bool hasLetter = true, bool hasDiffCaseLetter = false, bool hasSpecialChar = false)
{
return CheckPasswordComplexity(password, out string _, minLength,
maxLength, hasNumber, hasLetter, hasDiffCaseLetter, hasSpecialChar);
}
/// <summary>
/// 檢查密碼強度
/// </summary>
/// <param name="password">密碼</param>
/// <param name="errMsg">錯誤訊息</param>
/// <param name="minLength">最小長度</param>
/// <param name="maxLength">最大長度(null則略過檢查)</param>
/// <param name="hasNumber">必須至少有一個數字</param>
/// <param name="hasLetter">必須至少有一個英文字母(不區分大小寫)</param>
/// <param name="hasDiffCaseLetter">必須至少有一個大寫字母及一個小寫字母</param>
/// <param name="hasSpecialChar">必須至少有一個特殊符號</param>
/// <returns>是否通過檢查</returns>
public static bool CheckPasswordComplexity(string password, out string errMsg, int minLength = 8,
int? maxLength = null, bool hasNumber = true, bool hasLetter = true, bool hasDiffCaseLetter = false, bool hasSpecialChar = false)
{
errMsg = "";
if (string.IsNullOrEmpty(password))
{
errMsg = "密碼不得為空!";
return false;
}
if (password.Length < minLength)
{
errMsg = $"密碼長度至少為「{minLength}」!";
return false;
}
if (maxLength.HasValue && password.Length > maxLength)
{
errMsg = $"密碼長度至多為「{maxLength}」!";
return false;
}
if (hasNumber && !Regex.IsMatch(password, @"\d+"))
{
errMsg = "密碼必須包含數字!";
return false;
}
bool lowerSuccess = Regex.IsMatch(password, @"[a-z]");
bool upperSuccess = Regex.IsMatch(password, @"[A-Z]");
if (hasLetter)
{
if (!(lowerSuccess || upperSuccess))
{
errMsg = "密碼必須包含英文字母!";
return false;
}
}
if (hasDiffCaseLetter)
{
if (!(lowerSuccess && upperSuccess))
{
errMsg = "密碼必須包含大小寫英文字母!";
return false;
}
}
if (hasSpecialChar && !Regex.IsMatch(password, @"[ !""#$%&'()*+,-.\/:;<=>?@\[\\\]^_`{|}~]"))
{
errMsg = "密碼必須包含特殊字元!";
return false;
}
return errMsg == string.Empty;
}

在CheckPasswordComplexity工具函數中,利用連續多個判斷式,搭配正則運算式,逐項測試了以下幾個規則:

  • 最小長度
  • 最大長度
  • 必須至少有一個數字
  • 必須至少有一個英文字母(不區分大小寫)
  • 必須至少有一個大寫字母及一個小寫字母(混用大小寫)
  • 必須至少有一個特殊符號

這個工具函數使用了C#的具名和選擇性引數功能,可省略傳入選擇性引數,是因為函數定義時,提供了參數預設值,進而簡化了函數多載的撰寫。另外,具名引數則可讓我們以任意的順序傳遞引數,而不必依照參數的定義順序,具名引數的使用,也能改善程式碼的可讀性。

呼叫的範例如下:

// 僅傳入maxLength選擇性引數,其餘使用參數預設值
bool pass1 = CheckPasswordComplexity(somePassword, maxLength: 64);
// 使用具名引數以任意順序傳遞引數
bool pass2 = CheckPasswordComplexity(somePassword, hasSpecialChar: true, maxLength: 64);

Online Demo

含有CheckBox控制項的GridView,如何在分頁時紀錄每頁已勾選項目的狀態?

ASP.NET Web Forms框架提供開發人員相當多樣的伺服器控制項,讓我們可以用拖放控制項、所見即所得的方式,快速地建立網頁應用程式。

例如GridView控制項,可以用來呈現表格結構的資訊,而對其設定AllowPaging屬性為true,並建立換頁事件OnPageIndexChanging的處理函式,在每一次切換頁面時,重新對GirdView進行資料繫結,就可以完成簡易的表格分頁功能。

GridView也常與CheckBox搭配使用,可用來選擇要操作的資料列,

GridView常與CheckBox搭配使用,可用來選擇要操作的資料列。

然而,具有分頁功能的GridView,在換頁之後,就會失去換頁前已勾選CheckBox的狀態。我們必須自己將已勾選的項目紀錄下來,才能在GridView切換分頁時,正確呈現CheckBox的勾選狀態。

實現的概念很簡單,當使用者在前端點選資料列中的CheckBox時,將代表資料列的Id,以序列化為CSV格式的方式,儲存在一個隱藏欄位中,之後在每一次PostBack時,ASP.NET就都會把這個表示CheckBox已勾選狀態的隱藏欄位資料送回後端。

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="PagingAndCheckBox.aspx.vb" Inherits="PagingAndCheckBox" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"&gt;
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Grid View Paging + CheckBox</title>
<script>
function checkId(cb) {
var hidden = document.getElementById('checkedIds');
var checked_ids = hidden.value ? hidden.value.split(/\s*,\s*/) : [];
var id = cb.parentElement.attributes['data-id'].value;
var index = checked_ids.indexOf(id);
if (cb.checked) {
if (index == -1) {
checked_ids.push(id);
}
} else {
if (index != -1) {
checked_ids.splice(index, 1);
}
}
hidden.value = checked_ids.join();
}
</script>
</head>
<body>
<h5>測試GridView分頁 + CheckBox</h5>
<form id="form1" runat="server">
<asp:HiddenField ID="checkedIds" runat="server" ClientIDMode="Static" />
<asp:Button ID="btnTest" runat="server" Text="測試" />
<asp:GridView ID="gvTest" runat="server" HeaderStyle-BackColor="LightGray" AlternatingRowStyle-BackColor="LightGray" CellPadding="5"
AutoGenerateColumns="false" AllowPaging="true" PageSize="3">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:CheckBox ID="cbCheck" runat="server" data-id='<%#Eval("Id") %>' onclick="checkId(this)" />
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="Id" HeaderText="Id" />
<asp:BoundField DataField="Name" HeaderText="Name" />
<asp:BoundField DataField="Email" HeaderText="Email" />
</Columns>
</asp:GridView>
</form>
</body>
</html>
aspx網頁標記文件:PagingAndCheckBox.aspx

PagingAndCheckBox.aspx第33行的地方,放置了一個HiddenField隱藏欄位,用來儲存已勾選項目Id的CSV資料,特別要注意的是,因為要使用JavaScript存取這個隱藏欄位,建議將隱藏欄位的ClientIDMode屬性設為「Static」,避免ASP.NET轉譯HTML時,自動串連容器的Id值,而造成JS取不到元素的問題。

在第40行的地方,是GridView中放置CheckBox的位置,我使用了HTML5的data-*屬性功能,自訂了一個data-id屬性,搭配了<%#Eval(“Id") %>的單向資料繫結運算式,將資料列的Id,輸出在CheckBox控制項所轉譯出的HTML中。另外使用行內模式註冊Client端事件的方式,註冊了onclick事件到一個自訂的JavaScript函數「checkId」,並傳入以行內模式註冊事件時,表示CheckBox元素本身的「this」參考。

data-id及onclick屬性,兩者都不是CheckBox伺服器控制項所定義的類別屬性,因此不能使用Visual Studio的自動完成功能,不過並不影響程式的正常運作。

在PagingAndCheckBox.aspx第10行的地方,定義了一個JavaScript的自訂函數checkId,需要傳入一個參數cb,代表被點選的CheckBox元素,並利用這個元素找出代表資料列的Id值,接著將所有已選取的Id值,以序列化為CSV格式的方式,儲存在我們準備好的隱藏欄位中。

而在後端程式碼中,建立GridView的RowDataBound事件處理函式,用來自訂每一個資料列在資料繫結後的動作,在其中,我們將存放在隱藏欄位資料裡的CSV,反序列化為已勾選的Id陣列,接著和資料列的Id做比較,就能決定CheckBox的勾選與否。

Imports System.Data
Partial Class PagingAndCheckBox
Inherits System.Web.UI.Page
Private Function getTestData() As DataTable
Dim names As String() = {"Jason", "Bob", "Mary", "Chris", "Mike"}
Dim tb As New DataTable()
tb.Columns.Add("Id")
tb.Columns.Add("Name")
tb.Columns.Add("Email")
For i As Integer = 0 To names.Length – 1
Dim name As String = names(i)
tb.Rows.Add((i + 1), name, name.ToLower() + "@jzw.com")
Next
Return tb
End Function
Private Sub bindGVTest()
gvTest.DataSource = getTestData()
gvTest.DataBind()
End Sub
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not IsPostBack Then
bindGVTest()
End If
End Sub
Protected Sub gvTest_PageIndexChanging(sender As Object, e As GridViewPageEventArgs) Handles gvTest.PageIndexChanging
gvTest.PageIndex = e.NewPageIndex
bindGVTest()
End Sub
Protected Sub gvTest_RowDataBound(sender As Object, e As GridViewRowEventArgs) Handles gvTest.RowDataBound
If e.Row.RowType = DataControlRowType.DataRow Then
Dim checked_ids As String() = If(checkedIds.Value = String.Empty, New String() {}, Regex.Split(checkedIds.Value, "\s*,\s*"))
Dim id As Integer = CInt(e.Row.DataItem("Id"))
Dim cb As CheckBox = TryCast(e.Row.FindControl("cbCheck"), CheckBox)
cb.Checked = checked_ids.Contains(id.ToString())
End If
End Sub
Protected Sub btnTest_Click(sender As Object, e As EventArgs) Handles btnTest.Click
Dim checked_ids As String() = If(checkedIds.Value = String.Empty, New String() {}, Regex.Split(checkedIds.Value, "\s*,\s*"))
If checked_ids.Length > 0 Then
Response.Write("選取的項目Id為:" + String.Join(", ", checked_ids))
Else
Response.Write("沒有選取任何項目!")
End If
End Sub
End Class
aspx後置程式碼:PagingAndCheckBox.aspx.vb

利用隱藏欄位紀錄已勾選的CheckBox項目,就能在GridView切換分頁時,正確呈現CheckBox的勾選狀態。

GridView於切換分頁時紀錄CheckBox的勾選狀態

範例下載

ASP.NET 下載檔案含中文檔名的Header處理

使用ASP.NET下載檔案時,如果檔案名稱包含中文,在瀏覽器下載、儲存檔案時,就無法正確的顯示檔名。

檔案名稱為「測試中文.txt」,含有中文的檔名無法正確顯示。

我參考了ASP.NET MVC 5專案中的FileResult.cs原始碼,發現一個ContentDispositionUtil工具類別,用來取得下載檔案時,檔案名稱含有Unicode字元如中文時的Content-Disposition Header。原始碼中的註解,也說明了這個Header處理Unicode字元的標準出自RFC 2231。

我將原始碼中,處理Unicode字元Header的部分抽取出來,略加修改為以下的HeaderUtil工具類別。

// 下載檔案包含中文檔名Header處理
// Wang, Jian-Zhong
using System.Text;
namespace JZLib
{
public static class HeaderUtil
{
private const string HexDigits = "0123456789ABCDEF";
private static void AddByteToStringBuilder(byte b, StringBuilder builder)
{
builder.Append('%');
int i = b;
AddHexDigitToStringBuilder(i >> 4, builder);
AddHexDigitToStringBuilder(i % 16, builder);
}
private static void AddHexDigitToStringBuilder(int digit, StringBuilder builder)
{
builder.Append(HexDigits[digit]);
}
private static string CreateRfc2231FileName(string filename)
{
StringBuilder builder = new StringBuilder("");
byte[] filenameBytes = Encoding.UTF8.GetBytes(filename);
foreach (byte b in filenameBytes)
{
if (IsByteValidHeaderValueCharacter(b))
{
builder.Append((char)b);
}
else
{
AddByteToStringBuilder(b, builder);
}
}
return builder.ToString();
}
/// <summary>
/// 取得下載檔案名稱含Unicode字元如中文的Content-Disposition Header值
/// </summary>
/// <param name="filename">含Unicdoe字元的檔案名稱</param>
/// <param name="inline">true表示以inline的形式呈現於頁面中以瀏覽器預覽,false表示在Header中指定attachment強制瀏覽器下載</param>
/// <returns>下載檔案名稱含Unicode字元如中文的Content-Disposition Header值</returns>
/// <example>
/// Response.AppendHeader("Content-Disposition", JZLib.HeaderUtil.GetUnicodeContentDisposition("測試中文.txt", false));
/// </example>
public static string GetUnicodeContentDisposition(string filename, bool inline = true)
{
return $"{(inline ? "inline" : "attachment")}; filename*=UTF-8''{CreateRfc2231FileName(filename)}";
}
// Application of RFC 2231 Encoding to Hypertext Transfer Protocol (HTTP) Header Fields, sec. 3.2
// http://greenbytes.de/tech/webdav/draft-reschke-rfc2231-in-http-latest.html
private static bool IsByteValidHeaderValueCharacter(byte b)
{
if ((byte)'0' <= b && b <= (byte)'9')
{
return true; // is digit
}
if ((byte)'a' <= b && b <= (byte)'z')
{
return true; // lowercase letter
}
if ((byte)'A' <= b && b <= (byte)'Z')
{
return true; // uppercase letter
}
switch (b)
{
case (byte)'-':
case (byte)'.':
case (byte)'_':
case (byte)'~':
case (byte)':':
case (byte)'!':
case (byte)'$':
case (byte)'&':
case (byte)'+':
return true;
}
return false;
}
}
}
view raw HeaderUtil.cs hosted with ❤ by GitHub

Web Forms專案的使用範例

// 下載檔案
protected void btnDownload_Click(object sender, EventArgs e)
{
var fileBytes = Encoding.UTF8.GetBytes("Hello, World!");
Response.ClearContent();
Response.ClearHeaders();
Response.ContentType = "text/plain";
Response.AppendHeader("Content-Disposition",
JZLib.HeaderUtil.GetUnicodeContentDisposition("測試中文.txt", false));
Response.BinaryWrite(fileBytes);
Response.End();
}

而在MVC 5專案中,因為FileResult類別對含有Unicoden字元的檔名已有處理,所以如果是要強制下載檔案的話,在Controller的下載動作中,直接使用File()輔助方法,參數依序傳入檔案的位元組陣列、MIME 類型、檔案名稱,並回傳FileContentResult物件就可以了。

// 下載檔案
public ActionResult DownloadTestFile()
{
var fileBytes = Encoding.UTF8.GetBytes("Hello, World!");
return File(fileBytes, "text/plain", "測試中文.txt");
}
在MVC 5專案中,FileResult類別對含有Unicoden字元的檔名已有處理,使用File()輔助方法就可以正確顯示中文檔名。

如果是需要使用Inline的形式在瀏覽器中直接預覽檔案內容,例如圖片檔案、PDF檔案或文字檔案…等,File()輔助方法也有提供一個多載來實現,函式簽章如下。

protected internal FileContentResult File(byte[] fileContents, string contentType);

不過由於這個多載無法指定檔案名稱,因此在使用者要儲存檔案時,檔案名稱會以動作方法的識別名稱來顯示。

Inline形式的下載檔案,於存檔時會以動作方法的名稱作為檔案名稱。

這時候可以利用我所修改的HeaderUtil工具類別,呼叫GetUnicodeContentDisposition()方法時,除了將filename參數設為檔案名稱外,也將inline參數設為true(也是該參數的預設值),即可取得正確的Content-Disposition Header值,將它附加在回應裡就可以解決Inline形式的下載檔名問題了!

// Inline形式下載檔案
public ActionResult DownloadTestFile()
{
var fileBytes = Encoding.UTF8.GetBytes("Hello, World!");
Response.AppendHeader("Content-Disposition",
JZLib.HeaderUtil.GetUnicodeContentDisposition("測試中文.txt", true));
return File(fileBytes, "text/plain");
}
MVC 5的Inline形式下載檔案,在存檔時也可以正確顯示中文檔名了。