星期二, 6月 28, 2022

GodexRT 730X - 碳帶異常

在上次處理標籤機異常 - GodexRT 730X - 滾軸卡紙,從無法列印到可正常輸出,但有發現輸出品質似乎沒有很好,沒想到兩個禮拜後就又被告知異常啦

異常原因有兩重點
  • 第一點:列印品質異常,線條偏淡
  • 第二點:列印輸出過程中碳帶會斷
線條偏淡,用 Test 字樣來呈現,左側是異常輸出

GodexRT 730X - 碳帶異常-1

碳帶會斷,這次被通報的主因,因為碳帶斷掉後列印就會停止,使用者必須一直處理碳帶

GodexRT 730X - 碳帶異常-3

找不到原因情況下,只能請廠商跑一趟來了解,一打開就發現是碳帶異常,公司內有兩種碳帶,分別為一般和耐磨碳帶,不同碳帶得搭配不同紙張,以該例來說銅板紙必須搭配一般碳帶,但現場更換碳帶時竟然是拿耐磨碳帶來使用,才導致異常發生

兩種碳帶來個大合照,公司內也就這兩種,一般碳帶是消光、耐磨碳帶則是亮光,希望以後不要再發生啦

GodexRT 730X - 碳帶異常-2

星期二, 6月 21, 2022

[C#] 螢幕截圖含滑鼠指標

系統發生 Exception 時除了根據需求紀錄 Exception 外,在 WinForm 領域內常見會把螢幕截圖保留下來方便事後 debug,該篇紀錄螢幕截圖,還包含截圖當下的滑鼠指標並把滑鼠指標放大

範例應用重點內容

螢幕截圖範例

該範例預設應用程式為最大化,有三種截圖方式,分別為
  • 根據主螢幕
  • 根據應用程式所在螢幕
  • 多螢幕截圖
個人環境是筆電外接一個螢幕,有兩個螢幕可以進行螢幕截圖測試喔
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Windows.Forms;

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

            // 預設應用程式是最大化的
            WindowState = FormWindowState.Maximized;
        }

        #region 螢幕截圖

        private void btnPrimaryScreenCapture_Click(object sender, EventArgs e)
        {
            if (IsAppInPrimaryScreen() == false)
            {
                MessageBox.Show("應用程式不在主螢幕上,無法使用該功能進行截圖", this.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            Rectangle screenRectangle = Screen.PrimaryScreen.Bounds;
            Point cursorPoint = Cursor.Position;
            string screenCatureFullName = @"D:\ScreenCapture\PrimaryScreenCapture.png";
            ScreenCaptureWorkFlow(screenRectangle, cursorPoint, screenCatureFullName);
        }

        private void btnAppScreenCapture_Click(object sender, EventArgs e)
        {
            Rectangle screenRectangle = GetAppInScreen().Bounds;
            // 要以應用程式 Form 位置為計算基準
            Point cursorPoint = PointToClient(Cursor.Position);
            string screenCatureFullName = @"D:\ScreenCapture\AppScreenCapture.png";
            ScreenCaptureWorkFlow(screenRectangle, cursorPoint, screenCatureFullName);
        }

        private void btnAllScreenCature_Click(object sender, EventArgs e)
        {
            Rectangle allScreenRectangle = GetAllScreenRectangle();
            Point cursorPoint = Cursor.Position;
            string screenCatureFullName = @"D:\ScreenCapture\AllScreenCapture.png";
            ScreenCaptureWorkFlow(allScreenRectangle, cursorPoint, screenCatureFullName);
        }

        private void ScreenCaptureWorkFlow(Rectangle screenRectangle, Point cursorPoint, string screenCatureFullName)
        {
            try
            {
                Image image = CaptureScreenIncludeCursor(screenRectangle, cursorPoint);
                PutImage(image, screenCatureFullName);
                image.Dispose();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private bool IsAppInPrimaryScreen()
        {
            return GetAppInScreen().Primary;
        }

        private Screen GetAppInScreen()
        {
            return Screen.FromControl(this);
        }

        private Rectangle GetAllScreenRectangle()
        {
            return Screen.AllScreens.Aggregate(
                new Rectangle(),
                (current, screen) => Rectangle.Union(current, screen.Bounds));
        }

        private Image CaptureScreenIncludeCursor(Rectangle screenRectangle, Point cursorPoint)
        {
            // 產生畫布
            Size screenSize = new Size(screenRectangle.Width, screenRectangle.Height);
            Bitmap image = new Bitmap(screenSize.Width, screenSize.Height);

            using (Graphics g = Graphics.FromImage(image))
            {
                // 螢幕截圖
                Point upperLeftSource = screenRectangle.Location;
                Point upperLeftDestination = Point.Empty; // Point.Empty 即為 Point(0, 0)
                g.CopyFromScreen(upperLeftSource, upperLeftDestination, screenSize);

                // 繪製滑鼠
                Rectangle cursorRectangle = GetCursorRectangle(cursorPoint);
                if (cursorRectangle != Rectangle.Empty)
                    Cursor.DrawStretched(g, cursorRectangle);
            }

            return image;
        }

        private Rectangle GetCursorRectangle(Point cursorPoint)
        {
            if (Cursor == Cursors.Hand ||
                Cursor.Current != Cursors.Default)
                return Rectangle.Empty;

            // 把滑鼠放大兩倍
            int rate = 2;
            int cursorWidth = Cursor.Size.Width * rate;
            int cursorHeight = Cursor.Size.Height * rate;
            Size cursorSize = new Size(cursorWidth, cursorHeight);

            return new Rectangle(cursorPoint, cursorSize);
        }

        private void PutImage(Image image, string imageFullName)
        {
            image.Save(imageFullName, ImageFormat.Png);
        }
        #endregion
    }
}

螢幕截圖成果

原本想說要把下面工具列裁掉,後來發現剛好可以用在是別主螢幕還是延伸螢幕,不過為了閱讀方便,還是有把多餘空間裁掉

主螢幕截圖,可以看到那兩倍大滑鼠指標在那

[C#] 螢幕截圖含滑鼠指標-1

延伸螢幕截圖,滑鼠指標也正確繪製在 Button 上

[C#] 螢幕截圖含滑鼠指標-2

延伸螢幕上使用主螢幕截圖,就被防呆下來,記錄如何識別主螢幕語法

[C#] 螢幕截圖含滑鼠指標-3

多螢幕截圖

[C#] 螢幕截圖含滑鼠指標-4


星期一, 6月 20, 2022

[C#] 不拋出 Exception 處理

整理公司內部 NLog 時發現某個工具 Class 內某段 Code,竟然使用 try catch 包起來不拋出 Excpeton,在 catch 內直接用 NLog Context 來接收 Exception,工具 Class 和 NLog 緊緊相依,把該段 Code 修改為變成透過事件傳出去,NLog 在事件內接收 Exception

範例程式

之前在 FB 上也看見類似案例,是使用者自訂控件內不拋出 Exception,但是要有機制通知外部程式,所以該範例會有兩個部分,分別為
  • 使用者自訂控件:自訂事件來處理
  • 自訂 Class :直接使用內建 UnhandledException 事件
就練習寫 Event 就是

使用者自訂控件
using System;
using System.Windows.Forms;

namespace UCExcpetion
{
    public partial class NOExcetionButton : UserControl
    {
        public NOExcetionButton()
        {
            InitializeComponent();
        }

        // Step3-1:自訂 EventHandler Event 寫法
        public event ExceptionEventHandler ExceptionOccur;
        // Step3-2:使用 Action Event 寫法
        // public event Action<object , ExceptionEventArgs> ExceptionOccur;

        // Step4:呼叫事件
        protected virtual void OnExceptionOccur(ExceptionEventArgs e)
        {
            if (ExceptionOccur != null)
                ExceptionOccur(this, e);
        }

        private void btnException_Click(object sender, EventArgs e)
        {
            try
            {
                throw new Exception("自訂控件內部發生 Exception");
            }
            catch (Exception ex)
            {
                // 透過事件向外通知
                ExceptionEventArgs eventArgs = new ExceptionEventArgs() { excpetion = ex };
                OnExceptionOccur(eventArgs);
            }
        }
    }

    // Step2:自訂 EventHandler 或是直接使用 Action 
    public delegate void ExceptionEventHandler(object sender, ExceptionEventArgs e);

    // Step1:自訂 EventArgs
    public class ExceptionEventArgs : EventArgs
    {
        public Exception excpetion { get; set; }
    }
}
自訂 Class 功能
using System;

namespace UCExcpetion
{
    public class ToolClass
    {
        // 使用 .NET 內建事件
        public event UnhandledExceptionEventHandler UnhandledException;

        public void ExceptionCreate()
        {
            try
            {
                throw new Exception("Toolclass 內部產生 Exception");
            }
            catch (Exception ex)
            {
                UnhandledExceptionEventArgs eventArgs = new UnhandledExceptionEventArgs(ex, false);
                // 透過 Invoke 直接呼叫 Event
                UnhandledException?.Invoke(this, eventArgs);
            }
        }

    }
}
呼叫上述兩者
using System;
using System.Windows.Forms;

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

            btnUCException.ExceptionOccur += BtnUCException_ExceptionOccur;
        }

        private void BtnUCException_ExceptionOccur(object sender, ExceptionEventArgs e)
        {
            MessageBox.Show(e.excpetion.Message, this.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
        }

        private void btnToolClassExcpetion_Click(object sender, EventArgs e)
        {
            ToolClass t = new ToolClass();
            // 不去訂閱 UnhandledException Event 的話,ExceptionCreate() 產生的 Exception 是完全無感的 
            t.UnhandledException += (innerSender, args) =>
            {
                Exception ex = args.ExceptionObject as Exception;
                MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
            };
            t.ExceptionCreate();
        }
    }
}

畫面截圖

[C#] 不拋出 Exception 處理

星期日, 6月 19, 2022

[C#] ThreadExceptionDialog

在自家 WinForm 系統內的 Global Error Handle
  • Application.ThreadException
  • AppDomain.CurrentDomain.UnhandledException
內看見使用 ThreadExceptionDialog,用該型態來呈現 Exception 錯誤訊息還蠻工程師的,哈

[C#] ThreadExceptionDialog

星期六, 6月 18, 2022

[C#] NLog - 偵錯

[C#] NLog - DatabaseTarget 測試呼叫 Store Procedure 時,Exception 資料一直沒有 insert 進 DB,一查發現原來 NLog 為了不影響主程式運作,預設是不會拋出任何 Exception

要讓 NLog 可以拋出 Exception 必須設定 ThrowExceptions 屬性、如要輸出錯誤訊息至檔案可以透過 InternalLogger 來做到

InternalLogger.LogFile

InternalLogger.LogFile 是 NLog Exception 的檔案路徑,但並不是全部的 Layout Renderers 都支援,看 官方文章 - Internal Logging 介紹,從 NLog4.6 開始才開始逐漸可以使用

模擬 NLog 內發生錯誤
using NLog;
using NLog.Common;
using NLog.Config;
using NLog.Layouts;
using NLog.Targets;
using System;
using System.Data;

namespace NLogSample
{
    internal class Program
    {
        private static readonly Logger _logger = LogManager.GetCurrentClassLogger();

        static void Main(string[] args)
        {
            ExceptionThrowConfig();

            try
            {
                ExceptionMethod();
            }
            catch (Exception ex)
            {
                _logger.Error(ex);
            }
        }

        private static void ExceptionMethod()
        {
            int numerator = 1;
            int denominator = 0;
            Console.WriteLine(numerator / denominator);
        }

        private static void ExceptionThrowConfig()
        {
            InternalLogger.LogFile = @"d:\Logs\InternalLog.txt";
            InternalLogger.LogLevel = LogLevel.Error;
            LogManager.ThrowExceptions = true;

            DatabaseTarget target = new DatabaseTarget();

            target.ConnectionString = "Data Source=.;Initial Catalog=AdventureWorks2019;Integrated Security=True";

            // 錯誤的呼叫 Store Procedure 方式
            target.CommandType = CommandType.StoredProcedure;
            target.CommandText = "exec uspLogInsert @LogTime , @Message";

            target.Parameters.Add(new DatabaseParameterInfo() { Name = "@LogTime", Layout = "${date}" });
            target.Parameters.Add(new DatabaseParameterInfo("@Message", new SimpleLayout("${exception}")));

            SimpleConfigurator.ConfigureForTargetLogging(target, LogLevel.Trace);
        }
    }
}

關鍵錯誤訊息,這樣呼叫 Store Procedure 是錯誤的
2022-06-15 22:51:55.7917 Error DatabaseTarget([unnamed]): Error when writing to database. Exception: System.Data.SqlClient.SqlException (0x80131904): 找不到預存程序 'exec uspLogInsert @LogTime , @Message'。
完整錯誤訊息
2022-06-15 22:51:55.7917 Error DatabaseTarget([unnamed]): Error when writing to database. Exception: System.Data.SqlClient.SqlException (0x80131904): 找不到預存程序 'exec uspLogInsert @LogTime , @Message'。
   於 System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   於 System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   於 System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   於 System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   於 System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString, Boolean isInternal, Boolean forDescribeParameterEncryption, Boolean shouldCacheForAlwaysEncrypted)
   於 System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, Boolean inRetry, SqlDataReader ds, Boolean describeParameterEncryptionRequest)
   於 System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry)
   於 System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry)
   於 System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   於 NLog.Targets.DatabaseTarget.ExecuteDbCommandWithParameters(LogEventInfo logEvent, IDbConnection dbConnection, IDbTransaction dbTransaction)
   於 NLog.Targets.DatabaseTarget.WriteLogEventSuppressTransactionScope(LogEventInfo logEvent, String connectionString)
ClientConnectionId:404d03a2-984b-4b6e-81bf-ad66fb425678
Error Number:2812,State:62,Class:16

星期五, 6月 17, 2022

[C#] NLog - DatabaseTarget

該篇紀錄透過 NLog 把 Exception 資訊 insert 進 DB 保留

NLog.Database

從 NLog 5.0,Database 功能變成獨立套件 - NLog.Database,找到的範例都只要安裝 NLog 主套件就可以使用 DatabaseTarget,後來才在官網上找到說明 (下圖),namespace 還是維持在 NLog.Targets 內

[C#] NLog - DatabaseTarget-1


提供簡單 Target 和 Rule 設定方式,該設定方式只能設定一個 Target,且會覆蓋現有的 config 喔


SimpleLayout 是屬於 plain text Layout 為預設 Layout,輸入 Layout 資訊時不需要特別去指定

把 Exception 資訊 insert 進 DB

紀錄 Text 和兩種 Store Procedure 呼叫的使用方式

建立對應的 LogTable 和 Store Procedure
USE [AdventureWorks2019]
GO

CREATE TABLE [dbo].[LogTable](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[LogTime] [datetime] NULL,
	[Message] [nvarchar](4000) NULL,
 CONSTRAINT [PK_LogTable] 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

CREATE PROCEDURE [dbo].[uspLogInsert] 
(
	@LogTime datetime ,
	@Message nvarchar(4000)
)
AS
BEGIN

	SET NOCOUNT ON;

	INSERT INTO LogTable (LogTime , Message)
		VALUES(@LogTime , @Message)
END
GO

C# 相關語法
using NLog;
using NLog.Layouts;
using NLog.Targets;
using NLog.Config;
using System;

namespace NLogSample
{
    internal class Program
    {
        // 透過 LogManager.GetCurrentClassLogger() 取得 NLog 設定檔
        private static readonly Logger _logger = LogManager.GetCurrentClassLogger();

        static void Main(string[] args)
        {
            DatabaseConfig();

            try
            {
                ExceptionMethod();
            }
            catch (Exception ex)
            {
                _logger.Error(ex);
            }
        }

        private static void ExceptionMethod()
        {
            int numerator = 1;
            int denominator = 0;
            Console.WriteLine(numerator / denominator);
        }

        private static void DatabaseConfig()
        {
            DatabaseTarget target = new DatabaseTarget();

            // 連結本機 SQL Server 來測試
            target.ConnectionString = "Data Source=.;Initial Catalog=AdventureWorks2019;Integrated Security=True";

            // 方法一:利用 Text 輸入 insert 語法
            target.CommandText = "insert into LogTable(LogTime,Message) values(@LogTime, @Message);";

            // 方法二:呼叫 Store Procedure 來進行 insert
            target.CommandType = CommandType.StoredProcedure;
            target.CommandText = "uspLogInsert";

            // 方法三:呼叫 Store Procedure 來進行 insert
            target.CommandText = "exec uspLogInsert @LogTime , @Message";

            // 故意用兩種方式來記錄參數設定
            target.Parameters.Add(new DatabaseParameterInfo() { Name = "@LogTime", Layout = "${date}" });
            target.Parameters.Add(new DatabaseParameterInfo("@Message", new SimpleLayout("${exception}")));

            SimpleConfigurator.ConfigureForTargetLogging(target, LogLevel.Trace);
        }
    }
}
執行結果

[C#] NLog - DatabaseTarget-2

星期四, 6月 16, 2022

[C#] NLog - Layout Renderers

延續 [C#] NLog - Targets 和 Rules 該篇筆記,這篇主要記錄 Layout Renderers 使用

Exception layout renderer

NLog 內可以透過該 Renderer 來抓出 Exception 相關資訊,設定語法如下
${exception:
format=String:
innerFormat=String:
maxInnerExceptionLevel=Integer:
innerExceptionSeparator=String:
separator=String:
exceptionDataSeparator=string}
參數使用重點
  • format:預設為 ToString()、Data
  • maxInnerExceptionLevel:顯示 InnerException 資訊層深度,預設為 0

全域變數

NLog 有提供 GlobalDiagnosticsContext 可以視情況把相關變數塞進 Context 去,應用在 Layout Renderers 內

把自訂錯誤訊息寫進指定路徑的 txt 檔案

該範例內有把使用者相關資訊塞進 GlobalDiagnosticsContext 內,並透過 Gdc layout renderer 抓出來使用
using NLog;
using System;

namespace NLogSample
{
    internal class Program
    {
        // 透過 LogManager.GetCurrentClassLogger() 取得 NLog 設定檔
        private static readonly Logger _logger = LogManager.GetCurrentClassLogger();

        static void Main(string[] args)
        {
            GlobalDiagnosticsContext.Set("GlobalVar", "使用者相關資訊");
            ExceptionConfig();

            try
            {
                ExceptionMethod();
            }
            catch (Exception ex)
            {
                _logger.Error(ex);
            }
        }

        private static void ExceptionMethod()
        {
            int numerator = 1;
            int denominator = 0;
            Console.WriteLine(numerator / denominator);
        }

        private static void ExceptionConfig()
        {
            var config = new NLog.Config.LoggingConfiguration();

            var exceptionFile = new NLog.Targets.FileTarget("execptionfile")
            {
                FileName = "${basedir}/App_Data/Logs/${shortdate}/ExceptionFile.txt",
                // exception renderer 
                //  1. format:預設為 ToString()、Data
                //  2. maxInnerExceptionLevel:顯示 InnerException 資訊層深度,預設為 0
                Layout = @"
                    發生時間:${longdate}${newline}
                    電腦名稱:${machinename}${newline}
                    全域變數:${gdc:item=globalvar}${newline}$
                    {exception:maxInnerExceptionLevel=5}"
            };
            config.AddRule(LogLevel.Trace, LogLevel.Fatal, exceptionFile);

            LogManager.Configuration = config;
        }
    }
}

範例結果 1 - Exception 檔案所在路徑:NLog 會自行建立相關路徑資料夾,不需要特別去建立,但要特別注意是否有具有相關權限

[C#] NLog - Layout Renderers-1

範例結果 2-Exception 內自訂錯誤訊息:透過 Gdc Layout Renderer 把使用者資訊整合進錯誤訊息

[C#] NLog - Layout Renderers-2

全部 Layout Renderers 使用,可以在官方文章-Layout Renderers 內找到,另外還有每一個 Renderer 參數使用範例喔

星期三, 6月 15, 2022

[C#] NLog - Targets 和 Rules

透過 .net framework console 專案來了解並紀錄 NLog 5.0 使用方式

安裝 NLog

從官方文件 - Installing NLog 可以看見需要安裝 NLog 和 NLog.Config,但在安裝 NLog.Config 時發現該套件已經被淘汰,且 NLog.Config 現在可以透過程式產生 - Configure from code

[C#] NLog-1


Target 和 Rules

NLog 內的設定重點,透過兩者設定搭配來決定用哪種條件、方式來產生 Log

透過 Code 來建立 NLog 設定檔並觀察不同 Target 搭配 LogLevel 的輸出結果

LogLevel 依嚴重性排序依序為:Trace、Debug、Info、Warm、Error、Fatal,下圖為 VS Intellisence 的各 LogLevel 情境說明

[C#] NLog - Targets 和 Rules-4
using NLog;
using System;

namespace NLogSample
{
    internal class Program
    {
    	// 透過 LogManager.GetCurrentClassLogger() 取得 NLog 設定檔
        private static readonly Logger _logger = LogManager.GetCurrentClassLogger();

        static void Main(string[] args)
        {
            LogLevelConfig();

            try
            {
                int numerator = 1;
                int denominator = 0;
                Console.WriteLine(numerator / denominator);
            }
            catch (Exception ex)
            {
                // 觸發 Target1:ErrorFile 和 Taget3:ColoredConsole
                _logger.Error(ex, "輸入 Error 相關說明");
                // 觸發 Target2:FatalFile 和 Taget3:ColoredConsole
                _logger.Fatal(ex, "輸入 Fatal 相關說明");
                // 觸發 Taget3:ColoredConsole
                _logger.Trace(ex);
            }
        }

        private static void LogLevelConfig()
        {
            var config = new NLog.Config.LoggingConfiguration();

            // Target 1:
            // 當 LogLevel = Error 時,會根據 ErrorFile 設定,把錯誤訊息輸出至 txt 檔案 
            var errorFile = new NLog.Targets.FileTarget("errorfile") { FileName = "ErrorFile.txt" };
            config.AddRule(LogLevel.Error, LogLevel.Error, errorFile);

            // Target 2:
            // 當 LogLevel = Fatal 時,會根據 FatalFile 設定,把錯誤訊息輸出至 txt 檔案 
            var fatalFile = new NLog.Targets.FileTarget("fatalfile") { FileName = "FatalFile.txt" };
            config.AddRule(LogLevel.Fatal, LogLevel.Fatal, fatalFile);

            // Target 3:
            // 在 console 內發生 Exception 的話,其實不會顯示錯誤訊息,特地把它顯示出來
            // LogLevel.Trace 到 LogLevel.Fatal 代表任何情況都會觸發
            var coloredConsole = new NLog.Targets.ColoredConsoleTarget("ColoredConsole");
            config.AddRule(LogLevel.Trace, LogLevel.Fatal, coloredConsole);

            LogManager.Configuration = config;
        }
    }
}

ColoredConsole 輸出會根據 LogLevel 呈現不同顏色

[C#] NLog-2

根據不同 LogLevel 產生 Log 到不同 File 去,自行輸入的文字也會出現在 Log 資訊內

[C#] NLog-3

Layouts 和 Layouts Renderers

Log 內容輸出,可以透過 Layouts 和 Layouts Renderers 來變化,該部分只介紹預設輸出
預設輸出文字格式可以參考 官方文章 - TextLayout,文字格式分為四部份,以 | 符號分隔
 ${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}
因為有自行加入文字,所以會變成
${longdate}|${level:uppercase=true}|${logger}|文字輸入|${message:withexception=true}

星期二, 6月 14, 2022

GodexRT 730X - 滾軸卡紙

被告知標籤機列印模糊,去現場檢修時發現機器內滾軸 (其實不知道怎麼稱呼這根桿子) 竟然捲著一張紙,蠻意外的情況

星期四, 6月 09, 2022

[Azure Artifacts] failed to get feed

初學 Artifacts 就踩到坑,遇上下圖右側訊息 An unexpected error has occured,Failed to get feed,一開始以為是權限問題,閱讀官方文章 - Configure permissions 時,發現只要是 Project Collection Adminstraotr 就具備全部權限,還特地弄一個新帳號來測試,使用上跟查到教學文章步驟一致,比對新舊帳號權限也沒有發現異樣,OS表示:天公伯ㄟ,為什咪昧衝低林北

    [Azure Artifacts] failed to get feed-1

在該篇 討論 中找到解法,首先要去關閉 Preview features 內的 New Artifacts (Feeds) Experience,回復到舊介面,建立一個 Feed 後再開啟該功能就會回復正常,猜測是現在新建帳號會有預設 Feed,我開 Organization 時,似乎還沒有 Artifacts 功能,也就沒有預設 Feed,雖然文章內是說使用者刪除預設 Feed 才會造成該現象

對於新手說,連 Preview Featrue 要去哪裡按都要 Google 才知道,Orz

[Azure Artifacts] failed to get feed-2

關閉 New Artifacts (Feeds) Experience

[Azure Artifacts] failed to get feed-3

OS 表示:加油好嗎?

[Azure Artifacts] failed to get feed-4

關閉 New Artifacts (Feeds) Experience 後再進入 Artifacts 就會以舊操作畫面顯示,其實對初接觸的人來說,這根本就是隱藏功能

[Azure Artifacts] failed to get feed-5

建立一個預設 Feed 來使用

[Azure Artifacts] failed to get feed-6

開啟 New Artifacts (Feeds) Experience 後再進入 Artifacts 就會正常啦

[Azure Artifacts] failed to get feed-8

星期二, 6月 07, 2022

[C#] VisualBasic.PowerPacks.Vs 套件

嘗試要在 Azure Pipeline 上對傳統 WinForm 作 CI 時,出現下面錯誤訊息
 
[C#] VisualBasic.PowerPacks.Vs 套件-1

才想到 repo 每次 clone 下來都必須特別去安裝 [C#] Microsoft Visual Basic Power Packs 3.0 來畫線條,但因線條都是直線,所以就用 Panel 來取代結案

nuget 套件

在這 nuget 安裝套件時代,PowerPacks 當然也不例外 - VisualBasic.PowerPacks.Vs ,不過實際拿來用時發現沒有這麼完美,因為 PowerPacks 的 LineShape 不會出現在工具列上,Google 時發現該套件在 VS 2019 時就已經呈現該狀態,參考該討論

[C#] VisualBasic.PowerPacks.Vs 套件-2

雖然在工具列上看不見 LineShape,但還是可以硬用,分別是
  • Coding 執行時動態產生
  • 直接編輯 Designer 來產生

Coding 執行時動態產生

嘗試要打 Code 時才發現到,原來 LineShape 還要依賴 ShaprContainer 才能放進 Form 內,以前直接拖曳根本就沒有發現到這點,下方範例出至官方文章 - LineShape Constructor (ShapeContainer) Sample Code
using Microsoft.VisualBasic.PowerPacks;
using System;
using System.Drawing;
using System.Windows.Forms;

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

            ShapeContainer canvas = new ShapeContainer();
            LineShape line1 = new LineShape();
            canvas.Parent = this;
            line1.Parent = canvas;
            line1.StartPoint = new Point(0, 0);
            line1.EndPoint = new Point(1000, 1000);
        }
    }
}

直接編輯 Designer 產生

直接編輯 Desinger 時,常常在拖曳新控進進入 Form 時,LineShape 就又不知道跑到哪去,還要去 Designer 內 debug,而且都直接進 desinger 內編輯了,在設計階段還是無法看見線條,執行後才會出現
namespace WinFormCICD
{
    partial class Form1
    {
        /// <summary>
        /// 設計工具所需的變數。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 清除任何使用中的資源。
        /// </summary>
        /// <param name="disposing">如果應該處置受控資源則為 true,否則為 false。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form 設計工具產生的程式碼

        /// <summary>
        /// 此為設計工具支援所需的方法 - 請勿使用程式碼編輯器修改
        /// 這個方法的內容。
        /// </summary>
        private void InitializeComponent()
        {
            this.shapeContainer1 = new Microsoft.VisualBasic.PowerPacks.ShapeContainer();
            this.lineShape1 = new Microsoft.VisualBasic.PowerPacks.LineShape();
            this.SuspendLayout();
            // 
            // shapeContainer1
            // 
            this.shapeContainer1.Location = new System.Drawing.Point(0, 0);
            this.shapeContainer1.Margin = new System.Windows.Forms.Padding(0);
            this.shapeContainer1.Name = "shapeContainer1";
            this.shapeContainer1.Shapes.AddRange(new Microsoft.VisualBasic.PowerPacks.Shape[] {
            this.lineShape1});
            this.shapeContainer1.Size = new System.Drawing.Size(861, 579);
            this.shapeContainer1.TabIndex = 1;
            this.shapeContainer1.TabStop = false;
            // 
            // lineShape1
            // 
            this.lineShape1.Name = "lineShape1";
            this.lineShape1.X1 = 70;
            this.lineShape1.X2 = 71;
            this.lineShape1.Y1 = 108;
            this.lineShape1.Y2 = 245;
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(861, 579);
            this.Controls.Add(this.shapeContainer1);
            this.Name = "Form1";
            this.Text = "PowerPacks-LineShape";
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private Microsoft.VisualBasic.PowerPacks.ShapeContainer shapeContainer1;
        private Microsoft.VisualBasic.PowerPacks.LineShape lineShape1;
    }
}

下圖為效果呈現,左圖為設計階段、右圖為執行階段,兩種方式都必須在執行階段才會看見線條

[C#] VisualBasic.PowerPacks.Vs 套件-3

Panel 畫線條

透過設定 Panel 屬性,可以有直線和橫線效果,分別設定  
  • BorderStyle:不要有邊框
  • BackColor:背景顏色當成線條顏色
  • Size:看是直線還是橫線,來調整 Height 或 Width,還可以當成線條寬度調整
雖然 Panel 無法取代 LineShape,畢竟 LineShape 還可以畫斜線,但在個人實務環境內已經就足夠

三種方式大合照

[C#] VisualBasic.PowerPacks.Vs 套件-4

最後還是許願一下,希望未來可以直接從工具列上直接把 LineShape 拖曳進 Form 內使用

星期六, 6月 04, 2022

[LINQ] ANY、All

平時常用 any(),突然用到 all() 時腦袋有點打結,根據官方文章來練習並記錄兩者差異

Determines whether all elements of a sequence satisfy a condition. 
Determines whether any element of a sequence exists or satisfies a condition.
在 LinqPad 上練習
void Main()
{

	GetData().Where(p => p.Pets.Any() == false)
		.Select(p => p.LastName)
		.Dump("沒有飼養寵物的人");

	GetData().Single(p => p.LastName == "Philips")
		.Pets.Any(pet => pet.Age > 1 && pet.Vaccinated == false)
		.Dump("Philips 是否有寵物超過 1 歲還沒打疫苗");

	GetData().Single(p => p.LastName == "Haas")
		.Pets.All(pet => pet.Name.StartsWith("B"))
		.Dump("Haas 的寵物名稱,是否都是 B 開頭");

	GetData().Where(p => p.Pets.Any() && p.Pets.All(pet => pet.Age > 5))
		.Select(p => p.LastName)
		.Dump("有飼養寵物,且年紀都在 5 歲以上的飼主");

	List<Person> GetData()
	{
		return new List<Person>
		{
			new Person { LastName = "Haas" , 
				Pets = new Pet[] {  
					new Pet { Name = "Barley"   , Age = 10 , Vaccinated = true},
					new Pet { Name = "Boots"    , Age = 14 , Vaccinated = true},
					new Pet { Name = "Whiskers" , Age = 6  , Vaccinated = true}}},
				
		  	new Person { LastName = "Fakhouri" , 
				Pets = new Pet[] {  
					new Pet { Name = "Snowball" , Age = 1  , Vaccinated = false}}},
				
		  	new Person { LastName = "Antebi" , 
				Pets = new Pet[] {
					new Pet { Name = "Belle"    , Age = 8  , Vaccinated = true}}},
				
		  	new Person { LastName = "Philips" , 
				Pets = new Pet[] {
					new Pet { Name = "Sweetie"  , Age = 2  , Vaccinated = false},
					new Pet { Name = "Rover"    , Age = 13 , Vaccinated = true}}},
					
			// 該位沒有飼養寵物
			new Person { LastName = "NoName",
				Pets = new Pet[] { }},
		};
	}
}

class Pet
{
	public string Name { get; set; }
	public int Age { get; set; }
	public bool Vaccinated { get; set; }
}

class Person
{
	public string LastName { get; set; }
	public Pet[] Pets { get; set; }
}

[LINQ] ANY、All 

星期三, 6月 01, 2022

[C#] 註冊 OCX

公司舊系統有使用上 mscomctl.ocx TreeView 元件,安裝舊系統時常常會忘記安裝,乾脆把元件嵌在 C# ERP 上,只要一執行就會自動安裝,透過 C# ERP 來輔助舊系統,參考該文章 - 现在介绍一种使用资源文件,将dll、ocx打包进exe,点击直接注册的例子 來完成

把 ocx 加入 Resource.resx 內

[C#] 註冊 OCX -1

把 ocx 加入後,在 Resource 資料夾內可以找到檔案,並把該檔案屬性建置動作,設定為內嵌資源

[C#] 註冊 OCX -2

在程式執行時,偵測 ocx 是否存在,沒有就進行 ocx 安裝和註冊
using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;

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

        private void Form1_Load(object sender, EventArgs e)
        {
            Environment.SpecialFolder targettOS = GetBitOfOperatingSystem();
            string ocxName = "mscomctl.ocx";
            string fileFullName = Path.Combine(Environment.GetFolderPath(targettOS), ocxName);

            // 檔案存在就視同已經完成安裝和註冊
            if (File.Exists(fileFullName))
            {
                MessageBox.Show("檔案存在,不進行 OCX 註冊");
                return;
            }

            PutFileFromResource(fileFullName);
            RegisterDll(fileFullName);
        }

        /// <summary>
        /// 根據 OS 位元數,傳回對應系統資料夾完整路徑
        /// </summary>
        /// <returns>系統資料夾 (32bit:System32、64bit:SysWOW64)</returns>
        private Environment.SpecialFolder GetBitOfOperatingSystem()
        {
            Environment.SpecialFolder targettOS;
            if (Environment.Is64BitOperatingSystem)
                targettOS = Environment.SpecialFolder.SystemX86;
            else
                targettOS = Environment.SpecialFolder.System;

            return targettOS;
        }

        /// <summary>
        /// 把內嵌檔案抓出來存放在指定位置
        /// </summary>
        /// <param name="fileFullName">檔案完整路徑</param>
        private void PutFileFromResource(string fileFullName)
        {
            if (File.Exists(fileFullName)) return;

            byte[] fileData = Properties.Resources.mscomctl;
            File.WriteAllBytes(fileFullName, fileData);
        }

        /// <summary>
        /// 註冊 dll 或 OCX 檔案
        /// </summary>
        /// <param name="dllFullName">檔案完整路徑</param>
        public void RegisterDll(string dllFullName)
        {
            ProcessStartInfo startInfo = new ProcessStartInfo();
            startInfo.FileName = "Regsvr32.exe";
            startInfo.Arguments = " /s " + dllFullName;
            Process.Start(startInfo);
        }
    }
}
除了上述註冊 ocx 方式外,也可以把 regsvr32 語法寫成一個 bat,C# 只要執行該 bat 就行

regsvr32 參數說明,有應用到 /s 參數,程式執行時不要出現註冊成功訊息

   [C#] 註冊 OCX -3

測試時安裝時有反覆安裝、移除,發現移除時除了要反註冊 ocx 外,還要把檔案移除才算是完整移除,StackOverFlow 討論 上查到移除順序也是一個重點,要先反註冊後才能移除檔案,不能反過來