星期二, 8月 23, 2022

[EF] Update 語法觀察

延續 [SQL] Foreign Key - Update 觀念,該篇筆記是來觀察 EF 不同 update 方式,產生的 TSQL update 語法是否會包含值沒有變化欄位,透過 SQL Profile 來觀察 TSQL 語法

EF Update 資料方式
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;

namespace EFUpdate
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Random r = new Random(Guid.NewGuid().GetHashCode());
            int days = r.Next(0, 10);

            AdventureWorks2019 context = new AdventureWorks2019();
            
            var entityUpdate = new PurchaseOrderDetail
            {
                PurchaseOrderID = 1,
                PurchaseOrderDetailID = 1,
                DueDate = new DateTime(2011, 4, 30),
                OrderQty = 4,
                ProductID = 1,
                UnitPrice = 58.26m,
                LineTotal = 201.04m,
                ReceivedQty = 3,
                RejectedQty = 0,
                StockedQty = 3,
                // 修改 ModifiedDate
                ModifiedDate = DateTime.Today.AddDays(days)
            };

            #region 方法一:Attach

            context.PurchaseOrderDetail.Attach(entityUpdate);
            context.Entry(entityUpdate).Property(nameof(PurchaseOrderDetail.ModifiedDate)).IsModified = true;
            context.SaveChanges();

            #endregion

            #region 方法二:AddOrUpdate

            context.PurchaseOrderDetail.AddOrUpdate(entityUpdate);
            context.SaveChanges();

            #endregion

            var entityFromDB = context.PurchaseOrderDetail.Single(p => p.PurchaseOrderID == 1);

            #region 方法三:直接更新

            entityFromDB.ProductID = 1;
            entityFromDB.ModifiedDate = DateTime.Today.AddDays(days);
            context.SaveChanges();

            #endregion

            #region 方法四:設定 EntityState

            entityFromDB.ModifiedDate = DateTime.Today.AddDays(days);
            context.Entry(entityFromDB).State = EntityState.Modified;
            context.SaveChanges();

            #endregion

            #region 方法五:SetValue

            context.Entry(entityFromDB).CurrentValues.SetValues(entityUpdate);
            context.SaveChanges();

            #endregion

        }
    }
}
接續會把各 EF update 語法和 Profile 側錄到的 TSQL update 並列
 
方法一:Attach
context.PurchaseOrderDetail.Attach(entityUpdate);
context.Entry(entityUpdate).Property(nameof(PurchaseOrderDetail.ModifiedDate)).IsModified = true;
context.SaveChanges();
exec sp_executesql N'UPDATE [Purchasing].[PurchaseOrderDetail]
    SET[ModifiedDate] = @0
    WHERE(([PurchaseOrderID] = @1) AND([PurchaseOrderDetailID] = @2))

    SELECT[LineTotal], [StockedQty]
FROM[Purchasing].[PurchaseOrderDetail]
    WHERE @@ROWCOUNT > 0 AND[PurchaseOrderID] = @1 AND[PurchaseOrderDetailID] = @2',
    N'@0 datetime2(7),@1 int,@2 int',@0 = '2022 - 08 - 29 00:00:00',@1 = 1,@2 = 1
方法二:AddOrUpdate
context.PurchaseOrderDetail.AddOrUpdate(entityUpdate);
context.SaveChanges();
exec sp_executesql N'UPDATE [Purchasing].[PurchaseOrderDetail]
    SET[ModifiedDate] = @0
    WHERE(([PurchaseOrderID] = @1) AND([PurchaseOrderDetailID] = @2))

    SELECT[LineTotal], [StockedQty]
FROM[Purchasing].[PurchaseOrderDetail]
    WHERE @@ROWCOUNT > 0 AND[PurchaseOrderID] = @1 AND[PurchaseOrderDetailID] = @2',
    N'@0 datetime2(7),@1 int,@2 int',@0 = '2022 - 08 - 26 00:00:00',@1 = 1,@2 = 1
方法三:直接更新
entityFromDB.ProductID = 1;
entityFromDB.ModifiedDate = DateTime.Today.AddDays(days);
context.SaveChanges();
exec sp_executesql N'UPDATE [Purchasing].[PurchaseOrderDetail]
    SET[ModifiedDate] = @0
    WHERE(([PurchaseOrderID] = @1) AND([PurchaseOrderDetailID] = @2))

    SELECT[LineTotal], [StockedQty]
FROM[Purchasing].[PurchaseOrderDetail]
    WHERE @@ROWCOUNT > 0 AND[PurchaseOrderID] = @1 AND[PurchaseOrderDetailID] = @2',
    N'@0 datetime2(7),@1 int,@2 int',@0 = '2022 - 08 - 24 00:00:00',@1 = 1,@2 = 1
方法四:設定 EntityState
entityFromDB.ModifiedDate = DateTime.Today.AddDays(days);
context.Entry(entityFromDB).State = EntityState.Modified;
context.SaveChanges();
exec sp_executesql N'UPDATE [Purchasing].[PurchaseOrderDetail]
    SET[DueDate] = @0, [OrderQty] = @1, [ProductID] = @2, [UnitPrice] = @3, [ReceivedQty] = @4, [RejectedQty] = @5, [ModifiedDate] = @6
    WHERE(([PurchaseOrderID] = @7) AND([PurchaseOrderDetailID] = @8))

    SELECT[LineTotal], [StockedQty]
FROM[Purchasing].[PurchaseOrderDetail]
    WHERE @@ROWCOUNT > 0 AND[PurchaseOrderID] = @7 AND[PurchaseOrderDetailID] = @8',
    N'@0 datetime2(7),@1 smallint,@2 int,@3 decimal(19, 4),@4 decimal(8, 2),@5 decimal(8, 2),@6 datetime2(7),@7 int,@8 int',@0 = '2011 - 04 - 30 00:00:00',@1 = 4,@2 = 1,@3 = 58.2600,@4 = 3.00,@5 = 0,@6 = '2022 - 08 - 23 00:00:00',@7 = 1,@8 = 1

方法五:SetValue
context.Entry(entityFromDB).CurrentValues.SetValues(entityUpdate);
context.SaveChanges();
exec sp_executesql N'UPDATE [Purchasing].[PurchaseOrderDetail]
    SET[ModifiedDate] = @0
    WHERE(([PurchaseOrderID] = @1) AND([PurchaseOrderDetailID] = @2))

    SELECT[LineTotal], [StockedQty]
FROM[Purchasing].[PurchaseOrderDetail]
    WHERE @@ROWCOUNT > 0 AND[PurchaseOrderID] = @1 AND[PurchaseOrderDetailID] = @2',
    N'@0 datetime2(7),@1 int,@2 int',@0 = '2022 - 08 - 23 00:00:00',@1 = 1,@2 = 1

從上述可以發現,只有 [方法四:設定 EntityState] 會產生全部更新欄位,其他都能自動識別資料本身是否有異動

星期一, 8月 22, 2022

[SQL] Foreign Key - Update

在前兩篇分別就 [SQL] Foreign Key - Delete 和 [SQL] Foreign Key - Insert 討論 Foreign Key,該篇紀錄 Update 的影響

以 AdventureWorks2019 Purchasing.PurchaseOrderDetail 為筆記目標 Table 並停用該 Table Trigger,方便觀察執行計畫 ,測試目的為用相同資料更新該筆資料,藉此觀察執行計畫

更新 DueDate 欄位

更新 PurchaseOrderID = 1 的 DueDate 欄位,2011-04-30 為原本資料
UPDATE Purchasing.PurchaseOrderDetail
SET DueDate = '2011-04-30'
WHERE PurchaseOrderID = 1
從執行計畫中可以看出,雖然資料本身沒有變化,但內部是有進行更新動作,不會因為資料沒有變化就沒有作動

[SQL] Foreign Key - Update-1
更新 ProductID 欄位,該欄位有 Foreign Key 關聯

更新 PurchaseOrderID = 1 的 ProductID = 1 欄位,1 為原本資料,但該欄位有 Foreign Key 關聯至Production.Product Table
UPDATE Purchasing.[PurchaseOrderDetail
SET ProductID = 1
WHERE PurchaseOrderID = 1
從執行計畫可以發現先透過 Clustered Index Seek 找到 ProductID = 1 資料後進行更新,更新後還有進 Production.Product Table 內檢查 ProductID = 1 資料是否存在

[SQL] Foreign Key - Update-2

上圖執行計畫 operator Clustered Index Seek 和 Clustered Index Update 之間有四個 Compute Scalar,為了版面就截切掉,方便閱讀執行計畫

結論是資料進行更新時,該欄位資料若是沒有異動,就不要出現在更新欄位內,若更新欄位存在又有 Foreign Key 相依,會去檢查資料是否存在,資料沒有變化卻又再次進行檢查

星期日, 8月 14, 2022

[C#] ImageListView - 拖放功能

在 ImageListView 套件有內建檔案拖放功能,只要開啟就可以無腦使用,不用再去寫 DragEnter、DragDrop 等相關拖放事件,可以參考之前筆記

C# Code 開啟拖放功能
// 從控件內複製出來
imageListView1.AllowDrag = true;

// 拖曳進去控件
imageListView1.AllowDrop = true;

拖放效果


以系統管理員身分執行



根據 ImageListView 官方提供測試程式,確實只要開啟 AllowDrop 和 AllowDrag 就可以使用檔案拖放功能,但是整理範例筆記時發現,竟然只能使用 drag,drop 無法使用,追究原因發現,原來我開啟 VS 是以系統管理員身分執行,新專案在提高權限後的安全性和 Windows 安全性不一致而無法把檔案拖放進 ImageListView 內,詳見 MS 官方文件說明 - Q: Why Doesn’t Drag-and-Drop work when my Application is Running Elevated? – A: Mandatory Integrity Control and UIPI,該限制跟 ImageListView 無關,即使是 WinForm 標準控件,也會有該限制

要避免安全性不一致導致無法進行 drop,可以參考該文章內的 Code - WinForm中管理员权限下获取拖拽文件路径的解决方案

星期六, 8月 13, 2022

[C#] ImageListView

公司內部 ERP 使用該套件 - ImageListView 來顯示圖檔,根據官方文章來筆記基礎功能,了解有那些功能可以使用,紀錄版本為 V13.8.2

C# Code 紀錄
using Manina.Windows.Forms;
using Manina.Windows.Forms.ImageListViewRenderers;
using System;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;

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

            // 預設為 View.Thumbnails
            imageListView1.View = Manina.Windows.Forms.View.Thumbnails;

            // 預設為 true
            imageListView1.MultiSelect = true;

            // 是否允許檔案重覆,預設為 false
            imageListView1.AllowDuplicateFileNames = false;

            // 是否允許調整相片順序,預設為 true
            imageListView1.AllowItemReorder = true;

            // 調整縮圖大小
            imageListView1.ThumbnailSize = new Size(100,100);

            // CheckBox 相關設定
            // CheckBox 顯示
            imageListView1.ShowCheckBoxes = false;
            // CheckBox 位置
            imageListView1.CheckBoxAlignment = ContentAlignment.TopLeft;

            // 事件
            imageListView1.ItemClick += ImageListView1_ItemClick;

            // 設定 ZoomingRenderer,預設為 DefaultRenderer
            imageListView1.SetRenderer(new ZoomingRenderer(0.5f));
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }

        private void btnLoadPhoto_Click(object sender, EventArgs e)
        {
            var fbd = new FolderBrowserDialog();
            if (fbd.ShowDialog() != DialogResult.OK)
                return;

            DirectoryInfo dir = new DirectoryInfo(fbd.SelectedPath);
            if (dir.Exists == false)
            {
                MessageBox.Show("該路徑異常,無法顯示圖片", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); ;
                return;
            }

            var photos = dir.GetFiles().Select(file => file.FullName).ToArray();

            imageListView1.SuspendLayout();
            imageListView1.Items.AddRange(photos);
            imageListView1.ResumeLayout();

            // 故意把使用者選擇資料夾路徑塞進 Tag 內,單純紀錄有 Tag Property 存在而已
            imageListView1.Tag = fbd.SelectedPath;
        }

        private void btnRemove_Click(object sender, EventArgs e)
        {
            if (imageListView1.SelectedItems.Any() == false)
            {
                MessageBox.Show("請選擇相片", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); ;
                return;
            }

            foreach (ImageListViewItem item in imageListView1.SelectedItems)
            {
                imageListView1.Items.Remove(item);
            }
        }

        private void btnClear_Click(object sender, EventArgs e)
        {
            imageListView1.Items.Clear();
        }

        private void ImageListView1_ItemClick(object sender, ItemClickEventArgs e)
        {
            string itemInfo = string.Empty;
            itemInfo += $"FileName:{e.Item.FileName}" + Environment.NewLine;
            itemInfo += $"FilePath:{e.Item.FilePath}" + Environment.NewLine;
            itemInfo += $"Text:{e.Item.Text}" + Environment.NewLine;
            txtImageIfo.Text = itemInfo;
        }
    }
}

View Model

相片排版呈現方式,預設為 View.Thumbnails 如下圖,相片大小可以透過 ThumbnailSize 來調整,其他 View 效果可以參考官方文件 - View Modes

Renderers

Renderers 為圖檔呈現效果,內部較常用的是 ZoomingRenderer,滑鼠滑過該圖片有放大效果,放大狀態下也會觸發 ItemClick 事件,該範例是把圖片相關資訊顯示在下方的 TextBox 內,官方文件 - Custom Renderers 內有其他 Render 效果可以參考,預設為 DefaultRenderer

星期三, 8月 03, 2022

[C#] CheckedListBox

根據官方文章 - CheckedListBox 的簡易筆記文
using System;
using System.Windows.Forms;

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

            Init();
        }

        private void Init()
        {
            btnAddFruit.Enabled = false;
            btnShow.Enabled = false;
            btnReset.Enabled = false;

            checkedListBox1.Items.Clear();
            listBox1.Items.Clear();

            // 預設新增資料
            string[] myFruit = { "Apples", "Oranges", "Tomato" };
            checkedListBox1.Items.AddRange(myFruit);
            checkedListBox1.Items.Add("Grape", true);

            // 預設為 false,先點選該項目後,才能勾選 CheckBox,開啟後就可以直接點選
            checkedListBox1.CheckOnClick = true;

            // 只支援 None 和 One,不支援 MultiSimple 和 MultiExtended
            //  使用 None 情況下就完全無法點選,One 為預設值
            checkedListBox1.SelectionMode = SelectionMode.One;
        }

        private void txtAddFruit_TextChanged(object sender, EventArgs e)
        {
            // 有輸入文字才能新增
            btnAddFruit.Enabled = string.IsNullOrWhiteSpace(txtAddFruit.Text) == false;
        }

        private void btnAddFruit_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrWhiteSpace(txtAddFruit.Text))
                return;

            if (checkedListBox1.CheckedItems.Contains(txtAddFruit.Text))
                return;

            // 新增後清空
            checkedListBox1.Items.Add(txtAddFruit.Text, CheckState.Checked);
            txtAddFruit.Text = string.Empty;
        }

        private void btnShow_Click(object sender, EventArgs e)
        {
            listBox1.Items.Clear();
            btnReset.Enabled = false;

            for (int i = 0; i < checkedListBox1.CheckedItems.Count; i++)
            {
                listBox1.Items.Add(checkedListBox1.CheckedItems[i]);
            }

            if (listBox1.Items.Count > 0)
                btnReset.Enabled = true;
        }

        private void checkedListBox1_ItemCheck(object sender, ItemCheckEventArgs e)
        {
            if (e.NewValue == CheckState.Unchecked)
            {
                // 點擊當下的 CheckBox = Checked 控件數量,判斷是否為最後一個 Checked
                if (checkedListBox1.CheckedItems.Count == 1)
                {
                    btnShow.Enabled = false;
                }
            }
            else
            {
                btnShow.Enabled = true;
            }
        }

        private void btnReset_Click(object sender, EventArgs e)
        {
            Init();
        }
    }
}

[C#] CheckedListBox