星期二, 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() 就是找 [最後的 \ 符號] 至尾端文字,該端文字內 [最後的 . 符號] 至尾端文字內容

星期日, 10月 20, 2024

[C#] Path - 預設路徑

看見一段 C# 語法
Path.GetExtension("FileExtensiom.png");
有點意外這樣可以單獨取出副檔名,腦海裡 Path class 都是針對路徑來取得相對應資料,實際查詢時發現,原來使用 Path 時會有預設路徑的存在,不是真的直接對 FileExtensiom.png 這名稱去進行解析
namespace PathSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string targetFileName = @"FileExtension.png";
            string currentDirectory = Environment.CurrentDirectory;
            string fullPath = Path.GetFullPath(targetFileName);
            string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(targetFileName);
            string extension = Path.GetExtension(targetFileName);

            Console.WriteLine($"CurrentDirectory:{currentDirectory}");
            Console.WriteLine($"FullPath:{fullPath}");
            Console.WriteLine($"fileName:{fileNameWithoutExtension}");
            Console.WriteLine($"extension:{extension}");
        }
    }
}

星期六, 10月 19, 2024

[C#] File.Copy - UnauthorizedAccessException

C# File.Copy overwrite 參數,一直以為把它設定為 true,目的端檔案就一定可以覆蓋過去,沒想到它也有例外情況,實務上遇上指定 overwrite 情況下,拋出 [UnauthorizedAccessException 拒絕存取路徑] Exception,肯定有對應權限情況下,在 File.Copy 官方文件 上找到線索 UnauthorizedAccessException 有下列三種情況會發生
  • 呼叫端沒有對應權限
  • 目的端檔案屬性為唯讀 (ReadOnly)
  • overwrite 為 true,目的端檔案存在且檔案屬行為隱藏 (hidden),但是來源端檔案屬性不是隱藏 (hidden)
遇上第二種情況,下面簡易紀錄

唯讀檔案

C# Code
using System.IO;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string sourceFile = @"D:\FileCopyException\Source\FileCopyException.txt";
            string DestFile = @"D:\FileCopyException\Dest\FileCopyException.txt";

            // 目的端沒有檔案,可以正常複製過去
            File.Copy(sourceFile, DestFile, true);
            // 第一次複製過去後,複製檔案會保留唯讀屬性,
            // 所以第二次複製過去,就算有 overrite 也會拋出 Exception
            File.Copy(sourceFile, DestFile, true);
        }
    }
}
執行後拋出的 Exception
未處理的例外狀況: System.UnauthorizedAccessException: 拒絕存取路徑 'D:\FileCopyException\Dest\FileCopyException.txt'。
    於 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
    於 System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)
    於 System.IO.File.Copy(String sourceFileName, String destFileName, Boolean overwrite)
    於 ConsoleApp1.Program.Main(String[] args) 於 D:\ConsoleApp1\Program.cs: 行 20
一開始看到 System.IO.__Error.WinIOError 以為是 IOException,還想說不是有 overrite,被自己誤導。

星期五, 10月 18, 2024

IBM x3250 M3 - 指示橘燈

要拿這台 12 年的 [IBM x3250 M3] 來當成特定機台的 File Server,一開機就發現指示燈亮橘燈且風扇狂轉,發現是 BIOS System Log 滿造成亮橘燈,最新的 System Log 還在 2016 年,把 System Log 清除並完全斷電後再重啟,指示橘燈才熄滅且風扇轉速恢復正常

指示橘燈
第一直覺是 Disk 出問題,但 MegaRAID 顯示正常
發現 BIOS System Log 已滿訊息

星期三, 10月 16, 2024

[SQL] 視窗函數應用 - 連續字串分組

延續 [SQL] 分析函數應用 - 排班班表,班別基本上是連續字串,想要直接處理字串就產生群組,而不依賴日期欄位,資料來源和 TSQL 流程如下圖
TSQL
DECLARE @Temp TABLE
(
    ID int identity(1,1) ,
    Col1 char(1)
)

INSERT INTO @Temp VALUES
    ('A') , ('A') , ('A') , ('B') , ('C') , ('C') , ('A') , ('A') ,  ('A')

;
WITH T1 AS
(
    SELECT 
        * , 
        -- 第三參數:null 的預設值
        LAG(Col1 , 1 , Col1) OVER (ORDER BY ID)AS PreValue
    FROM @Temp
)
, T2 AS
(
    SELECT
        * ,
        -- 判斷連續字串是否有變
        CAST(IIF(Col1 = PreValue , 0 , 1) AS int) AS GroupChange
    FROM T1
)
, T3 AS
(
    SELECT
        * ,
        -- 使用彙總視窗函數跑 Running Total
        SUM(GroupChange) OVER (ORDER BY ID ROWS UNBOUNDED PRECEDING) AS GroupNO 
	FROM T2
)
SELECT * 
FROM T3
ORDER BY ID
LAG default 參數

官方文件說明
The value to return when offset is beyond the scope of the partition. If a default value is not specified, NULL is returned. default can be a column, subquery, or other expression, but it cannot be an analytic function. default must be type-compatible with scalar_expression.
該範例就是把第一筆資料的 null 塞 A 來取代

星期二, 10月 15, 2024

[SQL] 分析函數應用 - 排班班表

社群問題,簡化為下圖來理解,資料來源只顯示單一位員工資訊
相關商業邏輯
  • WorkWay 班別會混雜 [特、休] 休假資訊,A 代表 08-16 早班時段、B 代表 13-21 午班時段 
  • [特、休] 所屬班別判斷邏輯:班別是若遇當月休假換班別,就歸屬前一個班別,例如 10/13 休 10/14 換成 A 班,那麼 10/13 會往前抓到最後一天的班別,但是像月初特休就會往後抓班別來判斷

資料來源
USE TempDB
GO

DROP TABLE IF EXISTS #Attend

CREATE TABLE #Attend(
	Name nvarchar(20) ,
	WorkDate date ,
	WorkWay nvarchar(5))

INSERT INTO #Attend
VALUES 
	(N'小明','2024/10/1' ,N'A' ) , (N'小明','2024/10/2' ,N'A' ) , (N'小明','2024/10/3' ,N'A' ) , (N'小明','2024/10/4' ,N'A' ) , (N'小明','2024/10/5' ,N'休') , 
	(N'小明','2024/10/6' ,N'休') , (N'小明','2024/10/7' ,N'A' ) , (N'小明','2024/10/8' ,N'A' ) , (N'小明','2024/10/9' ,N'A' ) , (N'小明','2024/10/10',N'休') ,
	(N'小明','2024/10/11',N'A' ) , (N'小明','2024/10/12',N'休') , (N'小明','2024/10/13',N'休') , (N'小明','2024/10/14',N'B' ) , (N'小明','2024/10/15',N'B' ) ,
	(N'小明','2024/10/16',N'B' ) , (N'小明','2024/10/17',N'B' ) , (N'小明','2024/10/18',N'B' ) , (N'小明','2024/10/19',N'休') , (N'小明','2024/10/20',N'休') ,
	(N'小明','2024/10/21',N'A' ) , (N'小明','2024/10/22',N'A' ) , (N'小明','2024/10/23',N'A' ) , (N'小明','2024/10/24',N'A' ) , (N'小明','2024/10/25',N'A' ) ,
	(N'小明','2024/10/26',N'休') , (N'小明','2024/10/27',N'休') , (N'小明','2024/10/28',N'A' ) , (N'小明','2024/10/29',N'A' ) , (N'小明','2024/10/30',N'A' ) , (N'小明','2024/10/31',N'A') ,
	(N'大白','2024/10/1' ,N'特') , (N'大白','2024/10/2' ,N'特') , (N'大白','2024/10/3' ,N'B' ) , (N'大白','2024/10/4' ,N'B' ) , (N'大白','2024/10/5' ,N'休') ,	(N'大白','2024/10/6' ,N'休') , (N'大白','2024/10/7' ,N'B' ) , (N'大白','2024/10/8' ,N'B' ) , (N'大白','2024/10/9' ,N'B' ) , (N'大白','2024/10/10',N'休') ,
	(N'大白','2024/10/11',N'B' ) , (N'大白','2024/10/12',N'休') , (N'大白','2024/10/13',N'休') , (N'大白','2024/10/14',N'A' ) , (N'大白','2024/10/15',N'A' ) ,
	(N'大白','2024/10/16',N'A' ) , (N'大白','2024/10/17',N'A' ) , (N'大白','2024/10/18',N'A' ) , (N'大白','2024/10/19',N'休') , (N'大白','2024/10/20',N'休') ,
	(N'大白','2024/10/21',N'B' ) , (N'大白','2024/10/22',N'B' ) , (N'大白','2024/10/23',N'B' ) , (N'大白','2024/10/24',N'B' ) , (N'大白','2024/10/25',N'B' ) , 
	(N'大白','2024/10/26',N'休') , (N'大白','2024/10/27',N'休') , (N'大白','2024/10/28',N'B' ) , (N'大白','2024/10/29',N'B' ) , (N'大白','2024/10/30',N'B ') , (N'大白','2024/10/31',N'B')
TSQL 寫法

主要分為三個步驟,分別為
  • 把 [特、休] 字樣改成所屬班別,使用分析視窗函數參數 IGNORE NULLS 來整理
  • 透過連續日期、連續班別來判斷群組
  • 產生對應需求資料
; 
WITH T1 AS 
( 
    SELECT * ,
        IIF(WorkWay IN ('A' , 'B') , WorkWay , NULL) AS NewWorkWay
    FROM #Attend
)
, T2 AS
(
    SELECT * ,
        COALESCE(
            NewWorkWay ,
            LAG(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate) ,
            LEAD(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate)) AS GroupNO
    FROM T1
)
, T3 AS 
(
    SELECT 
        * ,
        DATEADD(
            d , 
            ROW_NUMBER() OVER (PARTITION BY Name , GroupNO ORDER BY WorkDate) * -1 , 
            WorkDate) AS GroupDate
        FROM T2
)
, T4 AS
(
    SELECT 
        T3.Name , 
        T3.GroupDate ,
        T3.GroupNO ,
        MIN(WorkDate) AS MinDate ,
        T3.GroupNO + ':' + CAST(DAY(MIN(WorkDate)) AS varchar(2)) + '~' + CAST(DAY(MAX(WorkDate)) AS varchar(2)) AS WorkInfo
    FROM T3
    GROUP BY Name , GroupDate , GroupNO
)
SELECT
    Name ,
    STRING_AGG(WorkInfo , ' , ') WITHIN GROUP (ORDER BY MinDate)
FROM T4
GROUP BY Name
分析視窗函數參數 IGNORE NULLS

因為 WorkWay 有 [特、休] 字樣來呈現該員工休假狀態,所以要把 [特、休] 改為 null,方便後續分析函數搭配 IGNORE NULLS,下面語法故意把 LAG 產生的 PreviousValue 和 LEAD 產生的 NextValue 資料獨立出來顯示,從圖片就可以觀察到 T1 和 T2 這兩個階段處理事項
; 
WITH T1 AS 
( 
    SELECT * ,
        IIF(WorkWay IN ('A' , 'B') , WorkWay , NULL) AS NewWorkWay
    FROM #Attend
    WHERE Name = N'大白'
)
, T2 AS
(
    SELECT * ,
        -- 顯示資料來理解
        LAG(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate) AS PreviousValue ,
        -- 顯示資料來理解
        LEAD(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate) AS NextValue ,
        COALESCE(
            NewWorkWay ,
            LAG(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate) ,
            LEAD(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate)) AS GroupNO
    FROM T1
)
SELECT * FROM T2
ORDER BY Name , WorkDate

透過連續日期、連續班別來判斷群組

透過 Name 和 GroupNO 來跑 Row_Number() 並和工作日期運算來判斷群組
, T3 AS 
(
    SELECT 
        * ,
        -- 顯示資料來理解
        ROW_NUMBER() OVER (PARTITION BY Name , GroupNO ORDER BY WorkDate) * -1 AS DisplayRowNO,
        DATEADD(
            d , 
            ROW_NUMBER() OVER (PARTITION BY Name , GroupNO ORDER BY WorkDate) * -1 , 
            WorkDate) AS GroupDate
    FROM T2
)
SELECT * FROM T3
ORDER BY Name , WorkDate

產生對應需求資料

分出員工連續工作日後,就可以找出每個群組起始和結束日期,再把資料整理成問題需求,最後使用 STRING_AGG 把資料串在一起呈現
, T4 AS
(
    SELECT 
        T3.Name , 
        T3.GroupDate ,
        T3.GroupNO ,
        MIN(WorkDate) AS MinDate ,
        T3.GroupNO + ':' + CAST(DAY(MIN(WorkDate)) AS varchar(2)) + '~' + CAST(DAY(MAX(WorkDate)) AS varchar(2)) AS WorkInfo
    FROM T3
    GROUP BY Name , GroupDate , GroupNO
)
SELECT
    Name ,
    STRING_AGG(WorkInfo , ' , ') WITHIN GROUP (ORDER BY MinDate)
FROM T4
GROUP BY Name

星期六, 10月 12, 2024

[SQL] 伺服器定序 - 變數大小寫

Line 討論發現到,原來定序會影響 TSQL 撰寫時的大小寫,原以為定序只對資料排序有影響而已,在官方文件 - 設定或變更伺服器定序 上發現文字說明
變數、資料指標和 GOTO 標籤的名稱。 例如,如果伺服器層級定序區分大小寫,變數 @pi 和 @PI 會視為不同的變數;如果伺服器層級定序不區分大小寫,則會視為相同的變數。
下列 TSQL 語法和圖示可以觀察到伺服器定序為 Chinese_Taiwan_Stroke_CS_AS 區分大小寫,可以看見變數大小寫在 IDE 上會有錯誤提示
SELECT SERVERPROPERTY('collation')

星期四, 10月 10, 2024

[C#] DataGridViewColumn.Clone()

延續 [C#] 提供資料行已經屬於 DataGridView 控制項 筆記,發現原來 DataGridViewColumn.Clone() 產生 DataGridViewColumn 是可以再放進其他 DataGridViw

C# Code
private void btnClone_Click(object sender, EventArgs e)
{
    // 寫法一
    List<DataGridViewColumn> cloneColumn = new List<DataGridViewColumn>();
    foreach (DataGridViewColumn col in dataGridView1.Columns)
        cloneColumn.Add(col.Clone() as DataGridViewColumn);

    // 寫法二
    List<DataGridViewColumn> cloneColumn = dataGridView1.Columns.OfType<DataGridViewColumn>()
        .Select(col => col.Clone() as DataGridViewColumn)
        .ToList();

    dataGridView2.Columns.AddRange(cloneColumn.ToArray());
}
從中斷點內可以觀察到 DataGridViewColumn.DataGridView 屬性變成 null
實際執行

星期二, 10月 08, 2024

[CCNA] 動態路由 - OSPF - 優先權

延續 [CCNA] 動態路由 - OSPF[CCNA] 動態路由 - OSPF - 負載平衡 筆記,從 router0 => router1 有兩條路由,如下圖
上述兩條路由 AD 相同,但 Cost 不同,優先是以 router0 => router1 為主,當 router0 => router1 不通時,才會改成 router0 => router3 => router2 => router1 ,Cost 計算可以參考 認識OSPF路由協定 活用相關指令設定參數值 內的公式介紹,兩條路由成本分別為
  • router0 => router1 :65
  • router0 => router3 => router2 => router1:193
OSPF AD 值預設為 110

router0 => router1

router0 => router3 => router2 => router1

故意關閉 router0 上往 router1 網卡

星期一, 10月 07, 2024

[CCNA] 動態路由 - OSPF - 負載平衡

延續 [CCNA] 動態路由 - OSPF 筆記,從 router0 至 router2 有兩條路由可以到達,可以達到負載平衡效果
show ip route

重新開啟 Packet Tracer 進入 router0 CLI 時發現 OSPF 自動進行同步,Router IP 以 192.168.120.1 (深綠) 和 192.168.140.1 (淺綠) 來表示,是因為該 IP 為 Router 上最大的 IPAddress 

紫框為 router0 至 router2 的 OSPF 路由設定

星期日, 10月 06, 2024

[CCNA] 動態路由 - OSPF

OSPF (Open Shortest Path First),簡單說就是讓 router 自行交換路由,不用每個都手動建立
在 Router 上啟動 OSPF 並進行設定

紀錄 router0、router1、router2 啟用和設定方式,router3 就省略囉,主要設定語法為
router ospf ProcessID => ProcessID 為識別用
network IPAddress wildcard area AreaID => 該範例會設定在同一個 area 內
wildcard 請參考該篇文章 - Wild Card反遮罩

router0 上啟用 OSPF, network 192.168.0.0 0.0.255.255 area 0,代表 192.168 開頭都會啟用 OSPF
router1 啟用 OSPF,network 192.0.0.0 0.255.255.255 area 0,代表 192 開頭都會啟用 OSPF
router2 啟用 OSPF,network 0.0.0.0 255.255.255.255 area 0,代表全部 IP 都會啟用 OSPF 
當 router OSPF 啟動後,每個 router 會出現同步訊息,Full 代表已經交換完,之後就是定期發出 Hello 封包確認 router 路由設定是否有異動
show ip route

四個 router 都啟用 OSPF 後,在 router0 上確認 OSPF 路由
show ip protocols

該語法可以查詢 Router 上 OSPF 設定,下圖以 router0 為例紀錄
Cient 端 PC 進行測試 OSPF 設定檢查
 

星期二, 10月 01, 2024

[SSRS] 雙面列印

被告知要製作郵件報表 (不知道這樣稱呼適不適當就是),一張 A4 紙兩頁都會有文字說明
  • 第一頁 (正面):為郵寄相關地址
  • 第二頁 (反面):我方提供給對方確認內容
第一頁三摺就可以寄出去,對方確認後反摺可以寄回來

透過雙面列印這想法完成該需求,重點如下
  • 報表來源
  • 資料成對
  • 識別第一頁、第二頁
以下會針對這三點來筆記

報表來源

報表來源是以 class 為主,所以該 class 會同時有第一頁和第二頁 property,用個 super class 承載全部資料,且該 class 必須自訂 PageNumber 來判斷是第一頁、第二頁
namespace PageNumberVisible
{
    public class ReportModel
    {
        public int PageNumber { get; set; }

        // 第一頁 Property

        // 第二頁 Property
    }
}

資料成對

是在 C# WinForm 上搭配 ReportViewer 來進行列印,所以在 AP 端要把資料變成兩筆,並同時給 PageNumber 值
using Microsoft.Reporting.WinForms;
using System;
using System.Collections.Generic;
using System.Windows.Forms;

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

        private void Form1_Load(object sender, EventArgs e)
        {
            // 理論上資料來源變成兩筆並要特別注意資料排序,筆記就簡易紀錄就好
            List<ReportModel> reportSource = new List<ReportModel>();
            reportSource.Add(new ReportModel() { PageNumber = 1 });
            reportSource.Add(new ReportModel() { PageNumber = 2 });
            reportSource.Add(new ReportModel() { PageNumber = 1 });
            reportSource.Add(new ReportModel() { PageNumber = 2 });

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

識別第一頁、第二頁

控件可見性屬性內使用 VB.NET MOD 函數搭配頁碼來判斷是第一頁、第二頁
  • 頁首、頁尾:使用 SSRS 內建欄位 PageNumber 
  • 報表主體:使用 class 內自訂的 PageNumber 欄位
另外不是每個控件 GUI 內都會有可見性可以進行設定,沒有的話,可以在屬性視窗內找到 Hide,英文和中文翻譯剛好相反詞

內建欄位 PageNumber 只能應用在頁首、頁尾 


線條控件就沒有 GUI 可見性,只能在屬性視窗內使用 Hide 設定
報表呈現設計

在報表頁首和實體內放三個控見示意,左側控件是顯示頁碼,右側控件則是根據頁碼來決定是否顯示

報表執行

第三頁 (正面)
第四頁 (反面)

輸出時選擇印表機雙面列印,兩兩一份就可以完成該需求