星期三, 12月 25, 2024

[CCNA] Vlan Routing - SubInterface

延續該筆記 - [CCNA] switch 上設定 trunk,在 Router 上設定 SubInterface,讓不同 Vlan 之間可以互通,網路拓樸圖說明如下
  • 上方為 Vlan2 (192.168.20.0)、下方為 Vlan3 (192.168.30.0)
  • Router0 負責 Vlan Routing
  • Switch0、Switch1、Rounter0 彼此之間使用 Trunk 連接
Switch0 g0/1 Port 設定 Trunk 

語法如下,要注意執行 show interface trunk 語法時,router0 g0/1 Port 要先 ON 起來,線路是通的狀態下,才會顯示出 g0/1 trunk 資訊

Router 上設定 SubInterface

步驟為
  • 建立 SubInterface
  • 指定 Vlan (dot1Q 即為 802.1Q)
  • 設定 SubInterface ip
最後要把終端設備 PC 和 Laptop gateway,指定到對應的 SubInterface 去
觀察封包內的 vlan tag

從 PC0 ping Laptop0,使用 Simulation 模式在 Router0 上觀察封包
  • 在 Inbound 上可以看見 TCI vlan 2
  • Outbound TCI valn 3 


實際測試

星期三, 12月 18, 2024

[EF] ExecuteSqlCommand 呼叫 Store Procedure - Output 參數

下列筆記
都是把 Store Procedure 匯進 Entity 內去使用,該筆記紀錄直接使用 ExecuteSqlCommand 呼叫 Store Procedure 並透過 output 回傳資料

TSQL - Store Procedure

在 AdventureWorks 上建立 SP 來存取 Person.Person Table 並回傳指定資料的全名 (LastName + FirstName)
use AdventureWorks2022
go 

CREATE OR ALTER PROCEDURE uspEF_ExecuteSqlCommand_Output
(
	@BusinessEntityID int ,
	@FullName varchar(100) OUTPUT
)
AS
	BEGIN
		SELECT 
			@FullName = TRIM(LastName) + '-' + TRIM(FirstName)
		FROM Person.Person
		WHERE BusinessEntityID = @BusinessEntityID
	END

C# EF edmx 呼叫 SP
namespace EFSPOutput_ExecuteSqlCommand
{
    internal class Program
    {
        static void Main(string[] args)
        {
            AdventureWorks2022Entities dbContext = new AdventureWorks2022Entities();

            string TSQL = @"EXEC uspEF_ExecuteSqlCommand_Output @BusinessEntityID , @FullName OUTPUT";

            List<SqlParameter> parameters = new List<SqlParameter>();
            parameters.Add(new SqlParameter("@BusinessEntityID", SqlDbType.Int) { Value = 1 });

            var outputPara = new SqlParameter("@FullName", SqlDbType.VarChar, 100)
            {
                Direction = ParameterDirection.Output
            };
            parameters.Add(outputPara);

            dbContext.Database.ExecuteSqlCommand(TSQL, parameters.ToArray());

            Console.WriteLine(outputPara.Value.ToString());
        }
    }
}

星期一, 12月 09, 2024

[CCNA] switch 上設定 trunk

trunk 用途在於讓不同 switch 上的 vlan 可以互通,下圖為練習拓樸圖
終端設備為 PC 歸屬在 vlan2 內、終端設備為 Laptop 歸屬在 vlan3 內,並在 switch 上設定 trunk,讓兩台 switch 上的 vlan 彼此可以互通,主要分為三個設定步驟
  • 在 switch 上設定為 trunk
  • 在 switch 上設定 vlan 並把 port 劃進歸屬的 vlan 內
  • 在終端設備上使用 ping 來確認網路設定是否正確
下述筆記內容只記錄在 switch0 上設定截圖,switch1 也必須有相同設定

trunk

在兩台 switch 上的 fa0/1 設定 trunk
vlan

新增 vlan 2、vlan 3 並命名為 lab2、lab3
把 PC (fa0/10) 歸屬在 vlan2、Laptop (fa0/11) 歸屬在 vlan3 內
ping

確認 trunk 和 vlan 設定值
跨 switch 的同一個 vlan 彼此可以 ping 通

IDE 操作
CLI 相關語法整理
-- 把介面轉成 trunk
switchport mode trunk

-- 顯示目前設定為 trunk 的介面
show interface trunk

星期日, 12月 01, 2024

[CCNA] switch 上設定 vlan

在 switch 上設定 vlan 並記錄相關語法,下圖為簡易網路拓樸圖


CLI 語法

show vlan brief 確認 switch 上 port 所屬 vlan,vlan1 為預設值

新增 vlan 2 和 vlan 3 並更名為 lab2 和 lab3

把 switch 上的 
  • fa0/1、fa0/2 設定為 vlan2:對單一 Port 進行設定
  • fa0/3、fa0/4 設定為 vlan3:對多個 Port 進行設定

再次使用 show vlan brief 確認 vlan 設定

測試 vlan 設定
  • 從 PC0 上 ping 同為 vlan2 的 PC1 (192.169.10.2):O
  • 從 PC0 上 ping 在 vlan3 的 laptop1 (192.168.10.4):X

CLI 相關語法整理
-- 顯示 switch 上的 vlan 設定
show vlan brief

-- 新增 vlan
vlan vlan數字

-- 刪除 vlan
no vlan vlan數字

-- vlan 更名
name vlan名稱

-- 指定多個 Port
interface range PortRange (EX:fa0/3 - fa0/4)

-- 指定 Port 所屬 vlan
switchport 存取模式 (access/trunk) vlan vlan數字

IDE 操作

星期一, 11月 25, 2024

[C#] DataGridView 和 DoubleBuffered - 避免閃爍

延續 [C#] DataGridView 和 DoubleBuffered 筆記,從官方文章 - Control.DoubleBuffered 屬性 可以理解開啟 DoubleBuffered 可以減少或防止重繪閃動,驗證方式是 DataGridView.CellFormatting 內針對 Cell 進行顏色變化,移動捲軸來觀察顏色變化

有開 DoubleBuffered

沒有開 DoubleBuffered,每次卷軸往下就可以看出畫面閃爍
不知道是文件太舊還是對於內容有所誤會,開啟 DoubleBuffered 效果還蠻顯著

星期六, 11月 23, 2024

[C#] DataGridView 和 DoubleBuffered

在官方文章 - DataGridView.ColumnHeadersHeightSizeMode Property 發現這段文字說明
The DataGridView control does not support double buffering. If DoubleBuffered is set to true in a derived DataGridView control, users will not receive visual feedback when resizing rows, columns, or headers or when reordering columns.
所在環境也是有自訂 DataGridView 並開啟 DoubleBuffered 來使用,一直以來沒有出現大問題,測試理解何謂 visual feedback 效果

自訂 DataGridView 並開啟 DoubleBuffered
using System.Windows.Forms;

namespace AvoidDoubleBuffer
{
    public class UCDataGridView : DataGridView
    {
        public UCDataGridView()
        {
            DoubleBuffered = true;
        }
    }
}

調整欄位寬度

DoubleBuffered = true
DoubleBuffered = false
調整欄位順序

DoubleBuffered = true
DoubleBuffered = false

星期三, 11月 13, 2024

[Shopify] 新版顧客帳號頁面網域設定

開一個新商城時注意到,在結帳設定有下面訊息提醒



點擊 [變更網域] 過去後被導向 [客戶帳號] => [新版客戶帳號]

點擊 [變更網域] 按鈕就會進入顧客帳號網域設定,預設是 account,可自行修改

決定好子網域後,按下 [繼續] 按鈕,會出現 Shopify DNS CName 設定,看網域是託管在哪,把該 CName 設定進去就行,DNS 設定生效後按下 [驗證] 按鈕,就等兩邊驗證完成

在 Hinet 上輸入 CName 後大概 2 hr 後設定生效,Shopify 驗證大約是 1 hr 完成

驗證完成後就可以在 [網域] 內看到顧客帳號網域設定
最後也是最神奇的地方在於根本就沒有使用新版客戶帳號功能,但是會提醒要進行網域設定

星期二, 11月 12, 2024

[SSRS] 首頁頂端空白

在 SSRS 報表上常常會發生第一頁控件上方會出現空白,但第二頁又沒有的情況,專屬於第一頁的空白,後來是設定頁首來控制該情況

Tablix 控件離報表上方有段距離

實際執行,第一頁離報表上方有段空白,但第二頁卻又貼合報表上方

把頁首加入並把把背景設定為灰色,Tablix 則是完全貼合頁首下緣
實際執行第一頁空白就不會出現且第一、第二頁就會一致

目前都是是把頁首當成 SSRS 報表一定要存在設計,也一併處理該情況
  • 參考資料
  • 論壇討論 12

星期三, 11月 06, 2024

[SSMS] 索引標籤文字

發現 SSMS 索引標籤文字是可以設定,下圖為預設顯示,包含
  • Login 名稱
  • Server 名稱
  • Database 名稱
  • File 名稱

選項 => 文字編輯器 => 編輯器索引標籤和狀態列 => 索引標籤文字內可以進行更改
下圖為保留 Database 名稱和 File 名稱效果


星期一, 11月 04, 2024

[EFCore] SqlQuery、SqlQueryRaw - 欄位對應

延續 [EFCore] SqlQuery、SqlQueryRaw 筆記,在 Raw SQL queries for unmapped types 內還有提到 Data Annotations - ColumnAttribute 應用,之前把 Product Name 欄位在 PurchaseOrderDetailViewModel class 內轉為 ProductName,造成寫 TSQL 時,一定會需要 P.[Name]  AS ProductName 這段轉換,要不然會拋出錯誤。

TSQL Name 欄位未轉換為 ProductName

CASE 1:沒有任何 where 條件
string TSQL10 = @"
    SELECT 
        P.ProductID ,
        P.[Name] ,
        POD.OrderQty ,
        POD.UnitPrice ,
        POD.LineTotal 
    FROM [Purchasing].[PurchaseOrderHeader] AS POH
        JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
    ";

var result10 = dbContext.Database
    .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL10);

var result = await result10.ToListAsync();
Console.WriteLine(result.Count().ToString());
// TSQL 語法正常輸出
//    SELECT
//        P.ProductID ,
//        P.[Name] , 
//        POD.OrderQty ,
//        POD.UnitPrice ,
//        POD.LineTotal
//    FROM[Purchasing].[PurchaseOrderHeader] AS POH
//        JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
//        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
執行後會拋出該錯誤,原來 SqlQueryRaw 底層是 FromSql 去執行的,所以每個 Property 都要有值才行,在 TSQL pass 但卡在 EFCore 上。
Unhandled exception. System.InvalidOperationException: The required column 'ProductName' was not present in the results of a 'FromSql' operation.

CASE2:TSQL WHERE 沒有條件,但 LINQ WHERE 內有
string TSQL11 = @"
    SELECT 
        P.ProductID ,
        P.[Name] , 
        POD.OrderQty ,
        POD.UnitPrice ,
        POD.LineTotal 
    FROM [Purchasing].[PurchaseOrderHeader] AS POH
        JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
    ";

var result11 = dbContext.Database
    .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL11)
    .Where(vw => vw.ProductID == productID);

var result = await result11.ToListAsync();
Console.WriteLine(result.Count().ToString());
產生 TSQL
exec sp_executesql N'
	SELECT 
	    [e].[LineTotal], 
	    [e].[OrderQty], 
	    [e].[ProductID], 
	    [e].[ProductName], -- 外層為 ProductName
	    [e].[UnitPrice]
	FROM
		(
			SELECT
				P.ProductID,
				P.[Name], -- 內層為 Name
				POD.OrderQty,
				POD.UnitPrice,
				POD.LineTotal
			FROM[Purchasing].[PurchaseOrderHeader] AS POH
				JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
				JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
		) AS[e]
	WHERE[e].[ProductID] = @__productID_1',
	N'@__productID_1 int',@__productID_1=1
執行後會拋出該錯誤,從 TSQL 語法可以觀察到,LINQ WHERE 條件,會根據 PurchaseOrderDetailViewModel Property 轉成對應 TSQL 欄位,因為 TSQL 內層沒有把 Name 轉成 ProductName,導致外層抓不到 ProductName。
Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): 無效的資料行名稱 'ProductName'。

Data Annotations - ColumnAttribute

在 PurchaseOrderDetailViewModel 上把 ProductName 加上 ColumnAttribute
namespace EFCoreQuery.Models
{
    public class PurchaseOrderDetailViewModel
    {
        public int ProductID { get; set; }

        /// <summary>
        /// 把 Name 轉為 ProductName
        /// </summary>
        [Column("Name")]
        public string ProductName { get; set; }

        public Int16 OrderQty { get; set; }

        public decimal UnitPrice { get; set; }

        public decimal LineTotal { get; set; }
    }
}
CASE 1:沒有任何 where 條件
string TSQL13 = @"
    SELECT 
        P.ProductID ,
        P.[Name] ,
        POD.OrderQty ,
        POD.UnitPrice ,
        POD.LineTotal 
    FROM [Purchasing].[PurchaseOrderHeader] AS POH
        JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
    ";

var result13 = dbContext.Database
    .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL13);

var result = await result13.ToListAsync();
Console.WriteLine(result.Count().ToString());
// TSQL 語法正常輸出
//    SELECT
//        P.ProductID ,
//        P.[Name] , 
//        POD.OrderQty ,
//        POD.UnitPrice ,
//        POD.LineTotal
//    FROM[Purchasing].[PurchaseOrderHeader] AS POH
//        JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
//        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
正常執行,沒有拋出 FromSql ProductName 沒有值的錯誤,從中斷點去觀察,也發現值有塞進去,TSQL Name 欄位和 PurchaseOrderDetailViewModel ProudctName 屬性會自動對應。

CASE2:TSQL WHERE 沒有條件,但 LINQ WHERE 內有
int productID = 1;

string TSQL14 = @"
    SELECT 
        P.ProductID ,
        P.[Name] , 
        POD.OrderQty ,
        POD.UnitPrice ,
        POD.LineTotal 
    FROM [Purchasing].[PurchaseOrderHeader] AS POH
        JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
    ";

var result14 = dbContext.Database
    .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL14)
    .Where(vw => vw.ProductID == productID);

var result = await result14.ToListAsync();
Console.WriteLine(result.Count().ToString());
產生 TSQL
exec sp_executesql N'
	SELECT 
	    [e].[LineTotal], 
	    [e].[OrderQty], 
	    [e].[ProductID], 
	    [e].[Name], -- 外層為 Name
	    [e].[UnitPrice]
	FROM
		(
			SELECT
				P.ProductID,
				P.[Name], -- 內層為 Name
				POD.OrderQty,
				POD.UnitPrice,
				POD.LineTotal
			FROM[Purchasing].[PurchaseOrderHeader] AS POH
				JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
				JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
		) AS[e]
	WHERE[e].[ProductID] = @__productID_1',
	N'@__productID_1 int',@__productID_1=1
外層 TSQL 產生 Name 而不是 ProductName

在 EF 學習 Data Annotations 時,是在 Code First FluentAPI 內設定給 Migration 使用,沒想到在 EFCore DbFirst 已經可以影響 TSQL 產生。

星期日, 11月 03, 2024

[EFCore] SqlQuery、SqlQueryRaw

延續 [EFCore] FromSql、FromSqlRaw 筆記,根據 SQL QueriesRaw SQL queries for unmapped types 文章內容,來記錄 SqlQuery 和 SqlQueryRaw,使用 EFCore 8、AdventureWorks2022 Purchasing 相關 Table 為資料來源並開啟 Sql Profile 來觀察產生的 TSQL 語法。

SqlQuery 和 SqlQueryRaw
  • 兩者都是在 EFore 7.0 新功能
  • EFCore 7.0:只能回傳 Scalar Value,EX:string、datetime、int
  • EFCore 8.0:能回傳 unmapped types,EX:DTO、ViewModel

PurchaseOrderDetailViewModel

用來承接 SqlQuery、SqlQueryRaw 結果
namespace EFCoreQuery.Models
{
    public class PurchaseOrderDetailViewModel
    {
        public int ProductID { get; set; }

        /// <summary>
        /// 把 Name 轉為 ProductName
        /// </summary>
        public string ProductName { get; set; }

        public Int16 OrderQty { get; set; }

        public decimal UnitPrice { get; set; }

        public decimal LineTotal { get; set; }
    }
}
EFCore 7.0 實際測試
namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();

            // CASE 1:使用 EFCore 7.0,只能回傳 Scalar Value
            var productID = new SqlParameter("@ProductID", SqlDbType.Int);
            productID.Value = 403;

            var result6 = await dbContext.Database.SqlQuery<int>(
                $"SELECT ProductID FROM Production.[Product] WHERE ProductID = {productID}")
                .ToListAsync();
            Console.WriteLine(result6.Count().ToString());
            // TSQL:exec sp_executesql
            //     N'SELECT ProductID FROM Production.[Product] WHERE ProductID = @ProductID',
            //     N'@ProductID int',@ProductID=403

            // CASE 2:EFCore 7.0 不支援回傳 unmapped types
            var purchaseOrderID = new SqlParameter("@PurchaseOrderID", SqlDbType.SmallInt);
            purchaseOrderID.Value = 8;

            string TSQL7 = @"
                SELECT 
                    P.ProductID ,
                    P.[Name] AS ProductName , 
                    POD.OrderQty ,
                    POD.UnitPrice ,
                    POD.LineTotal 
                FROM [Purchasing].[PurchaseOrderDetail] AS POD
                    JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
                WHERE POD.PurchaseOrderID = @PurchaseOrderID
                ";

            var result7 = await dbContext.Database.SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL7, purchaseOrderID).ToListAsync();
            Console.WriteLine(result7.Count().ToString());
            // EFCore 7.0 會拋出下列錯誤,EFCore 8.0 才能正常執行
            // Unhandled exception. System.InvalidOperationException:
            // The element type 'EFCoreQuery.Models.PurchaseOrderDetailViewModel' used in 'SqlQuery' method is not natively supported by your database provider.
            // Either use a supported element type, or use ModelConfigurationBuilder.DefaultTypeMapping to define a mapping for your type.
        }
    }
}
EFCore 8.0 實際測試

TSQL 和 LINQ WHERE 條件混搭方式在 EFCore 7.0 就可以,該筆記寫在 EFCore 8.0 內紀錄而已,SqlQuery 和 SqlQueryRaw 回傳 IQueryable,所以後續才能再使用 LINQ WHERE 來篩選資料
namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();

            // CASE 1::EFCore 8.0 支援 unmapped types
            var orderDate = new SqlParameter("@OrderDate", SqlDbType.DateTime);
            orderDate.Value = new DateTime(2014, 1, 1);

            FormattableString TSQL8 = $@"
                SELECT 
                    P.ProductID ,
                    P.[Name] AS ProductName , 
                    POD.OrderQty ,
                    POD.UnitPrice ,
                    POD.LineTotal 
                FROM [Purchasing].[PurchaseOrderHeader] AS POH
                    JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
                    JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
                WHERE POH.OrderDate >= {orderDate}
                ";

            var result8 = await dbContext.Database
                .SqlQuery<PurchaseOrderDetailViewModel>(TSQL8)
                .Where(vw => vw.UnitPrice > 80)
                .ToListAsync();

            Console.WriteLine(result8.Count().ToString());
            // TSQL:exec sp_executesql
            //     N'SELECT [e].[LineTotal], [e].[OrderQty], [e].[ProductID], [e].[ProductName], [e].[UnitPrice]
            //       FROM(
            //              SELECT
            //                  P.ProductID,
            //                  P.[Name] AS ProductName,
            //                  POD.OrderQty,
            //                  POD.UnitPrice,
            //                  POD.LineTotal
            //              FROM[Purchasing].[PurchaseOrderHeader] AS POH
            //                  JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
            //                  JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
            //              WHERE POH.OrderDate >= @OrderDate
            //           ) AS[e]
            //       WHERE[e].[UnitPrice] > 80.0',
            //     N'@OrderDate datetime',@OrderDate='2014 - 01 - 01 00:00:00'

            // CASE 2:模擬條件串接情況
            int? productID = 1;
            short? orderQty = 4;

            string TSQL9 = @"
                SELECT 
                    P.ProductID ,
                    P.[Name] AS ProductName , 
                    POD.OrderQty ,
                    POD.UnitPrice ,
                    POD.LineTotal 
                FROM [Purchasing].[PurchaseOrderHeader] AS POH
                    JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
                    JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
                ";

            var result = dbContext.Database
                .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL9);

            if (productID.HasValue)
                result = result.Where(vw => vw.ProductID == productID.Value);

            if (orderQty.HasValue)
                result = result.Where(vw => vw.OrderQty >= orderQty.Value);

            Console.WriteLine((await result.ToListAsync()).Count().ToString());

            // TSQL:exec sp_executesql 
            //     N'SELECT [e].[LineTotal], [e].[OrderQty], [e].[ProductID], [e].[ProductName], [e].[UnitPrice]
            //       FROM(
            //              SELECT
            //                  P.ProductID,
            //                  P.[Name] AS ProductName,
            //                  POD.OrderQty,
            //                  POD.UnitPrice,
            //                  POD.LineTotal
            //              FROM[Purchasing].[PurchaseOrderHeader] AS POH
            //                  JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
            //                  JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
            //           ) AS[e]
            //       WHERE[e].[ProductID] = @__productID_Value_1
            //              AND[e].[OrderQty] >= @__orderQty_Value_2',
            //       N'@__productID_Value_1 int, @__orderQty_Value_2 smallint',@__productID_Value_1=1,@__orderQty_Value_2=4

        }
    }
}
從上述兩段語法就可以發現 LINQ WHERE 語法產生條件,會在主要的 TSQL 語法外再包一層

星期六, 11月 02, 2024

[RV] 影像控件 - QRCode 顯示

論壇問題,基本上是在 SSRS 上顯示 QRCode,剛好可以拿來練習影像控件 (Image) 顯示圖檔。

SSRS Image 控件有三種影像來源模式,分別為
該筆記是使用 [資料庫 (data-bound)] ,來製作財產標籤,標籤內包含 QRCode

資料來源

在 AdventureWork2022 內建立 Asset
USE [AdventureWorks2022]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Asset](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[NO] [char](5) NOT NULL,
	[Name] [nvarchar](100) NOT NULL,
	[PurchDate] [date] NULL,
	[Location] [nvarchar](20) NOT NULL,
	[Department] [nvarchar](20) NOT NULL,
 CONSTRAINT [PK_Asset] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Asset] ADD  CONSTRAINT [DF_Asset_NO]  DEFAULT ('') FOR [NO]
GO

ALTER TABLE [dbo].[Asset] ADD  CONSTRAINT [DF_Asset_Name]  DEFAULT (N'') FOR [Name]
GO

ALTER TABLE [dbo].[Asset] ADD  CONSTRAINT [DF_Asset_Location]  DEFAULT (N'') FOR [Location]
GO

ALTER TABLE [dbo].[Asset] ADD  CONSTRAINT [DF_Asset_Department]  DEFAULT (N'') FOR [Department]
GO

-- 建立假資料
INSERT INTO [dbo].[Asset] ([NO],[Name],[PurchDate],[Location],[Department]) VALUES
	('A0001' , N'商用電腦' , '20240101' , N'職員一' , N'行政') ,
	('A0002' , N'事務機'   , '20230707' , N'職員一' , N'行政') ,
	('A0003' , N'油壓台車' , '20200820' , N'職員二' , N'運輸') ,
	('A0004' , N'貨車'     , '20190505' , N'職員二' , N'運輸') ,
	('A0005' , N'手機'     , '20190607' , N'職員二' , N'運輸')
GO

Model 和 ReportViewModel

使用 Entity Framework 來存取資料並建立 ReportViewModel 來呈現 QRCode
namespace ZXingQRCode
{
    public class AssetReportModel : Asset
    {
        public byte[] QRcodeInfo { get; set; }
    }
}

QRCode

使用 ZXing 套件來產生 QRCode 圖檔
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using ZXing;

namespace ZXingQRCode
{
    internal static class QRCodeGenerator
    {
        public static byte[] Create(string content)
        {
            if (string.IsNullOrWhiteSpace(content))
                throw new ArgumentNullException(content);

            BarcodeWriter writer = new BarcodeWriter
            {
                Format = BarcodeFormat.QR_CODE,
                Options = new ZXing.QrCode.QrCodeEncodingOptions
                {
                    Margin = 0,
                    Width = 200,
                    Height = 200
                }
            };

            using (Bitmap bitmap = writer.Write(content))
            using (MemoryStream stream = new MemoryStream())
            {
                bitmap.Save(stream, ImageFormat.Png);
                return stream.ToArray();
            }
        }
    }
}

報表設計

設計 8 X 5 cm 大小標籤,並使用清單來呈現每一筆資料,同時把自訂編號資訊轉成 QRCode 來呈現
影像控件設定
程式執行
using Microsoft.Reporting.WinForms;
using System;
using System.Data;
using System.Linq;
using System.Windows.Forms;

namespace ZXingQRCode
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            var dbContext = new AdventureWorks2022Entities();
            var dataSources = dbContext.Asset.ToList();
            var reportSource = dataSources.Select(s => new AssetReportModel()
            {
                NO = s.NO,
                Name = s.Name,
                PurchDate = s.PurchDate,
                Location = s.Location,
                Department = s.Department,
                QRcodeInfo = QRCodeGenerator.Create(s.NO)
            }).ToList();

            reportViewer1.LocalReport.DataSources.Add(new ReportDataSource(nameof(AssetReportModel), reportSource));
            reportViewer1.RefreshReport();
        }
    }
}

星期五, 11月 01, 2024

[SQL] 彙總函數應用 - 字串欄位分組

網路問題,基本上和 [SQL] 彙總函數應用 - 資料分組 是類似問題但相同解法,透過累計加總 (Running Total) 來做到字串欄位的資料分組
use tempdb
GO

DROP TABLE IF EXISTS #Temp

CREATE TABLE #Temp 
(
    ID int IDENTITY(1,1),
    Data char(1) null
)

INSERT INTO #Temp VALUES
    ('1') , (null) , (null) ,
    ('5') , (null) , (null) ,
    ('8') , (null) , (null) 

SELECT * , 
    SUM(IIF(Data IS NULL , 0 , 1)) OVER 
        (
            ORDER BY ID 
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 
        ) AS GroupNo
FROM #Temp

星期二, 10月 29, 2024

[EFCore] FromSql、FromSqlRaw

根據 SQL Queries 文章內容,來記錄 FromSql 和 FromSqlRaw,使用  EFCore 8、AdventureWorks2022 Production.Product Table 為資料來源並開啟 Sql Profile  來觀察產生的 TSQL 語法

FromSql

using EFCoreQuery.Models;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using System.Data;

namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();
            
            // CASE 1:常數
            var result1 = await dbContext.Product.FromSql(
                $"SELECT * FROM Production.Product WHERE Class = N'H'").ToListAsync();
            Console.WriteLine(result1.Count().ToString());
            // TSQL:SELECT * FROM Production.Product WHERE Class = N'H'

            // CASE2:C# 字串插補傳遞變數,TSQL 會參數化,資料型態、長度為 nvarchar(4000)
            string data = "H";
            var result2 = await dbContext.Product.FromSql(
                $"SELECT * FROM Production.Product WHERE Class = {data}").ToListAsync();
            Console.WriteLine(result2.Count().ToString());
            // TSQL:exec sp_executesql 
            //     N'SELECT * FROM Production.Product WHERE Class = @p0' ,
            //     N'@p0 nvarchar(4000)',@p0=N'H'

            // CASE3:C# 字串插補傳遞 SqlParameter 並對應欄位資料型態、長度 nchar(2),TSQL 會參數化
            var para = new SqlParameter("@Class", SqlDbType.NChar, 2);
            para.Value = "H";
            var result3 = await dbContext.Product.FromSql(
                $"SELECT * FROM Production.Product WHERE Class = {para}").ToListAsync();
            Console.WriteLine(result3.Count().ToString());
            // TSQL:exec sp_executesql 
            //     N'SELECT * FROM Production.Product WHERE Class = @Class',
            //     N'@Class nchar(2)',@Class=N'H '            

        }
    }
}

CASE2 雖然是字串插補傳遞變數,但會將變數包裝於 SqlParameter 內,產生的 TSQL 會參數化,不會是組合字串傳進去,可以避免 SqlInjection

FromSqlRaw

可以用它動態改變欄位名稱,但有 Sqlnjection 風險,不適合應用在實務上
using EFCoreQuery.Models;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using System.Data;

namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();

            // CASE1:C# 字串插補傳遞欄位名稱、SqlParameter 參數值
            string columnName = "Class";

            var para = new SqlParameter("@Class", SqlDbType.NChar, 2);
            para.Value = "H";

            var result4 = await dbContext.Product.FromSqlRaw(
                $"SELECT * FROM Production.Product WHERE {columnName} = @Class", para).ToListAsync();
            Console.WriteLine(result4.Count().ToString());
            // TSQL:exec sp_executesql
            //     N'SELECT * FROM Production.Product WHERE Class = @Class',
            //     N'@Class nchar(2)',@Class=N'H ' 

            // CASE2:C# 字串插補傳遞變數只能應用在欄位
            string data = "H";
            var result5 = await dbContext.Product.FromSqlRaw(
                $"SELECT * FROM Production.Product WHERE Class = {data}").ToListAsync();
            Console.WriteLine(result5.Count().ToString());
            // Exception:Unhandled exception. 
            // Microsoft.Data.SqlClient.SqlException (0x80131904):
            // 無效的資料行名稱 'H'
        }
    }
}

FromSql 和 FromSqlRaw 限制

官方文件上說明
  • The SQL query must return data for all properties of the entity type.
  • The column names in the result set must match the column names that properties are mapped to. Note that this behavior is different from EF6; EF6 ignored property-to-column mapping for SQL queries, and result set column names had to match those property names.
  • The SQL query can't contain related data. However, in many cases you can compose on top of the query using the Include operator to return related data (see Including related data).
個人理解是回傳的 Entity 每個 Property 都要對應到,只要有一個 Property 沒有對應到就會拋出  InvalidOperationException 且抓取關聯 Entity 時一定得透過 Include
namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();

            // CASE:回傳的 Entity 每個 Property 都要對應到,故意只抓 ProductID 而已
            var result = await dbContext.Product.FromSql(
                $"SELECT ProductID FROM Production.Product WHERE Class = N'H'").ToListAsync();
            Console.WriteLine(result.Count().ToString());
            // Unhandled exception. System.InvalidOperationException:
            // The required column 'Class' was not present in the results of a 'FromSql' operation.
        }
    }
}
函式演變

整理如下
  • EFCore 2:只有 FromSql
  • EFCore 3:FromSql 被移除,取而代之的是 FromSqlRaw 和 FromSqlInterpolated。
  • EFCore 7: FromSql 強勢回歸,用來取代 FromSqlInterpolated,但 FromSqlRaw 仍然存在
EFCore 3 BreakChange 內關於 FromSql 被取代的相關章節
EFCore 7 FromSql 回歸取代 FromSqlInterpolated 則是在 SQL Queries 內的備註上看到,EFCore 7 What's NewBreakChange 內反而沒有提到


星期六, 10月 26, 2024

[C#] DefaultEventAttribute

DefaultEventAttribute 可以用來指定元件預設事件,以 C# WinForm ComboBox 為例說明,當把 ComboBox 拉近 Form 內後,直接點擊控件,會直接在 cs 檔案內建立 SelectedIndexChanged 事件,SelectedIndexChanged 為其預設事件
ComboBox 事件觸發時機
  • SelectedIndexChanged:使用程式變更 Index、使用者點選
  • SelectionChangeCommitted:使用者點選
實務上是希望使用者點選時才觸發事件,但常常在畫面顯示上需要指定 ComboBox 顯示時的預設值,使用 Code 設定預設值時會觸發 SelectedIndexChanged,必須改用 SelectionChangeCommitted 才行。

建立自訂 ComboBox 控件並透過 DefaultEventAttribute 來自訂預設事件,把預設事件設定為 SelectionChangeCommitted,這樣直接點選 ComboBox 就會在 cs 檔案內產生 SelectionChangeCommitted 事件

UserControl 預設事件截圖

星期一, 10月 21, 2024

[C#] Path - GetExtension()

實務上有檔案檔名是 [日期 + 時間] 的情況,在使用 Path.GetExtension() 時發生異常

C# Code
namespace PathSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            List<string> target = new List<string>();
            target.Add(@"\2024-10-1817.29.37.777.jpg");   // 正常路徑
            target.Add(@"\2024-10-1817.29.37.777");       // 尾端沒有副檔名
            target.Add(@"\2024-10-1817.29.37.777.jpg.");  // 尾端有 . 符號
            target.Add(@"\2024-10-1817.29.37.777.jpg\");  // 尾端有 \ 符號

            string extension = string.Empty;
            foreach (var path in target)
            {
                extension = Path.GetExtension(path);
                extension = string.IsNullOrWhiteSpace(extension) ? "空值" : extension;
                Console.WriteLine($"{path} 副檔名:{extension}");
            }
        }
    }
}
顯示結果

官方文件內容

文件備註
This method obtains the extension of path by searching path for a period (.), starting with the last character in path and continuing toward the first character. If a period is found before a DirectorySeparatorChar or AltDirectorySeparatorChar character, the returned string contains the period and the characters after it; otherwise, String.Empty is returned.
C# SourceCode
基本上 GetExtension() 就是找 [最後的 \ 符號] 至尾端文字,該端文字內 [最後的 . 符號] 至尾端文字內容