WPF开发-日志记录及全局异常捕获

使用log4net

注意

稍大点的应用推荐这种方式。可定义参数多。

安装

Nuget安装log4net

image-20220314125020144

项目根目录添加log4net.config

设置属性

image-20211209183912018

下面两种任选一种即可。

下面两种方式主要是封装的要解决无法在日志中显示异常位置的问题。

默认(推荐)

log4net.config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<!--错误日志类-->
<logger name="logerror">
<!--日志类的名字-->
<level value="ALL" />
<!--定义记录的日志级别-->
<appender-ref ref="ErrorAppender" />
<!--记录到哪个介质中去-->
</logger>
<!--信息日志类-->
<logger name="loginfo">
<level value="ALL" />
<appender-ref ref="InfoAppender" />
</logger>
<!--错误日志附加介质-->
<appender name="ErrorAppender" type="log4net.Appender.RollingFileAppender">
<!-- name属性指定其名称,type则是log4net.Appender命名空间的一个类的名称,意思是,指定使用哪种介质-->
<param name="File" value="Log\\LogError\\" />
<!--日志输出到exe程序这个相对目录下-->
<param name="AppendToFile" value="true" />
<!--输出的日志不会覆盖以前的信息-->
<param name="MaxSizeRollBackups" value="100" />
<!--备份文件的个数-->
<param name="MaxFileSize" value="10240" />
<!--当个日志文件的最大大小-->
<param name="StaticLogFileName" value="false" />
<!--是否使用静态文件名-->
<param name="DatePattern" value="yyyyMMddHHmm&quot;.htm&quot;" />
<!--日志文件名-->
<param name="RollingStyle" value="Date" />
<!--文件创建的方式,这里是以Date方式创建-->
<!--错误日志布局-->
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="&lt;HR COLOR=red&gt;%n异常时间:%d [%t] &lt;BR&gt;%n异常级别:%-5p &lt;BR&gt;%n异常类:%class:%line &lt;BR&gt;%n%m &lt;BR&gt;%n &lt;HR Size=1&gt;" />
</layout>
</appender>
<!--信息日志附加介质-->
<appender name="InfoAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="Log\\LogInfo\\" />
<param name="AppendToFile" value="true" />
<param name="MaxFileSize" value="10240" />
<param name="MaxSizeRollBackups" value="100" />
<param name="StaticLogFileName" value="false" />
<param name="DatePattern" value="yyyyMMdd&quot;.htm&quot;" />
<param name="RollingStyle" value="Date" />
<!--信息日志布局-->
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="&lt;HR COLOR=blue&gt;%n日志时间:%d [%t] &lt;BR&gt;%n日志级别:%-5p &lt;BR&gt;%n日志类:%class:%line &lt;BR&gt;%n%m &lt;BR&gt;%n &lt;HR Size=1&gt;" />
</layout>
</appender>
</log4net>

其中

%class 显示包名和类名

%class{1} 只显示类名

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using System.IO;

using log4net;
using log4net.Config;

namespace Z.Utils.Common
{
public class ZLogHelper
{
public static readonly ILog Loginfo = LogManager.GetLogger("loginfo");
public static readonly ILog Logerror = LogManager.GetLogger("logerror");

public static void InitLog4Net()
{
FileInfo logCfg = new FileInfo(AppDomain.CurrentDomain.BaseDirectory + "log4net.config");
XmlConfigurator.ConfigureAndWatch(logCfg);
}

/// <summary>
/// 停止日志记录
/// </summary>
public static void StopLog()
{
log4net.LogManager.Shutdown();
}
}
}

调用

1
2
3
ZLogHelper.Loginfo.Info("课堂启动");

ZLogHelper.Logerror.Error("异常退出:",ex);

如果在应用结束前要上传日志文件,我们就要停止日志,防止文件占用

1
ZLogHelper.StopLog();

打印示例

日志时间:2023-11-06 15:21:41,108 [1]
日志级别:INFO
日志类:SchoolClient.MyApp:35
课堂启动

注意

这里之所以没有把打印日志封装是因为:打印的类和行号是被调用的类,如果封装后,显示的都会是封装的类,那么打印类名和行号就没有意义了。

简单封装(不推荐)

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<!--错误日志类-->
<logger name="logerror">
<!--日志类的名字-->
<level value="ALL" />
<!--定义记录的日志级别-->
<appender-ref ref="ErrorAppender" />
<!--记录到哪个介质中去-->
</logger>
<!--信息日志类-->
<logger name="loginfo">
<level value="ALL" />
<appender-ref ref="InfoAppender" />
</logger>
<!--错误日志附加介质-->
<appender name="ErrorAppender" type="log4net.Appender.RollingFileAppender">
<!-- name属性指定其名称,type则是log4net.Appender命名空间的一个类的名称,意思是,指定使用哪种介质-->
<param name="File" value="Log\\LogError\\" />
<!--日志输出到exe程序这个相对目录下-->
<param name="AppendToFile" value="true" />
<!--输出的日志不会覆盖以前的信息-->
<param name="MaxSizeRollBackups" value="100" />
<!--备份文件的个数-->
<param name="MaxFileSize" value="10240" />
<!--当个日志文件的最大大小-->
<param name="StaticLogFileName" value="false" />
<!--是否使用静态文件名-->
<param name="DatePattern" value="yyyyMMddHHmm&quot;.htm&quot;" />
<!--日志文件名-->
<param name="RollingStyle" value="Date" />
<!--文件创建的方式,这里是以Date方式创建-->
<!--错误日志布局-->
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="&lt;HR COLOR=red&gt;%n异常时间:%d [%t] &lt;BR&gt;%n异常级别:%-5p&lt;BR&gt;%n%m &lt;BR&gt;%n &lt;HR Size=1&gt;" />
</layout>
</appender>
<!--信息日志附加介质-->
<appender name="InfoAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="Log\\LogInfo\\" />
<param name="AppendToFile" value="true" />
<param name="MaxFileSize" value="10240" />
<param name="MaxSizeRollBackups" value="100" />
<param name="StaticLogFileName" value="false" />
<param name="DatePattern" value="yyyyMMdd&quot;.htm&quot;" />
<param name="RollingStyle" value="Date" />
<!--信息日志布局-->
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="&lt;HR COLOR=blue&gt;%n日志时间:%d [%t] &lt;BR&gt;%n日志级别:%-5p&lt;BR&gt;%n%m &lt;BR&gt;%n &lt;HR Size=1&gt;" />
</layout>
</appender>
</log4net>

添加工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using System;
using System.IO;

using log4net;
using log4net.Config;

namespace Z.Utils.Common
{
public class ZLogHelper
{
private static readonly ILog Loginfo = LogManager.GetLogger("loginfo");
private static readonly ILog Logerror = LogManager.GetLogger("logerror");

public static void InitLog4Net()
{
var logCfg = new FileInfo(AppDomain.CurrentDomain.BaseDirectory + "log4net.config");
XmlConfigurator.ConfigureAndWatch(logCfg);
}

/// <summary>
/// 停止日志记录
/// </summary>
public static void StopLog()
{
log4net.LogManager.Shutdown();
}

public static void WriteInfoLog
(
string message,
[System.Runtime.CompilerServices.CallerFilePath] string callerFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int callerLineNumber = 0
)
{
Loginfo.InfoFormat(
"日志类:{0}:{1}<br>{2}",
callerFilePath,
callerLineNumber,
message
);
}

public static void WriteErrLog
(
string message,
Exception ex,
[System.Runtime.CompilerServices.CallerFilePath] string callerFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int callerLineNumber = 0
)
{
Logerror.InfoFormat(
"日志类:{0}:{1}<br>{2}<br>{3}",
callerFilePath,
callerLineNumber,
message,
ex.Message
);
}
}
}

注意

在调用记录日志前,要调用InitLog4Net方法。

因为封装了,所以自带的打印类和行数就不生效了,我们可以自己进行格式化拼接。

调用

1
2
ZLogHelper.WriteInfoLog("课堂启动");
ZLogHelper.WriteErrLog("异常退出:",ex);

打印示例

这种方式获取到的文件的路径,暂时没找到获取包名+类名的方式。

日志时间:2023-11-06 14:59:36,168 [1]
日志级别:INFO
日志类:D:\Project\csharp\xh-schoolclient\MyApp.xaml.cs:35
课堂启动

自定义日志类

注意

小应用可以使用这种方式。

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.IO;
using System.Threading;

namespace Z.Utils.Common
{
public class LogHelper
{
//读写锁,当资源处于写入模式时,其他线程写入需要等待本次写入结束之后才能继续写入
private static readonly ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();

public static void LogWrite(Exception ex)
{
if (!Directory.Exists("Log"))
{
Directory.CreateDirectory("Log");
}
string logpath = @"Log\" + DateTime.Now.ToString("yyyy-MM-dd-HH") + ".html";
string log = "<div style='border: 1px solid #CCC;margin-bottom:10px;padding:10px;'>"
+ "<h3>" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "</h3>"
+ ex.Message
+ "<br>"
+ ex.InnerException
+ "<br>"
+ ex.StackTrace
+ "</div>";
try
{
//设置读写锁为写入模式独占资源,其他写入请求需要等待本次写入结束之后才能继续写入
LogWriteLock.EnterWriteLock();
File.AppendAllText(logpath, log);
}
finally
{
//退出写入模式,释放资源占用
LogWriteLock.ExitWriteLock();
}
}
}
}

调用时

1
2
//记录日志
LogHelper.LogWrite(ex);

异常测试

1
LogHelper.LogWrite(new Exception("主页面启动"));

全局异常捕获

这里的示例采用的第三方库的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
ZLogHelper.InitLog4Net();
RegisterEvents();
base.OnStartup(e);
}

private void RegisterEvents()
{
//UI线程未捕获异常处理事件(UI主线程)
DispatcherUnhandledException += App_DispatcherUnhandledException;

//非UI线程未捕获异常处理事件(例如自己创建的一个子线程)
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

//Task线程内未捕获异常处理事件
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}

//Task线程内未捕获异常处理事件
private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
e.SetObserved();
if (e.Exception is Exception exception)
{
HandleException(exception);
}
}

//非UI线程未捕获异常处理事件
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception exception)
{
HandleException(exception);
}
}

//UI线程未捕获异常处理事件(UI主线程)
private static void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
HandleException(e.Exception);
e.Handled = true;
}

//日志记录
private static void HandleException(Exception ex)
{
//记录日志
ZLogHelper.Logerror.Error(ex.Message);
Current.Dispatcher.Invoke(
() =>
{
Current.Shutdown();
}
);
}
}

在处理 TaskScheduler.UnobservedTaskException 事件时,确保调用 e.SetObserved() 将异常标记为已观察,以便避免默认的行为,即将异常重新抛出。

也就是说要先调用 e.SetObserved(),之后再写异常处理的逻辑。

应用退出

方式1

1
Environment.Exit(0);

Environment.Exit(0) 方法可以在非 UI 线程中调用,用于终止整个应用程序并返回给操作系统。

Environment.Exit(0) 方法立即终止当前进程,不管当前线程是 UI 线程还是其他线程。

它会终止所有线程,关闭所有打开的句柄,释放所有资源,并立即退出应用程序。

这种方式我只在两个地方用

应用退出时

1
2
3
4
5
6
protected override void OnExit(ExitEventArgs e)
{
// 在应用程序退出之前执行必要的操作
base.OnExit(e);
Environment.Exit(0);
}

检测程序唯一时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void CheckProcess()
{
Console.WriteLine(@"检查是否已开启");
Process current = Process.GetCurrentProcess();
//获取欲启动进程名
string strProcessName = Process.GetCurrentProcess().ProcessName;
Process[] processList = Process.GetProcessesByName(strProcessName);
if (processList.Length <= 1)
{
return;
}
MessageWindow.Show("当前程序已在运行,请勿重复运行。");
foreach (Process process in processList)
{
if (process.Id == current.Id) continue;
int hWnd = FindWindow(null, process.MainWindowTitle);
SetForegroundWindow(hWnd);
}
Environment.Exit(0); //退出程序
}

protected override void OnStartup(StartupEventArgs e)
{
CheckProcess();
base.OnStartup(e);
}

其它的地方都使用Application.Current.Shutdown(),否则不会触发程序的OnExit生命周期事件。

方式2

1
Application.Current.Shutdown();

是的,Application.Current.Shutdown() 方法只能在 UI 线程中调用,否则会抛出 InvalidOperationException 异常。

在 WPF 应用程序中,Application.Current 是一个静态属性,它代表了当前应用程序的 Application 实例。调用 Shutdown 方法将停止应用程序并关闭窗口,结束应用程序的 UI 线程。

在非 UI 线程(例如后台线程或 Task 中)调用 Application.Current.Shutdown() 方法可能会导致已经启动的 UI 线程被阻塞或退出应用程序,因为它们处于不同的线程上下文。

如果必须在非 UI 线程中关闭应用程序,可以通过将调用封装在 Dispatcher.InvokeDispatcher.BeginInvoke 方法中来实现在 UI 线程上下文中调用 Application.Current.Shutdown() 方法。

例如:

1
2
3
Application.Current.Dispatcher.Invoke(() => {
Application.Current.Shutdown();
});

这样可以确保 Shutdown() 方法在正确的线程上下文中调用。

应用退出删除空日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected override void OnExit(ExitEventArgs e)
{
// 在应用程序退出之前执行必要的操作
Console.WriteLine(@"----------------------------------------");
Console.WriteLine(@"应用退出");
//解除文件占用
ZLogHelper.StopLog();
string[] files = Directory.GetFiles(ZConfig.LogPath, "*.htm");
foreach (string filePath in files)
{
//跳过占用的文件
if (ZFileUtil.IsFileInUse(filePath))
{
continue;
}
if (new FileInfo(filePath).Length != 0)
{
continue;
}
//删除空日志文件
try
{
File.Delete(filePath);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
base.OnExit(e);
Environment.Exit(0);
}

文件是否占用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[DllImport("kernel32.dll")]
public static extern IntPtr _lopen(string lpPathName, int iReadWrite);


[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);


/// <summary>
/// 文件是否占用
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static bool IsFileInUse(string fileName)

{
const int ofReadwrite = 2;
const int ofShareDenyNone = 0x40;
IntPtr hfileError = new IntPtr(-1);
if (!File.Exists(fileName))
{
return false;
}

IntPtr vHandle = _lopen(fileName, ofReadwrite | ofShareDenyNone);

if (vHandle == hfileError)
{
return true;
}

CloseHandle(vHandle);

return false;
}