星期三, 1月 17, 2024

[Word] 超連結

同事來詢問 Word 內如何使用超連結,以下為操作步驟

Step1:插入 => 連結


Step2:輸入 [要顯示的文字] 並在 [網址] 內輸入網址


Step3:以上兩步驟即完成超連結設定,如果要在 Word 內點擊並進行網頁連結,要按 Ctrl + 滑鼠左鍵


萬一已經有文字說明的話,可以直接按右鍵,選單內有 [連結] 選項


星期日, 1月 14, 2024

[.NET] FileSystemWatcher

在 [C#] 檔案系統監視 筆記發現當初寫的還蠻簡易的,該篇從官方文件重新了解 FileSytemWatcher,這次主要參考這兩篇官方文章內範例
FileSystemWatcher 在 .NET Framework 和 .NET 基本上是通用,Core 3.0 版本時增加FileSystemWatcher.Filters 屬性而已

C# Code
using System.Text;

namespace FileSystemWatcherSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            using var watcher = new FileSystemWatcher();

            // 預設值為空值
            watcher.Path = @"D:\FileSystemWatcherDir";

            // 預設值為 NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName
            watcher.NotifyFilter = NotifyFilters.Attributes
                                 | NotifyFilters.CreationTime
                                 | NotifyFilters.DirectoryName
                                 | NotifyFilters.FileName
                                 | NotifyFilters.LastAccess
                                 | NotifyFilters.LastWrite
                                 | NotifyFilters.Security
                                 | NotifyFilters.Size;

            // 預設值為 false
            watcher.EnableRaisingEvents = true;

            // 預設值為 "*.*" (監看全部檔案)
            watcher.Filter = "*.txt";

            // 預設值為 false
            watcher.IncludeSubdirectories = true;

            // 預設值為 8,192 (8k)
            watcher.InternalBufferSize = 8192;

            // Changed、Created 和 Deleted 皆為 FileSystemEventArgs,可以共用 
            watcher.Changed += OnChanged;
            watcher.Created += OnChanged;
            watcher.Deleted += OnChanged;

            // Renamed 為 RenamedEventArgs
            watcher.Renamed += OnRenamed;

            // Error 為 ErrorEventArgs
            watcher.Error += OnError;

            Console.WriteLine("Press enter to exit.");
            Console.ReadLine();
        }

        private static void OnChanged(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine($"{e.ChangeType}: {e.FullPath}");
        }

        private static void OnRenamed(object sender, RenamedEventArgs e)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine($"{e.ChangeType}:");
            sb.AppendLine($"    Old: {e.OldFullPath}");
            sb.AppendLine($"    New: {e.FullPath}");
            Console.WriteLine(sb.ToString());
        }

        private static void OnError(object sender, ErrorEventArgs e)
        {
            PrintException(e.GetException());
        }

        private static void PrintException(Exception? ex)
        {
            if (ex == null)
                return;

            StringBuilder sb = new StringBuilder();

            //  This can happen if Windows is reporting many file system events quickly
            //  and internal buffer of the  FileSystemWatcher is not large enough to handle this
            //  rate of events. The InternalBufferOverflowException error informs the application
            //  that some of the file system events are being lost.
            if (ex.GetType() == typeof(InternalBufferOverflowException))
                sb.AppendLine("Error 事件觸發,Internal Buffer Overflow 發生");
            else
                sb.AppendLine("Error 事件觸發,錯誤訊息如下");

            sb.AppendLine(ex.ToString());
            Console.WriteLine(sb.ToString());

            PrintException(ex.InnerException);
        }
    }
}

Path Property

指定監看的資料夾,該資料夾即使更名,FileSystemWatcher 仍然會繼續監看,FileSystemWatcher 是認 handle 來監看資料夾,而非資料夾名稱,詳見該段官方說明
When a directory is renamed, the FileSystemWatcher automatically reattaches itself to the newly renamed item. For example, if you set the Path property to "C:\My Documents" and then manually rename the directory to "C:\Your Documents", the component continues listening for change notifications on the newly renamed directory. However, when you ask for the Path property, it contains the old path. This happens because the component determines what directory watches based on the handle, rather than the name of the directory. Renaming does not affect the handle. So, if you destroy the component, and then recreate it without updating the Path property, your application will fail because the directory no longer exists.
如想監看特定檔案,請搭配  Filter Property 來達到

EnableRaisingEvents Property

在 VS2005 該預設值為 true,之後的 VS 版本預設為 false,啟用該設定並設定 Path Property,FileSystemWatcher 才會開始監看資料夾內的檔案變化

InternalBufferSize Property

預設值為 8,192 (8k),官方建議是設定在 4,096 - 65,536 (4k - 64k),要避免 overflow 發生,最好是明確定義下述三個屬性
  • IncludeSubdirectories:預設值為 false,不監看子目錄
  • NotifyFilter:預設值為 LastWrite | FileName | DirectoryName
  • Filter:預設為 *.* 監看全部檔案,最好能指定特定檔案或是檔案類型,EX:txt 檔案 
下述為官方詳細說明
You can set the buffer to 4 KB or larger, but it must not exceed 64 KB. If you try to set the InternalBufferSize property to less than 4096 bytes, your value is discarded and the InternalBufferSize property is set to 4096 bytes. For best performance, use a multiple of 4 KB on Intel-based computers. 

The system notifies the component of file changes, and it stores those changes in a buffer the component creates and passes to the APIs. Each event can use up to 16 bytes of memory, not including the file name. If there are many changes in a short time, the buffer can overflow. This causes the component to lose track of changes in the directory, and it will only provide blanket notification. Increasing the size of the buffer can prevent missing file system change events. However, increasing buffer size is expensive, because it comes from non-paged memory that cannot be swapped out to disk, so keep the buffer as small as possible. To avoid a buffer overflow, use the NotifyFilter and IncludeSubdirectories properties to filter out unwanted change notifications.

Error 事件

印象中使用 FileSystemWatcher 最好使用 try catch 來處理 Exception 發生的建議,但發現 FileSystemWatcher 本身就有 Error Event 可以使用,官方說明有提到當無法繼續執行監看檔案變更或在內部緩衝區溢位 (Internal Buffer Overflows) 時就會被觸發,不知道哪來的印象

Change 事件

監看的檔案或資料夾,發生下述情況變化會觸發 Change Event
  • 系統屬性 (System Attributes)
  • 上次寫入時間 (Last Write Time)
  • 上次存取時間 ( Last Access Time)
  • 安全性許可權 (Security Permissions)
  • 檔案或資料夾的大小 (Size)
檔案操作常常會伴隨多個事件被觸發,以檔案從資料夾搬移至另一個資料夾為例討論,可能會有多次的 Change Event、Created Event 、Deleted Event 觸發

Renamed 事件

FileSystemWatecher.Path 設定的監看資料夾更名並不會觸發 Renamed Event,監看資料夾內的檔案更名才會觸發

五大事件 EventArgs 整理
  • FileSystemEventArgs:Changed Event、Created Event、Deleted Event
  • RenamedEventArgs:Renamed Event
  • ErrorEventArgs:Error Event

執行觀察監看結果

觸發多個事件觀察:把檔案直接複製進資料夾
監看資料夾更名仍然可以運作:在資料夾內新增檔案後並更名,並更改監看資料夾名稱 (橘線代表),最後修改另一檔案內容,FileSystemWatcher 仍然有回應
觸發 Error Event:直接把資料夾剪下貼到其他地方去,故意放一個檔案在資料夾內,可以看見有 Changed 和 Deleted Event 被觸發,之後 Error Event 觸發並回報錯誤訊息

星期三, 1月 10, 2024

[Shopify] DMARC 紀錄

收到 Shopify 官方來信,告知要趕快把 DMARC 加入 DNS 驗證

後續了解 SPF、DKIM 和 DMARC 彼此關係和用途後,發現 SPF 和 DKIM 就已經設定完成,依循該 Google 文件 - 新增 DMARC 記錄 去 Hinet 內把 DMARC 加進去,30 分鐘後就生效啟用


透過 Google Admin Toolbox - Dig 服務,就可以查到 DMARC 生效啟用


Hinet 設定 txt 小插曲,新增 txt 紀錄時並沒有自行加上雙引號,但儲存後看起來是有自行加上雙引號,所以使用 Dig 去查 DMARC 時,answer 只有顯示 "v=DMARC1;",後面似乎被截斷的感覺,重新設定時才發現 Hinet 網頁下方文字說明,要自行加上雙引號

星期六, 1月 06, 2024

[EFCore] Migration - Foreign Key

延續 [EFCore] Migration 內容,要使用 Migration 來建立 FK,商業邏輯會是衣服、衣服規格和狀態,衣服本身會有狀態 (EX:停售),衣服尺寸也需要狀態 (EX:停售、缺貨),ER 圖如下
閱讀 Data Annotations - ForeignKey Attribute in EF 6 & EF Core 時發現,Model 怎麼設計會影響定義 FK 方式,該筆記基本就是把 FK 建出來,沒有特別深究優劣

建立 Model 

Model:Clothes、ClothesDetail 和 Status
FK:
  • Clothes - Status:1 對 1
  • ClothesDetail - Status:1 對 1
  • Clothes - ClothesDetail:1 對多

Mode Code 如下
using System.ComponentModel.DataAnnotations.Schema;

namespace EFCoreMigration.Models
{
    public class Clothes
    {
        public int ClothesID { get; set; }

        public string ClothesName { get; set; }

        public int StatusID { get; set; }

        [ForeignKey("StatusID")]
        public Status Status { get; set; }

        public ICollection<ClothesDetail> ClothesDetail { get; set; }
    }
    
    public class ClothesDetail
    {
        public int ClothesDetailID { get; set; }

        public int ClothesID { get; set; }

        public int StatusID { get; set; }

        [ForeignKey("StatusID")]
        public Status Status { get; set; }

        public Clothes Clothes { get; set; }
    }
    
    public class Status
    {
        public int StatusID { get; set; }

        public string StatusName { get; set; }
    }    
}

以 [1 對 1] 的 Clothes 和 Status 來說,Clothes model 內有 StatusID Property 和 Status Class 屬性,透過 Data Annotations ForeignKeyAttribute 來進行 FK 設定
namespace EFCoreMigration.Models
{
    public class Clothes
    {
        public int ClothesID { get; set; }

        public string ClothesName { get; set; }

        public int StatusID { get; set; }

        [ForeignKey("StatusID")]
        public Status Status { get; set; }
    }
}


以 [1 對多] 的 Clothes 和 ClothesDetail 來說,Clothes model 內有 ICollection 的 ClothesDetail,ClothesDetail model 內有 Clothes Property,透過 Fluent API 來指定 FK
using System.ComponentModel.DataAnnotations.Schema;

namespace EFCoreMigration.Models
{
    public class Clothes
    {
        public int ClothesID { get; set; }

        public string ClothesName { get; set; }

        public ICollection<ClothesDetail> ClothesDetail { get; set; }
    }
    
    public class ClothesDetail
    {
        public int ClothesDetailID { get; set; }

        public int ClothesID { get; set; }

        public Clothes Clothes { get; set; }
    }
    
    public class MigrationDbContext : DbContext
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Clothes>()
                .HasMany(c => c.ClothesDetail)
                .WithOne(cd => cd.Clothes)
                // 關閉 Delete Cascade
                .OnDelete(DeleteBehavior.NoAction);

            base.OnModelCreating(modelBuilder);
        }
    }

}

Migration 設定檔案內容

為節省版面,Migration 設定檔案內容就以 ClothesDetail 為主而已,想記錄重點有下述兩點
  • migration 時 FK 欄位會自動建立 Index
  • Data Annotations 設定的 FK,預設會開啟 Delete Cascade 功能,嘗試透過 Data Annotations 關閉 Delete Cascade,但在 EF Core OnDelete 內提及 Data Annotations 沒有關閉 Delete Cascade 功能,網路上大多也都是透過 Fluent API 來進行關閉
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EFCoreMigration.Migrations
{
    /// <inheritdoc />
    public partial class AddFKLab : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "ClothesDetail",
                columns: table => new
                {
                    ClothesDetailID = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    ClothesID = table.Column<int>(type: "int", nullable: false),
                    StatusID = table.Column<int>(type: "int", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ClothesDetail", x => x.ClothesDetailID);
                    table.ForeignKey(
                        name: "FK_ClothesDetail_Clothes_ClothesID",
                        column: x => x.ClothesID,
                        principalTable: "Clothes",
                        principalColumn: "ClothesID");
                    table.ForeignKey(
                        name: "FK_ClothesDetail_Status_StatusID",
                        column: x => x.StatusID,
                        principalTable: "Status",
                        principalColumn: "StatusID",
                        // 開啟 Cascade Delete 功能
                        onDelete: ReferentialAction.Cascade);
                });

            // 建立 Index
            migrationBuilder.CreateIndex(
                name: "IX_ClothesDetail_StatusID",
                table: "ClothesDetail",
                column: "StatusID");
        }
    }
}

星期五, 1月 05, 2024

[EFCore] Migration

在該 [EFCore] 在 Console 專案上安裝設定 內筆記 Scaffold-DbContext 使用,該篇記錄 EF Core 8 使用 Code First Migration 操作

套件安裝

使用 EFCore 相關 nuget 套件,分別為
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tool
安裝 Microsoft.EntityFrameworkCore.Tool 才能使用 Migration 相關 API,另外 Microsoft.EntityFrameworkCore.Tool 安裝時就內含 Microsoft.EntityFrameworkCore.Design,不需要特別去安裝



建立 DbContext 和 Model 

資料型態有兩種指定方式分別為 Data Annotations 和 FluentAPI,實務上統一一種方式來進行指定就行,該筆記練習會同時使用

手動建立 Blog Model,並使用 Data Annotations 來指定資料型別
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EFCoreMigration.Models
{
    public class Blog
    {
        // 透過 Data Annotations 來設定 BlogID 和 PostDate
        
        [Key]
        public int BlogID { get; set; }

        public string BlogName { get; set; }

        [Required]
        [Column(TypeName = "date")]
        public DateTime PostDate { get; set; }
    }
}
手動建立 MigrationDbContext,MigrationDbContext 繼承 DbContext,並在 OnModelCreating() 內使用 Fluent API 指定 Blog.BlogName 資料型態,連線字串部分理論上不該寫死在 Code 內,該筆記重點放在熟悉 Migration 使用,就不理會它囉
using Microsoft.EntityFrameworkCore;

namespace EFCoreMigration.Models
{
    public class MigrationDbContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=.;Database=EFCoreMigration;Trusted_Connection=True;TrustServerCertificate=true;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // 透過 FluentAPI 來設定資料型態
            modelBuilder.Entity<Blog>()
                .Property(p => p.BlogName)
                .HasColumnType("nvarchar")
                .HasMaxLength(100);

            base.OnModelCreating(modelBuilder);
        }
    }
}

Migration

Migration API 語法
  • Add-Migration:新增 migration 設定
  • update-database:根據 migration 設定更新資料庫
在套件管理器控制台內輸入下述語法後,專案內就會出現 Migrations 資料夾
Add-Migration InitDB

Migrations 資料夾內會有 DB Snapshot 和 InitDB 檔案


打開 InitDB Migration 檔案可以看見相關內容
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EFCoreMigration.Migrations
{
    /// <inheritdoc />
    public partial class InitDB : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Blogs",
                columns: table => new
                {
                    BlogID = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    BlogName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
                    PostDate = table.Column<DateTime>(type: "date", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Blogs", x => x.BlogID);
                });
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Blogs");
        }
    }
}
實際執行 migations 檔案至 DB  內
update-database
在 SSMS 內可以看見 EFCoreMigration DB 和 Blog Table 被建立出來啦
更改 Model Property

在 Blog Model 內新增 CreatedTimestamp Property
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EFCoreMigration.Models
{
    public class Blog
    {
        [Key]
        public int BlogID { get; set; }

        public string BlogName { get; set; }

        [Required]
        [Column(TypeName = "date")]
        public DateTime PostDate { get; set; }

        // 新增該 Property
        public DateTime CreatedTimestamp { get; set; }

    }
}
進行 Migration 操作並更新至 DB 去
Add-Migration AddCreatedTimestamp
update-database
Migration 設定檔內容
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EFCoreMigration.Migrations
{
    /// <inheritdoc />
    public partial class AddCreatedTimestamp : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<DateTime>(
                name: "CreatedTimestamp",
                table: "Blogs",
                type: "datetime2",
                nullable: false,
                defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropColumn(
                name: "CreatedTimestamp",
                table: "Blogs");
        }
    }
}
在 SSMS 內就可以看見 CreateTimestamp 欄位
以上簡易紀錄 Code First Migration 流程

星期三, 1月 03, 2024

[C#] Header 背景顏色

每隔一段時間遇上要變更 DataGridView ColumnHeader 背景顏色時就會鬼打牆一段時間,筆記設定方式 

.NET Framework 或 .NET 8 程式啟動時,預設都開有該設定
Application.EnableVisualStyles();
而 DataGridView 內的 Header 也有該設定,要把該 Visual Styles 設定關閉後,才能變更 ColumnHeader、RowHeader 背景顏色
namespace CellHeaderColor
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            // 關閉 Headers Visual Styles
            dataGridView1.EnableHeadersVisualStyles = false;

            // 設定 ColumnHeader 預設背景顏色
            dataGridView1.ColumnHeadersDefaultCellStyle.BackColor = Color.LightGray;

            // 設定 RowHeader 預設背景顏色
            dataGridView1.RowHeadersDefaultCellStyle.BackColor = Color.Yellow;   

            // 設定欄位背景顏色 1
            dataGridView1.Columns[Column2.Index].HeaderCell.Style.BackColor = Color.LightBlue;

            // 設定欄位背景顏色 2
            Column4.HeaderCell.Style.BackColor = Color.LightPink;
        }
    }
}

星期二, 1月 02, 2024

[SQL] 產品組合

在 論壇 上看見的問題,需求是要組合每個 menu 內產品,該 menu 內假如有三個產品的話,產品組合就只要三個產品的組合,兩個產品的組合就過濾掉不需要出現,資料來源和期待結果如下圖
透過 CTE 遞迴來處理產品組合
CREATE TABLE Temp (
	menu smallint , 
	option_type char(1) , 
	option_val char(1));

INSERT INTO Temp VALUES
(1, 'A', '1') , (1, 'A', '2'),
(2, 'A', '1') , (2, 'A', '2'), (2, 'B', '1'), (2, 'B', '2'),
(3, 'A', '1') , (3, 'A', '2') , (3, 'B', '1') , (3, 'B', '2') , (3, 'C', '1') , (3, 'C', '2');

; 
WITH CTE AS
(
	SELECT 
		menu , 
		option_type , 
		option_val , 
		CAST(option_type + option_val AS varchar(8000)) AS Memo ,
		CAST(1 as int) AS Stat
	FROM Temp
	UNION ALL
	SELECT 
		T2.menu , 
		T2.option_type , 
		T2.option_val ,
		T1.Memo + '_' + T2.option_type + T2.option_val ,
		CAST(Stat + 1 as int) AS Stat
	FROM Temp AS T2
		JOIN CTE AS T1 ON T2.menu = T1.menu 
						AND T2.option_type > T1.option_type
)
SELECT 
	F1.* 
FROM CTE AS F1
	JOIN
		(
			SELECT 
				menu , 
				COUNT(DISTINCT option_type) AS Stat
			FROM Temp
			GROUP BY menu 
		) AS F2 ON F1.menu = F2.menu 
					AND F1.stat = F2.stat
ORDER BY menu , memo