星期五, 7月 29, 2022

[C#] Stack

對於 Stack 認知,只停留在後進先出 (LIFO) 觀念,剛好在社群討論上時有提到,根據官方文章 -  Stack<T> 來理解基礎使用並筆記
void Main()
{
	Stack<string> stack1 = new Stack<string>();

	if (stack1.TryPop(out string result) == false)
		"stack1 目前為空值,無法取值".Dump("Stack.TryPop() 使用");

	stack1.Push("one");
	stack1.Push("two");
	stack1.Push("three");
	stack1.Push("four");
	stack1.Push("five");

	stack1.Dump("顯示 stack 內元素,後進先出 (LIFO) 特色");

	stack1.Pop().Dump("Pop:第一次取值");
	stack1.Peek().Dump("Peek:下一次取值的資料");
	stack1.Pop().Dump("Pop:第二次取值");

	// 建構子接受參數為 IEnumerable<T>,透過它來建立 Stack2
	// stack1 經過上述兩次取值後,只剩下三筆資料 - three,two,one,
	// 因為後進先出 (LIFO) 特色,dump 出來後就又變成正向 one,two,three
	Stack<string> stack2 = new Stack<string>(stack1.ToArray());

	stack2.Dump("stack2 內容");
	@$"stack2.Contains(""four"") = {stack2.Contains("four")}".Dump("是否包含 four");

	// Create an array twice the size of the stack and copy the
	// elements of the stack, starting at the middle of the array.
	string[] array3 = new string[stack1.Count * 2];
	stack1.CopyTo(array3, stack1.Count);
	Stack<string> stack3 = new Stack<string>(array3);
	stack3.Dump("CopyTo():stack3 內容,包含 nulls");

	stack2.Clear();
	stack2.Count.Dump("使用 Clear() 清空 stakc2 並列出 stack2.Count 確認");
}
執行結果

[C#] Stack

星期三, 7月 27, 2022

[LINQ] Single、SingleOrDefault

以前對於 SingleOfDefault() 的認知是都不會拋出 Exception,直到踩到坑才發現,原來是空集合時會回傳預設值,萬一找出超過一個以上的資料,還是會拋出 Exception,重新認識 Single()、SingleOrDefault() 並筆記

SingleOrDefault 官方文件上說明

四種多載超過一個情況下都會拋出 Exception 的

[LINQ] Single、SingleOrDefault-1


簡易範例

用 LinqPad 來筆記語法
void Main()
{
	string[] fruits = { "apple", "banana", "mango", "orange", "passionfruit", "grape" };
	string[] emptyFruits = {};
	string defaultEmptyMessage = "no more fruits";
	
	"---------- Single 範例".Dump();
	
	fruits.Single(fruit => fruit == "apple").Dump("單一水果 apple 符合條件");
	
	try
	{	        
		emptyFruits.Single();
	}
	catch (InvalidOperationException ex)
	{
		ex.Message.Dump("空值拋出 InvalidOperationException");
	}
	
	emptyFruits.DefaultIfEmpty(defaultEmptyMessage).Single().Dump("使用 DefualtIfEmpty 避免 exception 發生");
	
	try
	{
		fruits.Single(fruit => fruit.Length > 15);
	}
	catch (InvalidOperationException ex)
	{
		ex.Message.Dump("沒有任何水果字母長度大於 15,拋出 InvalidOperationException");
	}

	"---------- SingleOrDefault 範例".Dump();

	fruits.SingleOrDefault(fruit => fruit == "orange").Dump("單一水果 orange 符合條件");

	emptyFruits.SingleOrDefault().Dump("空值,回傳預設值");
	
	emptyFruits.SingleOrDefault(defaultEmptyMessage).Dump("空值,回傳指定預設值");
	
	fruits.SingleOrDefault(fruit => fruit.Length > 10).Dump("長度大於 10 的水果");

	fruits.SingleOrDefault(fruit => fruit.Length > 15 , defaultEmptyMessage).Dump("沒有長度大於 15 的水果,回傳指定預設值");

	try
	{
		fruits.SingleOrDefault();
	}
	catch (InvalidOperationException ex)
	{
		ex.Message.Dump("超過一個水果符合條件,拋出 InvalidOperationException");
	}
}

Single 範例執行結果

[LINQ] Single、SingleOrDefault-2

SingleOrDefault 範例執行結果

[LINQ] Single、SingleOrDefault-3

星期四, 7月 21, 2022

[C#] DataGridView - Mouse Click

公司系統慣例是點選 DataGridView 非 DisplayedRow 區域新增空白資料,在 VFP GridView Mouse Click 相關事件,在 DisplayedRow 內是不會觸發,非 DisplayedRow 才會,而 C# WinForm 則是都會。

該差異導致移轉新系統後,使用者操作上不順暢,常常莫名就會新增資料至 DataGridView 內去,需要手動去刪除空白資料,就是會要求儲存時自動把空白資料刪除,特地去找出解法,讓 C# Mouse Click 事件只會在非 DisplayedRow 上觸發,減少這類無意義 feedback

DisplayedRow 名詞介紹

是從 C# DataGridView 看來,直接看圖較直白
  • 綠框為 DisplayRow
  • 藍框在官方文件上查不到專有名稱,就稱呼非 DisplayedRow 區域或空白區吧
[C#] DataGridView - Mouse Click-1


DataGridView 上顯示的資料筆數,透過 includePartialRow 參數來決定是否抓取顯示不完整的 Row,以下圖為例,false 是抓到兩筆,true 則為三筆

[C#] DataGridView - Mouse Click-2


抓取 DataGridView 上顯示第一筆 Row 的 RowInde,以下圖為例,會抓到編號 5 資料的 RowIndex

[C#] DataGridView - Mouse Click-3


自訂 DataGridView 

該筆記是在 MouseDown 事件並建立 InsertNewDataRow Event 來進行改寫,實務上就根據需求,看要在哪個 Mouse Click 事件內處理,也可以把整個 Mouse Click 事件都覆寫掉,那也就不需要特別去建立事件來觸發

該寫法個重點
  • 點擊在 ColumnHeader 時不觸發
  • 點擊在 RowHeader 時不觸發
  • 點擊在非 DisplayedRow 區域時觸發
  • ScrollBar 出現時,ScrollBar 必須在最下方時觸發
using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace UCDataGridViewMouseDown
{
    public class UCDataGridView : DataGridView
    {
        [Description("點擊 DataGridView 非 DisplayedRow 區域時觸發")]
        public event EventHandler InsertNewDataRow;

        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);

            int displayRowCount = DisplayedRowCount(false);
            int rowHeightIncludeHeaderHeight = ColumnHeadersHeight + (RowTemplate.Height * displayRowCount);
            
            bool isClickInRowHeader = RowHeadersWidth > e.X;            
            bool isClickInColumnHeader = ColumnHeadersHeight >= e.Y;
            bool isClickInNonDisplayedRow = rowHeightIncludeHeaderHeight < e.Y;

            if (isClickInRowHeader ||
                isClickInColumnHeader)
                return;

            VScrollBar vScrollBar = Controls.OfType<VScrollBar>().First();

            int rowIndexMax = RowCount - 1;
            // DataGridView 畫面上第一筆資料的 RowIndex 加上 畫面上資料筆數
            int indexStat = FirstDisplayedScrollingRowIndex + displayRowCount;
            bool isScrolBottom = indexStat > rowIndexMax;

            if (isClickInNonDisplayedRow && (
                    RowCount == 0 ||
                    vScrollBar.Visible == false ||
                    (vScrollBar.Visible == true && isScrolBottom)))
                    InsertNewDataRow?.Invoke(this, e);
        }
    }
}
把自訂控件拉進 Form 內使用
using System;
using System.Data;
using System.Windows.Forms;

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

            dataGridView1.AllowUserToAddRows = false;
            dataGridView1.AutoGenerateColumns = false;
            dataGridView1.InsertNewDataRow += DataGridView1_InsertNewDataRow;

            DataTable dt = new DataTable();
            dt.Columns.Add("ID", typeof(int));
            dt.Columns.Add("Name", typeof(string));
            dt.Rows.Add(1, "姓名一");
            dt.Rows.Add(2, "姓名二");
            dt.Rows.Add(3, "姓名三");
            dt.Rows.Add(4, "姓名四");
            dt.Rows.Add(5, "姓名五");
            dt.Rows.Add(6, "姓名六");
            dataGridView1.DataSource = dt;
        }

        private void DataGridView1_InsertNewDataRow(object sender, EventArgs e)
        {
            MessageBox.Show(nameof(UCDataGridView.InsertNewDataRow));
        }
    }
}
  • 參考資料
  • 網路討論 12

星期一, 7月 11, 2022

[C#] DataGridView - CalendarColumn

根據官方文章 - How to: Host Controls in Windows Forms DataGridView Cells 來學習如何把 DateTimePicker 放進 DataGridViewColumn 內,基本上把文章內的 Code 直接 copy 出來就可以直接建立出 CalendarColumn 並使用,該內容重點分別為
  • 實作 IDataGridViewEditingControl  interface
  • DataGridViewCell
  • DataGridViewColumn

IDataGridViewEditingControl

建立 CalendarEditingControl 繼承 DateTimePicker 並實作IDataGridViewEditingControl

IDataGridViewEditingControl.EditingControlWantsInput 重點文字
The EditingControlWantsInputKey method is called by the DataGridView. The DataGridView will pass in true for dataGridViewWantsInputKey when it can process the keyData. If the editing control can let the DataGridView handle the keyData, EditingControlWantsInputKey should return false when dataGridViewWantsInputKey is true. Other implementations of EditingControlWantsInputKey may ignore a dataGridViewWantsInputKey value of true and handle the keyData in the editing control.
Some situations require that cell contents reposition when the value changes. For example, cell contents may need to reposition when a cell wraps text and the contents become larger.

星期日, 7月 03, 2022

[C#] ParentControlDesigner

一直以為 UserControl 就必須事先把控件設計好,整個 UserControl 拉進來使用就是,在閱讀官方文件 - Use Visual C# to make a UserControl object act as a control container design-time 後,發現原來在設計階段 (design mode) 下可以讓 UserControl 變成父容器,能把控件拖曳進去變成子控件

ParentControlDesigner 是從 ControlDesigner 繼承而來,所以除了讓 UserControl 在設計階段變成容器外,也同樣可以設計智能標籤來使用喔

設計智能標籤內容

只設計一個背景顏色屬性來使用
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;

namespace UCParentControlDesigner
{
    public class UCContainerActionList : DesignerActionList
    {
        private UCContainer _ucContainer;

        public UCContainerActionList(IComponent component) : base(component)
        {
            _ucContainer = component as UCContainer;
        }

        private PropertyDescriptor GetPropertyByName(string propName)
        {
            PropertyDescriptor prop = TypeDescriptor.GetProperties(_ucContainer)[propName];
            if (null == prop)
                throw new ArgumentException($"UCContainer 屬性:{propName} 沒有找到");
            else
                return prop;
        }

        public Color BackColor
        {
            get
            {
                return _ucContainer.BackColor;
            }
            set
            {
                GetPropertyByName(nameof(BackColor)).SetValue(_ucContainer, value);
            }
        }

        public override DesignerActionItemCollection GetSortedActionItems()
        {
            DesignerActionItemCollection items = new DesignerActionItemCollection();

            items.Add(new DesignerActionPropertyItem(
                 nameof(BackColor),
                 "背景顏色",
                 "外觀",
                 "選擇背景顏色"));

            return items;
        }
    }
}

使用 ParentControlDesigner

UCContainerControlDesigner 繼承 ParentControlDesigner,使 UserControl 變成父容器並把上述設計好的智能標籤放進來
using System.ComponentModel.Design;
using System.Windows.Forms.Design;

namespace UCParentControlDesigner
{
    public class UCContainerControlDesigner : ParentControlDesigner
    {
        private DesignerActionListCollection actionLists;

        public override DesignerActionListCollection ActionLists
        {
            get
            {
                if (null == actionLists)
                {
                    actionLists = new DesignerActionListCollection();
                    actionLists.Add(new UCContainerActionList(this.Component));
                }
                return actionLists;
            }
        }
    }
}

設計階段具容器功能的 UserControl

建立 UCContainer UserControl 並使用 UCContainerControlDesigner
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace UCParentControlDesigner
{

    [Designer(typeof(UCContainerControlDesigner))]
    public partial class UCContainer : UserControl
    {
        public UCContainer()
        {
            InitializeComponent();

            // 預設背景顏色為淡灰色
            BackColor = Color.LightGray;
        }
    }
}

設計效果顯示

該 gif 示範把 Button 控件拖曳進 UCContainer 內,並成為 UCContainer 的子控件
背景顏色的智能標籤

星期六, 7月 02, 2022

[C#] DesignerSerializationVisibilityAttribute (續)

接續 [C#] DesignerSerializationVisibilityAttribute 筆記,該篇主要釐清 DesignerSerializationVisibility.Content 和 DesignerSerializationVisibility.Visible 差異,在上一篇範例不容易看出差異,因此該篇特地把 UserControl 內的 TextBox 透過屬性向外開放存取

 
類型說明
Hidden程式碼產生器產生物件內容的程式碼,而非物件本身的程式碼
Visible程式碼產生器會產生物件的程式碼
Content程式碼產生器產生物件內容的程式碼,而非物件本身的程式碼

官方範例說明

加入 TextBoxInUCControl Property 來存取 UserControl 內的 TextBox
using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace UCSerialization
{
    public partial class SerializationDemoControl : UserControl
    {
        private string[] stringsValue = new string[1];

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public string[] Strings
        {
            get
            {
                return this.stringsValue;
            }
            set
            {
                this.stringsValue = value;
                this.textBox1.Text = string.Join(Environment.NewLine, this.stringsValue);
            }
        }

        // 重點
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public TextBox TextBoxInUCControl { get { return textBox1; }}

        public SerializationDemoControl()
        {
            InitializeComponent();

            Padding = new Padding(10);

            textBox1.Multiline = true;
            textBox1.Dock = DockStyle.Fill;
            textBox1.ScrollBars = ScrollBars.Vertical;
            textBox1.ReadOnly = true;
        }
    }
}

序列化觀察

透過改變 TextBoxInUCControl DesignerSerializationVisibility 並拖曳到 Form 上來觀察 Form.Designer 內序列化的 Code,下列 Code 只列出觀察重點而已
  • DesignerSerializationVisibility.Visible (預設值)
只看見 serializationDemoControl 被序列化而已
private void InitializeComponent()
{
    // 
    // serializationDemoControl1
    // 
    this.serializationDemoControl1.Location = new System.Drawing.Point(188, 185);
    this.serializationDemoControl1.Name = "serializationDemoControl1";
    this.serializationDemoControl1.Padding = new System.Windows.Forms.Padding(10);
    this.serializationDemoControl1.Size = new System.Drawing.Size(130, 50);
    this.serializationDemoControl1.Strings = new string[] {null};
    this.serializationDemoControl1.TabIndex = 0;
}
  • DesignerSerializationVisibility.Content
除了 serializationDemoControl 外,還多出 serializationDemoControl.TextBoxInUCControl 相關 Code
private void InitializeComponent()
{
    // 
    // serializationDemoControl1
    // 
    this.serializationDemoControl1.Location = new System.Drawing.Point(141, 140);
    this.serializationDemoControl1.Name = "serializationDemoControl1";
    this.serializationDemoControl1.Padding = new System.Windows.Forms.Padding(10);
    this.serializationDemoControl1.Size = new System.Drawing.Size(130, 50);
    this.serializationDemoControl1.Strings = new string[] {null};
    this.serializationDemoControl1.TabIndex = 0;
    // 
    // 
    // 
    this.serializationDemoControl1.TextBoxInUCControl.Dock = System.Windows.Forms.DockStyle.Fill;
    this.serializationDemoControl1.TextBoxInUCControl.Location = new System.Drawing.Point(10, 10);
    this.serializationDemoControl1.TextBoxInUCControl.Multiline = true;
    this.serializationDemoControl1.TextBoxInUCControl.Name = "textBox1";
    this.serializationDemoControl1.TextBoxInUCControl.ReadOnly = true;
    this.serializationDemoControl1.TextBoxInUCControl.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
    this.serializationDemoControl1.TextBoxInUCControl.Size = new System.Drawing.Size(110, 30);
    this.serializationDemoControl1.TextBoxInUCControl.TabIndex = 0;
}
經過上述測試,就能了解該篇文章 - How to: Serialize Collections of Standard Types with the DesignerSerializationVisibilityAttribute 所說的 
When your custom controls expose a collection as a property, you can serialize the collection at design time. 
 設定為 DesignerSerializationVisibility.Content 後,就能直接在 UserControl 上直接對 TextBox 屬性進行變更並序列化至 Form.Designer,假如是 DesignerSerializationVisibility.Visible 的話,雖然可以在屬性視窗內變更 TextBox 屬性,但因為沒有序列化,重開 Form 後設定效果也就不見囉

[C#] DesignerSerializationVisibilityAttribute (續)-1

星期五, 7月 01, 2022

[C#] TextBoxRenderer

找資料時發現原來有 TextBoxRenderer 內建 Render class,用官方文章範例來了解如何使用,該範例重點
  • 使用 TextBxRender.DrawTextBox 繪製 TextBox
  • 直接新增 ComboBox 並把 TextFormatFlags 放進去當成選項
  • 使用 ComboBox 選擇 TextFormatFlags 來變化、觀察 TextBox 內文字效果 
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;

namespace TextBoxRendererSample
{
    public class CustomTextBox : Control
    {
        private TextFormatFlags _textFlags = TextFormatFlags.EndEllipsis;
        private ComboBox _comboBox = new ComboBox();
        private Rectangle _textBorder = new Rectangle();
        private Rectangle _textRectangle = new Rectangle();

        public CustomTextBox() : base()
        {
            this.Location = new Point(10, 10);
            this.Size = new Size(300, 200);
            this.Font = SystemFonts.IconTitleFont;

            TextBoxInit();
            ComboBoxInit();
        }

        private void TextBoxInit()
        {
            _textBorder.Location = new Point(10, 10);
            _textBorder.Size = new Size(200, 50);
            _textRectangle.Location = new Point(_textBorder.X + 2, _textBorder.Y + 2);
            _textRectangle.Size = new Size(_textBorder.Size.Width - 4, _textBorder.Height - 4);
        }

        private void ComboBoxInit()
        {
            _comboBox.Location = new Point(10, 100);
            _comboBox.Size = new Size(150, 20);
            _comboBox.DropDownStyle = ComboBoxStyle.DropDownList;

            // 把 TextFormatFlags Enum 塞進 ComboBox 內去
            _comboBox.Items.AddRange(Enum.GetNames(typeof(TextFormatFlags)).ToArray());

            _comboBox.SelectedIndex = _comboBox.FindStringExact(nameof(TextFormatFlags.EndEllipsis));
            _comboBox.SelectionChangeCommitted += ComboBox_SelectionChangeCommitted;
            this.Controls.Add(_comboBox);
        }


        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            // TextBoxRenderer class can be used to draw a text box with visual styles.
            if (TextBoxRenderer.IsSupported)
            {
                this.Parent.Text = "CustomTextBox Enabled";
                this.Text = "This is a long sentence that will exceed the text box bounds";

                // 繪製 TextBox 重點
                TextBoxRenderer.DrawTextBox(
                    e.Graphics,
                    _textBorder,
                    this.Text,
                    this.Font,
                    _textRectangle,
                    _textFlags,
                    TextBoxState.Normal);
            }
            else
            {
                this.Parent.Text = "CustomTextBox Disabled";
            }
        }

        private void ComboBox_SelectionChangeCommitted(object sender, EventArgs e)
        {
            if (Enum.TryParse(_comboBox.Text, out TextFormatFlags flags) == false)
                return;

            this._textFlags = flags;
            Invalidate();
        }
    }
}

把自訂控件拖曳至 Form 內執行,下面為執行結果截圖,除了可以看見繪製出來的 TextBox 外,還可以觀察 TextFormatFlags 文字效果

TextFormatFlags.Default 效果:該範例設計就是文字要超過 TextBox

[C#] TextBoxRenderer-2

TextFormatFlags.NoClipping 效果:文字超過 TextBox

[C#] TextBoxRenderer-4

TextFormatFlags.EndEllipsis 效果:文字碰上 TextBox 邊界,會以 ... 來表示截斷文字

   [C#] TextBoxRenderer-1

TextFormatFlags.WordBreak 效果:文字折行

[C#] TextBoxRenderer-3

感覺 TextFormatFlags 文字效果最後變成文章重點,哈