WPF事件绑定和解绑、应用退出监听

前言

WPF中事件的绑定和解绑放在什么生命周期中比较合适呢?

窗口

在 WPF 中,窗口(Window)是一种特殊的控件,其生命周期也包括了一系列的事件,你可以在这些事件中进行事件的绑定和解绑。

生命周期

OnInitialized => Loaded => Closing => Closed

以下是一些比较合理的时机:

Loaded 事件: 当窗口加载完成并准备好与用户交互时,可以在 Loaded 事件中进行事件绑定。这是一个非常常用的时机。

1
2
3
4
5
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// 在这里进行事件绑定
myButton.Click += MyButton_Click;
}

Closing 事件: 当用户关闭窗口时,可以在 Closing 事件中进行事件解绑。这是释放资源和进行清理操作的好时机。

1
2
3
4
5
private void Window_Closing(object sender, CancelEventArgs e)
{
// 在这里进行事件解绑
myButton.Click -= MyButton_Click;
}

OnInitialized 方法: 在窗口初始化时进行事件绑定也是一种合理的方式。

1
2
3
4
5
6
7
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);

// 在这里进行事件绑定
myButton.Click += MyButton_Click;
}

Closed 事件: 当窗口已经完全关闭并且将要从视图中移除时,可以在 Closed 事件中进行一些清理工作。

1
2
3
4
private void Window_Closed(object sender, EventArgs e)
{
// 在这里进行一些清理工作
}

总的来说,绑定和解绑事件的时机取决于你的具体需求和情况。

通常来说:

Loaded 时绑定事件,因为窗口已经准备好与用户交互。

Closing 时解绑事件,因为你可以在窗口关闭前进行一些资源释放和清理工作。

控件

在 WPF(Windows Presentation Foundation)开发中,通常在控件的生命周期方法中进行事件的绑定和解绑是比较合理的。

基本

以下是一些常用的生命周期方法,你可以考虑在这些方法中进行事件的绑定和解绑:

Loaded 事件: 控件已经被加载到 Visual 树中,可以安全地进行事件绑定。

在 Loaded 事件中进行事件绑定是比较常见的做法,因为此时控件已经准备好与用户交互。

1
2
3
4
5
private void MyControl_Loaded(object sender, RoutedEventArgs e)
{
// 在这里进行事件绑定
myButton.Click += MyButton_Click;
}

Unloaded 事件: 控件即将从 Visual 树中移除,适合在此时进行事件解绑。

1
2
3
4
5
private void MyControl_Unloaded(object sender, RoutedEventArgs e)
{
// 在这里进行事件解绑
myButton.Click -= MyButton_Click;
}

MVVM

OnDetaching 方法 (MVVM 模式): 如果你使用了 MVVM 模式,你可以在自定义的附加行为中实现事件的绑定和解绑逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyButtonBehavior : Behavior<Button>
{
protected override void OnAttached()
{
base.OnAttached();

AssociatedObject.Click += MyButton_Click;
}

protected override void OnDetaching()
{
base.OnDetaching();

AssociatedObject.Click -= MyButton_Click;
}
}

应用

应用退出

应用退出的监听

1
2
3
4
5
6
7
8
9
protected override void OnExit(ExitEventArgs e)
{
// 在应用程序退出之前执行必要的操作
// 例如保存数据、关闭连接、清理资源等
ZAppDataHelper.SaveData("" + ZHttpUtil.isSt);
Console.WriteLine(@"应用退出");
base.OnExit(e);
Environment.Exit(0);
}

应用退出的方式

1
Application.Current.Shutdown();

1
Environment.Exit(0);

注意其它界面退出应用

不要使用Environment.Exit(0);

不要使用Environment.Exit(0);

不要使用Environment.Exit(0);

这样OnExit中监听不到应用的退出。

启动监听异常退出

异常退出的处理

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
protected override void OnStartup(StartupEventArgs e)
{
ZLogHelper.InitLog4Net();
RegisterEvents();
base.OnStartup(e);
}

private void RegisterEvents()
{
//Task线程内未捕获异常处理事件
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

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

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

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

//非UI线程未捕获异常处理事件(例如自己创建的一个子线程)
private static void CurrentDomain_UnhandledException
(
object sender,
UnhandledExceptionEventArgs e
)
{
try
{
if (e.ExceptionObject is Exception exception)
{
HandleException(exception);
}
}
catch (Exception ex)
{
HandleException(ex);
}
}

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

//日志记录
private static void HandleException(Exception ex)
{
//记录日志
ZLogHelper.Logerror.Error(ex.Message);
Current.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
33
34
35
36
37
38
[DllImport("User32.dll")]
private static extern int FindWindow
(
string className,
string windowName
);

[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(int hWnd);

private static void CheckProcess()
{
Console.WriteLine(@"程序启动");
Process current = Process.GetCurrentProcess();
//获取欲启动进程名
string strProcessName = Process.GetCurrentProcess().ProcessName;
var processList = Process.GetProcessesByName(strProcessName);
if (processList.Length > 1)
{
MessageWindow.Show("当前程序已在运行,请勿重复运行。");
foreach (var process in processList)
{
if (process.Id == current.Id) continue;
int hWnd = FindWindow(
null,
process.MainWindowTitle
);
SetForegroundWindow(hWnd);
}
Environment.Exit(1); //退出程序
}
}

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

自动释放内存

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

/// <summary>
/// 内存释放.
/// </summary>
/// <param name="sleepSpan">
/// 周期
/// </param>
public static void CrackerOnlyGC(int sleepSpan = 30)
{
void ThreadStart(object s)
{
while (true)
try
{
GC.Collect();
GC.WaitForPendingFinalizers();
Thread.Sleep(TimeSpan.FromSeconds(sleepSpan));
}
catch (Exception)
{
// ignored
}
// ReSharper disable once FunctionNeverReturns
}
new Thread(ThreadStart) { IsBackground = true }.Start();
}

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

内存泄漏示例

使用Lambda绑定事件

有一些特殊情况可能会导致内存泄漏:Lambda 表达式中的引用捕获

如果在 Lambda 表达式中捕获了外部的引用(比如一个对象实例),而这个 Lambda 表达式又被长时间保持,这可能会导致对象无法被垃圾回收,从而间接导致内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
public class Example
{
private SomeClass _someObject;

public Example()
{
_someObject = new SomeClass();

myButton.Click += (sender, e) => _someObject.DoSomething();
}
}

在这个例子中,Lambda 表达式捕获了外部的 _someObject 引用。

如果 Example 对象一直存在并持有对 myButton.Click 事件的订阅,那么 _someObject 将无法被垃圾回收,直到 Example 对象被释放。

为了避免这种情况,可以在不需要订阅事件时,取消订阅以释放对对象的引用,或者使用弱事件(Weak Event)模式来管理事件订阅,以确保对象可以被垃圾回收。