4. WPF应用程序中的未捕获异常处理

文章目录

      • 一. 目标
      • 二. 技能介绍
        • ① UI未捕获异常的处理方式
        • ② 全局程序域抛出的未处理异常的捕获
        • ③ 异步Task任务中的异常捕获
      • 三. 总结

一. 目标

  1. 理解和使用UI未捕获异常DispatcherUnhandledException的使用方法和触发方式.
  2. 理解和使用程序域未捕获异常AppDomain.CurrentDomain.UnhandledException的使用方法和触发方式.
  3. 理解和使用异步代码中未观察的异常TaskScheduler.UnobservedTaskException的使用方法和触发方式.

二. 技能介绍

① UI未捕获异常的处理方式

DispatcherUnhandledExceptionUI线程未处理异常捕获事件介绍

  • 用途: 专门用于捕获UI线程上抛出的未处理的异常,在WPF应用程序中就是指的主线程.
  • 特点: 允许开发者阻止异常终止应用程序,默认情况下,如果不处理这个事件,则异常会导致应用程序关闭
  • 注册方式: 通常在App()构造哈数中注册其事件

使用案例: 捕获到异常,并且显示弹窗,弹窗完之后,写一个按钮事件,点击会抛出一个异常.

namespace DispatcherExceptionHandlerSimple
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            DispatcherUnhandledException += App_DispatcherUnhandledException;
        }

        private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine($"UI线程上未处理的异常: {e.Exception.Message}");
            e.Handled = false;  // 这里表示异常已经被处理,不继续往上抛了,程序不会关闭
        }
    }
}
namespace DispatcherExceptionHandlerSimple
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        // 点击了UI模拟UI线程按钮
        private void Btn_ThrowEOnUIClick(object sender, RoutedEventArgs e)
        {
            // 这里在UI线程上抛出一个异常
            throw new Exception("UI线程上发生了异常");
        }
    }
}

注意这里在调试的过程中会发现,点击了按钮之后,程序中断了,还没有到UI线程异常捕获的地方就中断了,然后可能会看到如下的截图:
在这里插入图片描述
此时我们把下面的引发此异常类型时中断关闭之后,然后再点击运行,还是会出现如下的窗口.

我先说结论,第二种情况下,其实已经捕获了这个异常了,控制台可以看到输出,打断点的时候我们也可以验证,确实App_DispatcherUnhandledException执行了,但是为什么最后程序又崩了,又回来了呢,因为这个异常在App_DispatcherUnhandledException这里没有处理,它会继续冒泡,然后就会被系统捕获到,租后就会显示界面上这种情况,怎么样让程序不崩溃的,只要把上面的e.Handle=True即可,表示这个异常异常处理了,程序就不会崩了.

上面的代码我学到了哪些技能:

  1. 调试过程中可以设置异常是否中断来控制调试流程
  2. 如果是在UI线程(主线程)上抛出了异常,又没有被处理,会被DispatcherUnHandledException捕获,并且可以通过设置e.Handle=true防止应用程序崩溃,如果没有捕获这个异常,会引起程序崩溃
② 全局程序域抛出的未处理异常的捕获

AppDomain.CurrentDomain.UnhandledException异常捕获事件介绍:

  • 用途: 用于捕获当前应用程序中抛出的所有未处理的异常,不仅仅局限于UI线程上发生的异常.
  • 特点: 此事件是.NetFramework的一部分,适用于所有类型的.Net应用程序,包括WPF,WinForms,Console等.处理这个事件主要用于记录信息和进行清理工作,因为一旦触发此事件,应用程序将无法阻止关闭
  • 注册方式: 在应用程序的任何地方注册都可以,推荐在app构造函数中注册

下面我们模拟一个在非UI线程上抛出未捕获异常的例子

public partial class App : Application
    {
        public App()
        {
            DispatcherUnhandledException += App_DispatcherUnhandledException;
            AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; ;
        }

        private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine($"程序域内捕获了异常: {(e.ExceptionObject as Exception)?.Message}");
        }

        private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine($"UI线程上未处理的异常: {e.Exception.Message}");
            e.Handled = true;  // 这里表示异常已经被处理,不继续往上抛了,程序不会关闭
        }
    }
 public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        // 点击了UI模拟UI线程按钮
        private void Btn_ThrowEOnUIClick(object sender, RoutedEventArgs e)
        {
            // 这里在UI线程上抛出一个异常
            throw new Exception("UI线程上发生了异常");
        }

        private void Btn_ThrowEOnNotUIClick(object sender, RoutedEventArgs e)
        {
            new Thread(() =>
            {
                throw new Exception("非UI线程上发生了异常");
            }).Start();
        }
    }

运行程序,我们会发现点击捕获非UI线程上的异常按钮,会触发异常 new Exception("非UI线程上发生了异常")并且这个异常处理之后,程序就中断了,然后回到了发生异常的地方.

思考: 为什么我们这里要使用Thread()启动线程,假如我们使用了Task来启动会有什么结果呢? 下面我们把抛出异常的线程改成异步任务Task,看看会发生什么吧

        private void Btn_ThrowEOnNotUIClick(object sender, RoutedEventArgs e)
        {
            Task.Run(() =>
            {
                throw new Exception("非UI线程上发生了异常");
            });
        }

可以看到这里程序域异常捕获事件并没有被触发呢,这是为什么呢?
原因:

.Net中,通过Task.Run()方法启动的任务中抛出的异常具有特殊的处理机制.这些异常在任务内部如果没有捕获到,它们会封装在AggregateException对象中.这些异常不会立即触发AppDomain.CurrentDomain.UnhandledException事件,因为任务系统会处理这些异常并将它们存储起来,等待调用方法去查询或者是处理.

这个例子我们收获了什么呢?

  1. 程序域捕获异常之后,不能截获异常,这个时候程序都会终止.
  2. 如果是异步任务Task.Run()触发的异常,程序域异常捕获器并不能捕获到,原因是因为异步任务的异常有特殊的处理方式.
③ 异步Task任务中的异常捕获

TaskScheduler.UnobservedTaskException异常捕获介绍:

  • 用途: 用于捕获在Task中未被观察(即未被await或者访问其Exception属性)的异常
  • 特点: 这个事件提供了一个机会用来处理那些在异步操作中被遗漏的异常
  • 注册方式: 通常在应用陈旭启动时注册.

简单例子,未被捕获的异步任务异常捕获方式:

       private void Btn_ThrowTaskExceptionClick(object sender, RoutedEventArgs e)
        {
            Task.Run(() =>
             {
                 throw new Exception("这是一个未捕获的异步任务中发生的异常");
             });
            // 模拟一段时间后的垃圾回收,通常在应用程序生命周期的某个时刻自然发生
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
namespace DispatcherExceptionHandlerSimple
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            DispatcherUnhandledException += App_DispatcherUnhandledException;
            AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
            TaskScheduler.UnobservedTaskException += Task_UnobservedTaskException;
        }

        private void Task_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
        {
            e.SetObserved(); // 标记异常已经被观察,防止进程终止
            var ex = e.Exception; // 获取异常,进行日志记录或者是其他处理
            System.Diagnostics.Debug.WriteLine($"捕获未观察到的Task异常: {ex.Message}");
        }

        private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine($"程序域内捕获了异常: {(e.ExceptionObject as Exception)?.Message}");
        }

        private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine($"UI线程上未处理的异常: {e.Exception.Message}");
            e.Handled = true;  // 这里表示异常已经被处理,不继续往上抛了,程序不会关闭
        }
    }
}

.Net异步异常被触发的时机问题:

上面的代码我在测试的时候发现,异步任务抛出异常之后,即使调用了垃圾回收,但是异常依旧没有被捕获,当点击再次按钮的时候,上次的异步异常才会被捕获.这是什么原因呢,这是因为垃圾回收的不确定性,GC.Collect()是用来建议进行垃圾回收,但是.NET运行时仍然拥有最终的决定权,何时以及什么时候进行垃圾回收.只有在垃圾回收的时候Task相关的异常才会抛出,被TaskScheduler.UnobservedTaskException事件捕获. 有没有办法每次点击按钮的时候都能够捕获到该异常呢,通过在下面添加一段代码就可以,比如await Task.Delay(100)

        private async void Btn_ThrowTaskExceptionClick(object sender, RoutedEventArgs e)
        {
            Task.Run(() =>
             {
                 throw new Exception("这是一个未捕获的异步任务中发生的异常");
             });
            await Task.Delay(100);
             模拟一段时间后的垃圾回收,通常在应用程序生命周期的某个时刻自然发生
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
    }

具体是什么原因导致的,不知道.还有就是如果Task.Run()这里加上await,比如下面这种

        private async void Btn_ThrowTaskExceptionClick(object sender, RoutedEventArgs e)
        {
            await Task.Run(() =>
             {
                 throw new Exception("这是一个未捕获的异步任务中发生的异常");
             });
        }

这里又是什么原因呢?为什么这里加上await 之后,异常捕获变成在UI线程异常捕获那里捕获到异常了呢?

原因:

  • 异步任务和异步传播

当你使用await关键字等待一个任务的时候,你实际上是在告诉编译器: 我只关心这个任务的结果,如果任务失败了,我想知道失败的原因.因此,如果被等待的Task抛出异常,这个异常会从Task中传播出来,并被重新抛出到await表达式所在的上下文中.而上面例子的上下文就是UI线程,所以这里异常就回到了UI线程.

  • 异常的捕获
  1. UI线程上,由于await通常会在捕获它的同一个上下文(例如上面的UI线程)中继续执行,所以异常会被传回到UI线程并在那里抛出.
  2. TaskScheduler.UnobservedTaskException不再触发,因为异常已经被观察处理(通过await机制),所以不会触发TaskScheduler.UnobservedTaskException事件.

三. 总结

关于异常捕获:

  1. 如果是UI线程异常捕获中,可以设置handle = True来标记这个异常已经被处理,防止异常继续往下传递导致程序崩溃
  2. 如果是Task异步任务捕获,可以设置e.SetObserved()防止应用程序终止,表示这个异常已经被观察到,在.Net Core.Net5之前,如果出现未观察的异步任务异常,会导致程序崩溃,虽然后续改变了这一异常的默认行为,但是设置e.SetObserved()依旧是一个良好的编程习惯.e.SetObserved()还有助于告诉应用程序日志或者其他诊断输出中显示为已经处理,从而清晰的知道这些异常已经被关注到.
  3. 如果UI线程上的异常捕获到没有设置为handle=True,异常会继续上抛,然后程序域异常捕获同样会再次捕获到该异常.如果程序异常走到了程序域异常捕获的时候,这个程序就控制不了,最后就会异常关闭.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/545666.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vs2022断点调试怎么看堆栈帧,找异常位置

打一个断点以后&#xff0c;会出现如图报错 我们要怎么找到报错的语句&#xff1f;鼠标点击->堆栈帧->上一行运行的位置->直到找到错误出错如图所示&#xff1a; 跳转到&#xff0c;我们手写的代码&#xff0c;执行出错的位置

【第十五届】蓝桥杯省赛C++b组

今年的蓝桥杯省赛已经结束了&#xff0c;与以往不同&#xff0c;今年又回到了8道题&#xff0c;而22&#xff0c;23年出现了10道题 大家觉得难度怎么样&#xff0c;欢迎进来讨论&#xff0c;博主今年没参加哈&#xff0c;大家聊聊&#xff0c;我听听大家的意见和看法哈 试题A:…

DSP笔记13-时间基准子模块Time base(TB)比较子模块Counter cpmpare(CC)

时间基准子模块Time base(TB) 同步&#xff0c;计数 CTR计数寄存器 PRD周期寄存器 CMP比较寄存器&#xff0c;占空比 EPWMA&#xff0c; EPWMB&#xff0c;两个比较寄存器&#xff0c;但只有以及计数寄存器以及一个周期寄存器 计数模式 计数时钟TBCLK HSPCLKDIVx x0,分…

HBuilderX 中开发vue,引入百度地图获取当前ip地址定位

实现功能&#xff1a;使用百度地图获取IP地址&#xff0c;定位到当前位置 参考文档地址&#xff1a;MapVGL | 快速入门 一、在有外网的情况下&#xff0c;常规引入百度地图的方法如下&#xff1a; 1、在index.html中引入 <script src"//api.map.baidu.com/api?v1.…

element问题总结之el-table使用fixed固定列后滚动条滑动到底部或者最右侧的时候错位问题

el-table使用fixed固定列后滚动条滑动到底部或者最右侧的时候错位 效果图前言解决方案纵向滑动滚动条滑动到底部的错位解决横向滚动条滑动到最右侧的错位解决 效果图 前言 在使用el-table固定行的时候移动滚动条会发现移动到底部或者移动到最右侧的时候会出现表头和内容错位或…

在 Flutter App 中使用 GPS 定位

现代手机上&#xff0c;不论是苹果 iPhone 还是安卓 Android&#xff0c;都配备了强大的定位能力。 定位主要通过卫星和地面基站提供的信号&#xff0c;获得不同精度的定位信息。 通过手机的操作系统&#xff0c;可以获取这些定位信息。这是手机操作系统给应用层提供的能力。…

Udio——革命性的AI音乐生成软件

Udio是一款革命性的AI音乐生成软件&#xff0c;由前谷歌DeepMind的顶尖AI研究人员和工程师共同创立&#xff0c;得到著名风险投资公司a16z的支持。它旨在为音乐爱好者和专业人士提供一个全新的音乐创作和分享平台。用户可以通过文本提示来生成音乐&#xff0c;支持广泛的音乐风…

numpy学习笔记(5),其他实用函数

8. 更多函数 8.1 随机数 8.1.1 常用随机数 8.1.1.1 numpy.random.rand(d0, d1, …, dn) 返回[0.0, 1.0)随机浮点数&#xff0c;即大于等于0.0&#xff0c;小于1.0。d0, d1, …, dn&#xff1a;返回的数组形状 # 使用numpy.random.rand函数 import numpy as np np.random.r…

百元内的运动蓝牙耳机哪个牌子好?五大高分品牌实测推荐

在追求健康生活的当下&#xff0c;运动已成为许多人日常生活的一部分&#xff0c;而音乐更是运动时的最佳伴侣&#xff0c;对于预算有限的学生党或普通消费者来说&#xff0c;如何在百元内挑选到一款性能优越、品质可靠的运动蓝牙耳机&#xff0c;确实是个不小的挑战&#xff0…

(六)C++自制植物大战僵尸游戏关卡数据讲解

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/xjvbb 游戏关卡数据文件定义了游戏中每一个关卡的数据&#xff0c;包括游戏类型、关卡通关奖励的金币数量、僵尸出现的波数、每一波出现僵尸数量、每一波僵尸出现的类型等。根据不同的游戏类型&#xff0c;定义了不同的通…

ATA-214高压放大器用在哪些实验中使用的

高压放大器在科学实验和工程应用中扮演着关键角色。它是一种能够将低电压信号放大到高电压水平的设备。这种放大器通常用于需要处理高电压信号的实验和应用中。以下是高压放大器在各种实验中的应用范围。 粒子物理实验&#xff1a;在粒子物理实验中&#xff0c;科学家使用高压放…

【分享】3种方法取消Word文档的“打开密码”

我们知道&#xff0c;Word文档可以设置“打开密码”&#xff0c;防止文档被随意打开&#xff0c;那后续不需要密码保护了&#xff0c;要怎么取消呢&#xff1f;不小心把密码忘记了还可以取消吗&#xff1f;不清楚的小伙伴一起来看看吧&#xff01; 如果是Word文档不再需要密码…

Python Flask-Security- 构建安全而强大的Web应用

Flask-Security是一个基于Flask的安全扩展&#xff0c;为开发者提供了构建安全且强大的Web应用的工具。本文将深入探讨Flask- Security的核心功能、基本用法以及在实际应用中的一些高级特性&#xff0c;通过丰富的示例代码&#xff0c;助您更全面地了解和应用这一用于Web应用安…

windows的jar包开机自启动【搬代码】

感觉最方便的就是放到启动项目里操作步骤 winR 输入&#xff1a;shell:startup回车或点击确定 3.将自己jar包右键创建快捷方式 4.然后放进去 5.重启电脑&#xff0c;浏览器输入网址&#xff0c;就可以看到重启成功了 另外一个就是放入.exe文件的快捷方式 首先&#xff0c;…

docker安装nessus服务及使用

Nessus 是目前全世界最多人使用的系统漏洞扫描与分析软件&#xff0c;现在软件服务越来越多&#xff0c;越来越复杂&#xff0c;涉及的数据也更多&#xff1b;因此系统完成后对于系统漏洞的检测并对其进行修改十分有必要&#xff0c;本文介绍通过docker安装nessus服务及简单的使…

15 Python进阶: random和pyecharts

Python random 模块主要用于生成随机数。 random 模块实现了各种分布的伪随机数生成器。 要使用 random 函数必须先导入&#xff1a; import randompython random 模块的一般用法 Python中的random模块提供了生成伪随机数的功能&#xff0c;可以用于模拟、游戏开发、密码学…

关于centos8自带的apache2.4开启https后,XP系统的IE8无法显示网页的问题

经检验&#xff0c;是因为系统的apache和openssl版本太高导致的。 禁用系统默认的apache2.4&#xff0c;自己重新源码编译安装一套openssl-1.0.1fapache2.2.23php7.1.2即可。跟update-crypto-policies没有关系&#xff0c;可保持默认的DEFAULT状态。 关于centos8自带的apache2…

多无人机集群协同避障

matlab2020a正常运行 场景1规划结果 场景2规划结果 场景3规划结果 代码地址&#xff1a; 多无人机集群协同避障效果&#xff08;5架&#xff09;资源-CSDN文库

解锁生成式 AI 的力量:a16z 提供的 16 个企业指南

企业构建和采购生成式AI方面的16项改变 生成式 AI 领域趋势洞察&#xff1a;企业构建和采购生成式 AI 的方式正在发生重大转变&#xff0c;具体表现在&#xff1a;* 专注于可信度和安全性&#xff1a;75% 的企业将信任和安全性视为关键因素。* 优先考虑可扩展性和灵活性&#x…

unity shader学习练笔日记(三)

1、单张纹理 Shader "Unity Shaders Study/Day Three/SingleTexture" {Properties{_Color("Colot Tint", Color) (1, 1, 1, 1)//2D是纹理属性的声明方式。以一个字符串后跟一个花括号作为它的初始值&#xff0c;"white"是内置纹理的名字&#…