dotnet 职业技术学院

博客

dotnet 职业技术学院

.NET 设计一套高性能的弱事件机制

dotnet 职业技术学院 发布于 2019-09-18

弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。网上有很多弱事件的 NuGet 包,不过仅仅支持定义事件的时候写成弱事件而不支持让任意事件变成弱事件,并且存在性能问题。

本文将设计一套弱事件机制,不止可以让任意一个 CLR 事件成为弱事件,还具有近乎原生事件的性能。


系列博客:

场景与问题

本文主要为了设计一套弱事件机制而编写,因此如果你感兴趣,应该已经理解了我试图做什么事情。

当然,如果并不理解,可以阅读这个机制的应用篇,里面有具体的应用场景:

现有设计

在我进行此设计之前,已有如下种类的弱事件机制:

  1. WPF 框架自带的 WeakEventManager
    • 功能非常有限,自己继承实现一个的难度非常高,但具有很高的性能;WPF 绑定等机制的底层实现用到了这个类型。
  2. WPF 框架自带的泛型类 WeakEventManager<TEventSource, TEventArgs>
    • 可以让你更容易地实现一个自己的弱事件,但是性能非常差
  3. 使用网上很多的 NuGet 包
    • 下载量较高的几个 NuGet 包我都有研究过其中的源代码,要么有限制必须是定义事件的时候就必须使用弱事件,要么使用反射或其他动态调用方法性能较差
  4. StackOverflow 上关于 Weak Event 的高赞回答
    • 目前还没有找到可以支持将任意事件添加弱事件支持的回答

由于我希望编写的弱事件机制尽可能减少对非预期框架的依赖,而且具有很高的性能,所以我打算自己实现一套。

设计原则

  1. 支持为任意类型的事件添加弱事件支持,而不只是自己定义新事件的时候可以使用(对标主流 NuGet 包和 StackOverflow 上的回答)
  2. 具有很高的性能(对标主流的 NuGet 包和 WPF 泛型版本的 WeakEventManager)
  3. 类的使用者只需要编写极少量的代码就能完成(对标 WPF 非泛型版本的 WeakEventManager)

这三个原则,从上到下优先级依次降低。

要支持所有类型的 CLR 事件,意味着我的设计中必须要能够直接监听到任意事件,而不能所有代码都从我自己编写的代码开始。

要有很高的性能,就意味着我几乎不能使用“反射”,也不能使用委托的 DynamicInvoke 方法,还不能生成 IL 代码(首次生成很慢),也不能使用表达式树(首次编译很慢)。那么可以使用的也就只剩下两个了,一个是纯 C#/.NET 带的编译期就能确定执行的代码,另一个是使用 Roslyn 编译期在编译期间进行特殊处理。

类的使用者要编写极少量的代码,意味着能够抽取到框架中的代码就尽量抽取到框架中。

取名

俗话说,一个好的名字是成功的一半。

因为我希望为任意 CLR 事件添加弱事件支持,所以其职责有点像“代理、中间人、中继、中转”,对应英文的 Proxy Agent Relay Transfer。最终我选择名称 Relay(中继),因为好听。

API 设计

对于 API 的设计,我有一个小原则:

  • 如果技术实现很难,那么 API 迁就技术实现;如果技术实现很容易,那么技术迁就 API

我总结了好的 API 设计的一些原则:

不得不说,此类型设计的技术难度还是挺大的。虽然我们知道有 WeakReference<T> 可用,但依然存在很多的技术难点。于是 API 的设计可能要退而求其次优先满足前两个优先级更高的目标。

我们期望 API 足够简单,因此在几个备选方案中选择:

  1. WeakEventRelay.Subscribe("Changed", OnChanged)
    • 使用字符串来表示事件,肯定会用到反射,不可取
  2. WeakEventRelay.Subscribe(o => o.Changed, OnChanged)
    • 如果使用 Action 来做,会遇到 o.Changed 必须出现在 += 左边的编译错误
    • 如果使用表达式树,也一样会遇到 o.Changed 必须出现在 += 左边的编译错误,同时还会出现少量性能问题

因此,直接一个方法就能完成事件注册是不可能的了,我们改用其他方法——继承自某个基类:

internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
    public event FileSystemEventHandler Changed
    {
        add => /*实现弱事件订阅*/;
        remove => /*实现弱事件注销*/;
    }
}

那么实现的难点就都在 addremove 方法里面了。

技术实现

我们究竟需要哪些信息才可以完成弱事件机制呢?

  • 事件源(也就是在使用弱事件机制之前最原始的事件引发者,经常以 object sender 的形式出现在你的代码中)
  • 要订阅的事件(比如 FileSystemWatcher.Changed 事件)
  • 新注册的事件处理函数(也就是 addremove 方法中的 value

然而事情并没有那么简单:

在框架通用代码中,我不可能获取到要订阅的事件。因为事件要求只能出现在 += 的左边,不能以任何其他形式使用(包括但不限于通过参数传递,伪装成 Lambda 表达式,伪装成表达式树)。这意味着 o.Changed += OnChanged 这样的事件订阅完全写不出来通用代码(除非牺牲性能)。

那么还能怎么做呢?只能将这段写不出来的代码留给业务编写者来编写了。

也就是说,类似于 o.Changed += OnChanged 这样的代码只能交给业务开发者来实现。与此同时也注定了 OnChanged 必须由业务开发者编写(因为无法写出通用的高性能的事件处理函数,并且还能在 +=-= 的时候保持同一个实例。

我没有办法通过抽象的办法引发一个事件。具体来说,无法在抽象的通用代码中写出 Changed.Invoke(sender, e) 这样代码。因为委托的基类 Delegate MultiCastDelegate 没有 Invoke 方法可以使用,只有耗性能的 DynamicInvoke 方法。各种不同的委托定义虽然可以有相同的参数和返回值类型,但是却不能相互转换,因此我也不能将传入的委托转换成 Action<TSender, TArgs> 这样的通用委托。

庆幸的是,C# 提供了将方法组隐式转换委托的方法,可以让两个参数和返回值类型相同的委托隐式转换。但注意,这是隐式转换,没有运行时代码可以高性能地完成这件事情。

addremove 方法中,value 参数就是使用方传入的事件处理函数,value.Invoke 就是方法组,可以隐式转换为通用的 Action<TSender, TArgs>

这意味着,我们可以将 value.Invoke 传入来以通用的方式调用事件处理函数。但是请特别注意,这会导致新创建委托实例,导致 -= 的时候实例与 += 的时候不一致,无法注销事件。因此,我们除了传入 value.Invoke 之外,还必须传入 value 本身。

API 半残品预览

internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
    public event FileSystemEventHandler Changed
    {
        add => Subscribe(o => o.Changed += OnChanged, value, value.Invoke);
        remove => Unsubscribe(o => o.Changed -= OnChanged, value);
    }

    private void OnChanged(object sender, FileSystemEventArgs e) => /* 引发弱事件 */;
}

这已经开始让业务方的代码变得复杂起来了。

方案完善

我们还需要能够注册、注销和引发弱事件,而这部分就没那么坑了。因为:

  1. 我们已经把最坑的 o.Changed += OnChangedvaluevalue.Invoke 都传进来了;
  2. 在类型中定义一个弱事件,目前网上各种主流弱事件 NuGet 包都有实现。

我写了一个 WeakEvent<TSender, TArgs> 泛型类专门用来定义弱事件。

不过,这让业务方的代码压力更大了:

internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
    private readonly WeakEvent<FileSystemEventArgs> _changed = new WeakEvent<FileSystemEventArgs>();

    public event FileSystemEventHandler Changed
    {
        add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
        remove => _changed.Remove(value);
    }

    private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e);
}

最后,订阅事件所需的实例,我认为最好不要能够让业务方直接能访问。因为弱事件的实现并不简单(看上面如此复杂的公开 API 就知道了),如果能够直接访问,势必带来更复杂的使用问题。所以我仅在部分方法和 Lambda 表达式参数中开放实例。

所以,构造函数需要传入事件源。

最后的问题

最后还留下了一个问题

  • 订阅者现在确实“弱事件”了,但这个“中继”怎么办?可是被强引用了啊?

虽然中继的类实例小得多,但这确实依然也是泄漏,因此需要回收。

于是我在任何可能执行代码的时机加上了回收检查:如果发现所有订阅者都已经被回收,那么“中继”也就可以被回收了,将注销所有事件源的订阅。(当然要允许重新开始订阅。)

所以最后业务方编写的中继代码又多了一些:

using System.IO;
using Walterlv.WeakEvents;

namespace Walterlv.Demo
{
    internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
    {
        public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }

        private readonly WeakEvent<FileSystemEventArgs> _changed = new WeakEvent<FileSystemEventArgs>();

        public event FileSystemEventHandler Changed
        {
            add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
            remove => _changed.Remove(value);
        }

        private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e);

        protected override void OnReferenceLost(FileSystemWatcher source)
        {
            source.Changed -= OnChanged;
        }
    }
}

实际使用

虽然弱事件中继的代码复杂了点,但是:

1 最终用户的使用可是非常简单的:

public class WalterlvDemo
{
    public WalterlvDemo()
    {
        _watcher = new FileSystemWatcher(@"D:\Desktop\walterlv.demo.md")
        {
            EnableRaisingEvents = true,
        };
        _watcher.Created += OnCreated;
        _watcher.Changed += OnChanged;
        _watcher.Renamed += OnRenamed;
        _watcher.Deleted += OnDeleted;
    }

    private readonly FileSystemWatcher _watcher;
    private void OnCreated(object sender, FileSystemEventArgs e) { }
    private void OnChanged(object sender, FileSystemEventArgs e) { }
    private void OnRenamed(object sender, RenamedEventArgs e) { }
    private void OnDeleted(object sender, FileSystemEventArgs e) { }
}

2 是在懒得写,我可以加上 Roslyn 编译器生成中继代码的方式,这个我将在不久的将来加入到 Walterlv.WeakEvents 库中。

相关源码

更具体的使用场景和示例代码,请阅读:

本文所涉及的全部源代码,已在 GitHub 上开源:

注意开源协议:

996.icu

LICENSE


参考资料

.NET/C# 利用 Walterlv.WeakEvents 高性能地中转一个自定义的弱事件(可让任意 CLR 事件成为弱事件)

dotnet 职业技术学院 发布于 2019-09-18

弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。网上有很多弱事件的 NuGet 包,不过仅仅支持定义事件的时候写成弱事件而不支持让任意事件变成弱事件,并且存在性能问题。

本文介绍 Walterlv.WeakEvents 库来做弱事件。你可以借此将任何一个 CLR 事件当作弱事件来使用。


系列博客:

场景与问题

了解一下场景,你就能知道这是否是适合你的方案。

比如我正在使用 FileSystemWatcher 来监听一个文件的改变,我可能会使用到这些事件:

  • Created 在文件被创建时引发
  • Changed 在文件内容或属性发生改变时引发
  • Renamed 在文件被重命名时引发
  • Deleted 在文件被删除时引发

更具体一点的代码是这样的:

public class WalterlvDemo
{
    public WalterlvDemo()
    {
        _watcher = new FileSystemWatcher(@"D:\Desktop\walterlv.demo.md")
        {
            EnableRaisingEvents = true,
        };
        _watcher.Created += OnCreated;
        _watcher.Changed += OnChanged;
        _watcher.Renamed += OnRenamed;
        _watcher.Deleted += OnDeleted;
    }

    private readonly FileSystemWatcher _watcher;
    private void OnCreated(object sender, FileSystemEventArgs e) { }
    private void OnChanged(object sender, FileSystemEventArgs e) { }
    private void OnRenamed(object sender, RenamedEventArgs e) { }
    private void OnDeleted(object sender, FileSystemEventArgs e) { }
}
private void Foo()
{
    var demo = new WalterlvDemo();
    // 使用 demo
    // 此方法结束后,demo 将脱离作用域,本应该可以被回收的。
}

但是,一旦我们这么写,那么我们这个类型 WalterlvDemo 的实例 demo 将无法被回收,因为 FileSystemWatcher 将始终通过事件引用着这个实例。即使你已经不再引用这个类型的任何一个实例,此实例也会被 _watcher 的事件引用着,而 FileSystemWatcher 的实例也因为 EnableRaisingEvents 而一直存在。

一个可行的解决办法是调用 FileSystemWatcherDispose 方法。不过有些时候很难决定到底在什么时机调用 Dispose 合适。

现在,我们希望有一种方法,能够在 WalterlvDemo 的实例失去作用域后被回收,最好 FileSystemWatcher 也能够自动被 Dispose 释放掉。

如果你试图解决的是类似这样的问题,那么本文就可以帮到你。

总结一下:

  1. 用到了一个现有的类型(你无法修改它的源代码,本例中是 FileSystemWatcher);
  2. 你无法决定什么时候释放此类型的实例(本例中是不知道什么时候调用 Dispose);
  3. 一旦你监听此类型的事件,将产生内存泄漏,导致你自己类型的实例无法释放(本例中是 demo 变量脱离作用域。)。

目前有 WPF 自带的 WeakEventManager 机制,网上也有很多可用的 NuGet 包,但是都有限制:

  1. 只能给自己定义的类型引入弱事件机制,不能给现有类型引入弱事件;
  2. 要么用反射,要么用 IL 生成代码,性能都不高。

而 Walterlv.WeakEvents 除了解决了给任一类型引入弱事件的问题,还具有非常高的性能,几乎跟定义原生事件无异。

下载安装 Walterlv.WeakEvents

在你需要做弱事件的项目中安装 NuGet 包:

编写自定义的弱事件中继

现在,我们需要编写一个自定义的弱事件中继类 FileSystemWatcherWeakEventRelay,即专门为 FileSystemWatcher 做的弱事件中继。

下面是一个简单点的例子,为其中的 Changed 事件做了一个中继:

using System.IO;
using Walterlv.WeakEvents;

namespace Walterlv.Demo
{
    internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
    {
        public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }

        private readonly WeakEvent<FileSystemEventArgs> _changed = new WeakEvent<FileSystemEventArgs>();

        public event FileSystemEventHandler Changed
        {
            add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
            remove => _changed.Remove(value);
        }

        private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e);

        protected override void OnReferenceLost(FileSystemWatcher source)
        {
            source.Changed -= OnChanged;
        }
    }
}

你可能会看到代码有点儿多,但是我向你保证,这是除了采用 Roslyn 编译器技术以外最高性能的方案了。如果你对弱事件的性能有要求,那么还是接受这些代码会比较好。

不要紧张,我来一一解释这些代码。另外,如果你不想懂这些代码,就按照模板一个个敲就好了,都是模板化的代码(特别适合使用 Roslyn 编译器生成,我可能接下来就会做这件事情避免你写出这些代码)。

  1. 首先,我们定义了一个自定义的弱事件中继 FileSystemWatcherWeakEventRelay,继承自库 Walterlv.WeakEvents 中的 WeakEventRelay<FileSystemWatcher> 类型。带上的泛型参数表明是针对 FileSystemWatcher 类型做弱事件中继。
  2. 一个构造函数,将参数传递给基类:public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }。这个构造函数是可以用 Visual Studio 生成的,快捷键是 Ctrl + . 或者 Alt + Enter(快捷键功效详见:提高使用 Visual Studio 开发效率的键盘快捷键
  3. 定义了一个私有的 WeakEvent<FileSystemEventArgs>,名为 _changed,这个就是弱事件的核心。泛型参数是事件参数的类型(注意,为了极致的性能,这里的泛型参数是事件参数的名称,而不是大多数弱事件框架中提供的事件处理委托类型)。
  4. 定义了一个对外公开的事件 public event FileSystemEventHandler Changed
    • add 方法固定调用 Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));。其中 ChangedFileSystemWatcher 中的事件,OnChanged 是我们即将定义的事件处理函数,_changed 是前面定义好的弱事件字段,而后面的 valuevalue.Invoke 是固定写法。
    • remove 方法固定调用弱事件的 Remove 方法,即 _changed.Remove(value);
  5. 编写针对公开事件的事件处理函数 OnChanged,并在里面固定调用 TryInvoke(_changed, sender, e)
  6. 重写 OnReferenceLost 方法,用于在对象已被回收后反注册 FileSystemWatcher 中的事件。

希望看了上面这 6 点之后你还能理解这些代码都是在做啥。如果依然不能理解,可以考虑:

  1. 参考下面 FileSystemWatcherWeakEventRelay 的完整代码来理解哪些是可变部分哪些是不可变部分,自己替换就好;
  2. 等待 Walterlv.WeakEvents 库的作者更新自动生成这段代码的功能。
using System.IO;
using Walterlv.WeakEvents;

namespace Walterlv.Demo
{
    internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
    {
        public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }

        private readonly WeakEvent<FileSystemEventArgs> _created = new WeakEvent<FileSystemEventArgs>();
        private readonly WeakEvent<FileSystemEventArgs> _changed = new WeakEvent<FileSystemEventArgs>();
        private readonly WeakEvent<RenamedEventArgs> _renamed = new WeakEvent<RenamedEventArgs>();
        private readonly WeakEvent<FileSystemEventArgs> _deleted = new WeakEvent<FileSystemEventArgs>();

        public event FileSystemEventHandler Created
        {
            add => Subscribe(o => o.Created += OnCreated, () => _created.Add(value, value.Invoke));
            remove => _created.Remove(value);
        }

        public event FileSystemEventHandler Changed
        {
            add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
            remove => _changed.Remove(value);
        }

        public event RenamedEventHandler Renamed
        {
            add => Subscribe(o => o.Renamed += OnRenamed, () => _renamed.Add(value, value.Invoke));
            remove => _renamed.Remove(value);
        }

        public event FileSystemEventHandler Deleted
        {
            add => Subscribe(o => o.Deleted += OnDeleted, () => _deleted.Add(value, value.Invoke));
            remove => _deleted.Remove(value);
        }

        private void OnCreated(object sender, FileSystemEventArgs e) => TryInvoke(_created, sender, e);
        private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e);
        private void OnRenamed(object sender, RenamedEventArgs e) => TryInvoke(_renamed, sender, e);
        private void OnDeleted(object sender, FileSystemEventArgs e) => TryInvoke(_deleted, sender, e);

        protected override void OnReferenceLost(FileSystemWatcher source)
        {
            source.Created -= OnCreated;
            source.Changed -= OnChanged;
            source.Renamed -= OnRenamed;
            source.Deleted -= OnDeleted;
            source.Dispose();
        }
    }
}

使用自定义的弱事件中继

当你把上面这个自定义的弱事件中继类型写好了之后,使用它就非常简单了,对我们原有的代码改动非常小。

    public class WalterlvDemo
    {
        public WalterlvDemo()
        {
            _watcher = new FileSystemWatcher(@"D:\Desktop\walterlv.demo.md")
            {
                EnableRaisingEvents = true,
            };
++          var weakEvent = new FileSystemWatcherWeakEventRelay(_watcher);
--          _watcher.Created += OnCreated;
--          _watcher.Changed += OnChanged;
--          _watcher.Renamed += OnRenamed;
--          _watcher.Deleted += OnDeleted;
++          weakEvent.Created += OnCreated;
++          weakEvent.Changed += OnChanged;
++          weakEvent.Renamed += OnRenamed;
++          weakEvent.Deleted += OnDeleted;
        }

        private readonly FileSystemWatcher _watcher;
        private void OnCreated(object sender, FileSystemEventArgs e) { }
        private void OnChanged(object sender, FileSystemEventArgs e) { }
        private void OnRenamed(object sender, RenamedEventArgs e) { }
        private void OnDeleted(object sender, FileSystemEventArgs e) { }
    }

最终效果预览

我写了一个程序,每 1 秒修改一次文件;每 5 秒回收一次内存。然后使用 FileSystemWatcher 来监视这个文件的改变。

可以看到,在回收内存之后,将不会再监视文件的改变。当然,如果你期望一直可以监视改变,当然也不希望用到本文的弱事件。

可以回收事件

为什么弱事件中继的 API 如此设计?

一句话解答:为了高性能

请参见我的另一篇博客:


参考资料

C#/.NET 中启动进程时所使用的 UseShellExecute 设置为 true 和 false 分别代表什么意思?

dotnet 职业技术学院 发布于 2019-09-17

在 .NET 中创建进程时,可以传入 ProcessStartInfo 类的一个新实例。在此类型中,有一个 UseShellExecute 属性。

本文介绍 UseShellExecute 属性的作用,设为 truefalse 时,分别有哪些进程启动行为上的差异。


本质差异

Process.Start 本质上是启动一个新的子进程,不过这个属性的不同,使得启动进程的时候会调用不同的 Windows 的函数。

当然,如果你知道这两个函数的区别,那你自然也就了解此属性设置为 truefalse 的区别了。

效果差异

ShellExecute 的用途是打开程序或者文件或者其他任何能够打开的东西(如网址)。

也就是说,你可以在 Process.Start 的时候传入这些:

  • 一个可执行程序(exe)
  • 一个网址
  • 一个 html / mp4 / jpg / docx / enbx 等各种文件
  • PATH 环境变量中的各种程序

不过,此方法有一些值得注意的地方:

  • 不支持重定向输入和输出
  • 最终启动了哪个进程可能是不确定的,你可能需要注意潜在的安全风险

CreateProcess 则会精确查找路径来执行,不支持各种非可执行程序的打开。但是:

  • 支持重定向输入和输出

如何选择

UseShellExecute 在 .NET Framework 中的的默认值是 true,在 .NET Core 中的默认值是 false

如果有以下需求,那么建议设置此值为 false

  • 需要明确执行一个已知的程序
  • 需要重定向输入和输出

如果你有以下需求,那么建议设置此值为 true 或者保持默认:

  • 需要打开文档、媒体、网页文件等
  • 需要打开 Url
  • 需要打开脚本执行
  • 需要打开计算机上环境变量中路径中的程序

参考资料

.NET Framework 的 bug?try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃

dotnet 职业技术学院 发布于 2019-09-12

在 .NET Framework 4.8 中,try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃。而 .NET Core 3.0 中不会出现这样的问题。

本文涉及的 Bug 已经报告给了微软,并且得到了微软的回复。是 .NET Framework 4.8 为了解决一个安全性问题而强行结束了进程。


This post is written in multiple languages. Please select yours:

官方文档中 when 的行为

你可以前往官方文档:

在其中,你可以找到这样一段话:

用户筛选的子句的表达式不受任何限制。 如果在执行用户筛选的表达式期间发生异常,则将放弃该异常,并视筛选表达式的值为 false。 在这种情况下,公共语言运行时继续搜索当前异常的处理程序。

即当 when 块中出现异常时,when 表达式将视为值为 false,并且此异常将被忽略。

示例程序

鉴于官方文档中的描述,我们可以编写一些示例程序来验证这样的行为。

using System;
using System.IO;

namespace Walterlv.Demo.CatchWhenCrash
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            try
            {
                try
                {
                    Console.WriteLine("Try");
                    throw new FileNotFoundException();
                }
                catch (FileNotFoundException ex) when (ex.FileName.EndsWith(".png"))
                {
                    Console.WriteLine("Catch 1");
                }
                catch (FileNotFoundException)
                {
                    Console.WriteLine("Catch 2");
                }
            }
            catch (Exception)
            {
                Console.WriteLine("Catch 3");
            }
            Console.WriteLine("End");
        }
    }
}

很显然,我们直接 new 出来的 FileNotFoundExceptionFileName 属性会保持为 null。对其解引用会产生 NullReferenceException。很显然代码不应该这么写,但可以用来验证 catch-when 语句的行为。

按照官网描述,输出应该为 Try-Catch 2-End。因为 when 中的异常被忽略,因此不会进入到外层的 catch 块中;因为 when 中出现异常导致表达式值视为 false,因此进入了更合适的异常处理块 Catch 2 中。

在 .NET Core 3.0 中的行为和 .NET Framework 4.8 中的行为

下面两张图分别是这段代码在 .NET Core 3.0 和 .NET Framework 4.8 中的输出:

.NET Core 3.0 中的行为

.NET Framework 4.8 中的行为

可以注意到,只有 .NET Core 3.0 中的行为符合官方文档的描述,而 .NET Framework 4.8 中甚至连 End 都没有输出!几乎可以确定,程序在 .NET Framework 4.8 中出现了致命的崩溃!

如果我们以 Visual Studio 调试启动此程序,可以看到抛出了 CLR 异常:

抛出了 CLR 异常

以下是在 Visual Studio 中单步跟踪的步骤:

单步调试

Issue 和行为

由于本人金鱼般的记忆力,我竟然给微软报了三次这个 Bug:

此问题是 .NET Framework 4.8 为了修复一个安全性问题才强行结束了进程:

Process corrupting exceptions in exception filter (like access violation) now result in aborting the current process. [110375, clr.dll, Bug, Build:3694]

请参见:

如何在 WPF 中获取所有已经显式赋过值的依赖项属性

dotnet 职业技术学院 发布于 2019-09-12

获取 WPF 的依赖项属性的值时,会依照优先级去各个级别获取。这样,无论你什么时候去获取依赖项属性,都至少是有一个有效值的。有什么方法可以获取哪些属性被显式赋值过呢?如果是 CLR 属性,我们可以自己写判断条件,然而依赖项属性没有自己写判断条件的地方。

本文介绍如何获取以及显式赋值过的依赖项属性。


需要用到 DependencyObject.GetLocalValueEnumerator() 方法来获得一个可以遍历所有依赖项属性本地值。

public static void DoWhatYouLikeByWalterlv(DependencyObject dependencyObject)
{
    var enumerator = dependencyObject.GetLocalValueEnumerator();
    while (enumerator.MoveNext())
    {
        var entry = enumerator.Current;
        var property = entry.Property;
        var value = entry.Value;
        // 在这里使用 property 和 value。
    }
}

这里的 value 可能是 MarkupExtension 可能是 BindingExpression 还可能是其他一些可能延迟计算值的提供者。因此,你不能在这里获取到常规方法获取到的依赖项属性的真实类型的值。

但是,此枚举拿到的所有依赖项属性的值都是此依赖对象已经赋值过的依赖项属性的本地值。如果没有赋值过,将不会在这里的遍历中出现。


参考资料

在 WPF 中获取一个依赖对象的所有依赖项属性

dotnet 职业技术学院 发布于 2019-09-12

本文介绍如何在 WPF 中获取一个依赖对象的所有依赖项属性。


通过 WPF 标记获取

public static IEnumerable<DependencyProperty> EnumerateDependencyProperties(object element)
{
    if (element is null)
    {
        throw new ArgumentNullException(nameof(element));
    }

    MarkupObject markupObject = MarkupWriter.GetMarkupObjectFor(element);
    if (markupObject != null)
    {
        foreach (MarkupProperty mp in markupObject.Properties)
        {
            if (mp.DependencyProperty != null)
            {
                yield return mp.DependencyProperty;
            }
        }
    }
}

public static IEnumerable<DependencyProperty> EnumerateAttachedProperties(object element)
{
    if (element is null)
    {
        throw new ArgumentNullException(nameof(element));
    }

    MarkupObject markupObject = MarkupWriter.GetMarkupObjectFor(element);
    if (markupObject != null)
    {
        foreach (MarkupProperty mp in markupObject.Properties)
        {
            if (mp.IsAttached)
            {
                yield return mp.DependencyProperty;
            }
        }
    }
}

通过设计器专用方法获取

本来 .NET 中提供了一些专供设计器使用的类型 TypeDescriptor 可以帮助设计器找到一个类型或者组件的所有可以设置的属性,不过我们也可以通过此方法来获取所有可供使用的属性。

下面是带有重载的两个方法,一个传入类型一个传入实例。

/// <summary>
/// 获取一个对象中所有的依赖项属性。
/// </summary>
public static IEnumerable<DependencyProperty> GetDependencyProperties(object instance)
    => TypeDescriptor.GetProperties(instance, new Attribute[] { new PropertyFilterAttribute(PropertyFilterOptions.All) })
        .OfType<PropertyDescriptor>()
        .Select(x => DependencyPropertyDescriptor.FromProperty(x)?.DependencyProperty)
        .Where(x => x != null);

/// <summary>
/// 获取一个类型中所有的依赖项属性。
/// </summary>
public static IEnumerable<DependencyProperty> GetDependencyProperties(Type type)
    => TypeDescriptor.GetProperties(type, new Attribute[] { new PropertyFilterAttribute(PropertyFilterOptions.All) })
        .OfType<PropertyDescriptor>()
        .Select(x => DependencyPropertyDescriptor.FromProperty(x)?.DependencyProperty)
        .Where(x => x != null);

参考资料

临时编写和调试 C++ 代码?用 VSCode 就够了!一分钟搭好 C++ 调试环境

dotnet 职业技术学院 发布于 2019-09-07

突然间要编写或者调试几个 C++ 的小程序,动用 Visual Studio 创建一个解决方案显得大了些。如果能够利用随时随地就方便打开的 Visual Studio Code 来开发,则清爽很多。

本文教你一分钟在 Visual Studio Code 中搭建好 C++ 开发环境。


本文大纲

本文总共分为三个步骤,每个步骤都非常简单。

第一步:安装扩展

你需要在 Visual Studio Code 中安装 C/C++ 扩展。

安装 C++ 扩展

第二步:启动 VSCode

注意,安装完成后,要通过 Visual Studio 自带的 Developer Command Prompt for VS 2019 来启动 Visual Studio Code。这样才可以获得 Visual Studio 2019 自带的各种编译工具路径的环境变量。Visual Studio Code 就可以无缝使用 Visual Studio 2019 附带的那些工具。

启动 Developer Command Prompt for VS 2019

然后,在新启动的命令行工具中启动 Visual Studio Code。

输入 code 即可启动:

> code

如果已有线程的路径,可以带上路径的命令行参数:

> code C:\Users\lvyi\Desktop\Walterlv.CppDemo

启动 Visual Studio Code

第三步:F5 运行

随便在目录中新建一个文件,写上 C++ 代码。比如在 example.cpp 文件中写上如下代码:

#include<iostream>
using namespace std;

int main()
{
    cout<<"welcome to blog.walterlv.com";
    return 0;
}

按下 F5,选择对应的 C++ 编译平台(我这里选择 C++ (Windows)),然后选择 cl.exe build and debug active file

选择编译平台

cl.exe build and debug active file 的目的是调试当前激活的文件,这样的调试方式在 python/java 等语言中大家屡见不鲜,好处是对于小型代码调试起来非常简单直接。

选择调试当前文件

接下来 Visual Studio Code 就会生成一些调试所需的配置文件。

再次按下 F5,Visual Studio Code 会提示没有编译任务,点击 Configure Task,随后选择 C/C++: cl.exe build active file

Configure Task

C/C++: cl.exe build active file

接下来 Visual Studio Code 就会生成一些编译所需的配置文件。

再次按下 F5 就可以直接编译 example.cpp 文件然后运行调试了。

调试当前文件

输出在 Debug Console 里面:

Debug Console

其他注意事项

如果你给 Visual Studio 设置了非默认的终端,那么需要注意:

  • 应该使用 PowerShell 系列的终端(例如 pwsh)不能使用 bash 系列的终端。因为 Windows 下工具使用的路径格式是反斜杠 \,而 bash 系列终端使用的路径是斜杠 /。如果使用 bash 终端,编译工具会因为路径问题导致编译失败。

另外,不要怪我说我是这么编写教程的:

首先,我们已知 1+1=2

1+1=2

于是可以推导出……

推导出

提高使用 Visual Studio 开发效率的键盘快捷键

dotnet 职业技术学院 发布于 2019-09-07

Visual Studio 的功能可谓真是丰富,再配合各种各样神奇强大的插件,Visual Studio 作为太阳系最强大的 IDE 名副其实。

如果你能充分利用起 Visual Studio 启用这些功能的快捷键,那么效率也会很高。


建议记住

功能 快捷键 建议修改成
重构 Ctrl + . Alt + Enter
转到所有 Ctrl + , Ctrl + N
重命名 F2  
打开智能感知列表 Ctrl + J Alt + 右
注释 Ctrl + K, Ctrl + C  
取消注释 Ctrl + K, Ctrl + U  
保存全部文档 Ctrl + K, S  
折叠成大纲 Ctrl + M, Ctrl + O  
展开所有大纲 Ctrl + M, Ctrl + P  
加入书签 Ctrl + K, Ctrl + K  
上一书签 Ctrl + K, Ctrl + P  
下一书签 Ctrl + K, Ctrl + N  
切换自动换行 Alt + Z  

万能重构

你可以不记住本文的其他任何快捷键,但这个你一定要记住,那就是:

Ctrl + .

当然,因为中文输入法会占用这个快捷键,所以我更喜欢将这个快捷键修改一下,改成:

Alt + Enter

修改方法可以参见:如何快速自定义 Visual Studio 中部分功能的快捷键

它的功能是“快速操作和重构”。你几乎可以在任何代码上使用这个快捷键来快速修改你的代码。

比如修改命名空间:

修改命名空间

比如提取常量或变量:

提取常量

比如添加参数判空代码:

参数判空

还有更多功能都可以使用此快捷键。而且因为 Roslyn 优秀的 API,有更多扩展可以使用此快捷键生效,详见:基于 Roslyn 同时为 Visual Studio 插件和 NuGet 包开发 .NET/C# 源代码分析器 Analyzer 和修改器 CodeFixProvider

转到所有

不能每次都去解决方案里面一个个找文件,对吧!所以一个快速搜索文件和符号的快捷键也是非常能够提升效率的。

Ctrl + , 转到所有(go to all)

不过我建议将其改成:

Ctrl + N 这是 ReSharper 默认的转到所有(Goto Everything)的快捷键

这可以帮助你快速找到整个解决方案中的所有文件或符号,看下图:

转到所有

修改方法可以参见:如何快速自定义 Visual Studio 中部分功能的快捷键,下图是此功能的命令名称 编辑.转到所有Edit.GoToAll):

编辑.转到所有

有一些小技巧:

  • 你可以无需拼写完整个单词就找到你想要的符号
    • 例如输入 mw 就可以找到 MainWindow
  • 对于两个以上单词拼成的符号,建议将每个单词的首字母输入成大写,这样可以提高目标优先级,更容易找到
    • 例如 PrivateTokenManager,如果希望干扰少一些,建议输入 PTM 而不是 ptm;当然想要更少的干扰,可以打更多的字母,例如 priToM 等等

注意到上面的界面里面右上角有一些过滤器吗?这些过滤器有单独的快捷键。这样就直接搜索特定类型的符号,而不是所有了,可以提高查找效率。

Ctrl + O 查找当前文件中的所有成员(只搜一个文件,这可以大大提高命中率) Ctrl + T 转到符号(只搜类型名称、成员名称) Ctrl + G 查找当前文件的行号(比如你在代码审查中看到一行有问题的代码,得知行号,可以迅速跳转到这一行)

重构

重命名

F2

重命名

如果你在一个标识符上直接重新输入改了名字,也可以通过 Ctrl + . 或者 Alt + Enter 完成重命名。

其他

这些都可以被最上面的 Ctrl + . 或者 Alt + Enter 替代,因此都可以忘记。

Ctrl + R, Ctrl + E 封装字段
Ctrl + R, Ctrl + I 提取接口
Ctrl + R, Ctrl + V 删除参数
Ctrl + R, Ctrl + O 重新排列参数

IntelliSense 自动完成列表

智能感知

IntelliSense 以前有个漂亮的中文名字,叫做“智能感知”,不过现在大多数的翻译已经与以前的另一个平淡无奇的功能结合到了一起,叫做“自动完成列表”。Visual Studio 默认只会让智能感知列表发挥非常少量的功能,如果你不进行一些配置,使用起来会“要什么没什么”,想显示却不显示。

请通过另一篇博客中的内容把 Visual Studio 的智能感知列表功能好好配置一下,然后我们才可以再次感受到它的强大(记得要翻到最后哦):

如果还有一些时机没有打开智能感知列表,可以配置一个快捷键打开它,我这边配置的快捷键是 Alt + 右

设置打开智能感知的快捷键

参数信息

Ctrl + Shift + 空格

显示方法的参数信息。

显示参数信息

默认在输入参数的时候就已经会显示了;如果错过了,可以在输入 , 的时候继续出现;如果还错过了,可以使用此快捷键出现。

编写

代码格式化

Ctrl + K, Ctrl + E 全文代码清理(包含全文代码格式化以及其他功能) Shift + Alt + F 全文代码格式化 Ctrl + K, Ctrl + F 格式化选定的代码

关于代码清理,你可以配置做哪些事情:

配置代码清理

配置代码清理

其他

Ctrl + K, Ctrl + / 将当前行注释或取消注释
Ctrl + K, Ctrl + C 将选中的代码注释掉
Ctrl + K, Ctrl + UCtrl + Shift + / 将选定的内容取消注释

Ctrl + U 将当前选中的所有文字转换为小写(请记得配合 F2 重命名功能使用避免编译不通过)
Ctrl + ] 增加行缩进
Ctrl + [ 减少行缩进

Ctrl + S 保存文档 Ctrl + K, S 保存全部文档(注意按键,是按下 Ctrl + K 之后所有按键松开,然后单按一个 S

导航

Ctrl + F 打开搜索面板开始强大的搜索功能
Ctrl + H 打开替换面板,或展开搜索面板为替换面板
Ctrl + I 渐进式搜索(就像 Ctrl + F 一样,不过不会抢焦点,搜索完按回车键即完成搜索,适合键盘党操作)
Ctrl + Shift + F 打开搜索窗口(与 Ctrl + F 虽然功能重合,但两者互不影响,意味着你可以充分这两套搜索来执行两套不同的搜索配置)
Ctrl + Shift + H 打开替换窗口(与 Ctrl + H 虽然功能重合,但两者互不影响,意味着你可以充分这两套替换来执行两套不同的替换配置)
Alt + 下 在当前文件中,将光标定位到下一个方法
Alt + 上 在当前文件中,将光标定位到上一个方法

Ctrl + M, Ctrl + M 将光标当前所在的类/方法切换大纲的展开或折叠
Ctrl + M, Ctrl + L 将全文切换大纲的展开或折叠(如果当前有任何大纲折叠了则全部展开,否则全部折叠)
Ctrl + M, Ctrl + P 将全文的大纲全部展开
Ctrl + M, Ctrl + U 将光标当前所在的类/方法大纲展开
Ctrl + M, Ctrl + O 将全文的大纲都折叠到定义那一层

Ctrl + D 查找下一个相同的标识符,然后放一个新的脱字号或者称作输入光标)(多次点按可以在相同字符串上出很多光标,可以一起编辑,如下图) Ctrl + Insert 查找所有相同的标识符,然后全部放置脱字号(如下图)

多个脱字号

脱字号 是 Visual Studio 中对于输入光标的称呼,对应英文的 Caret。

书签

Ctrl + K, Ctrl + K 为当前行加入到书签或从书签中删除 Ctrl + K, Ctrl + P 切换到上一个书签 Ctrl + K, Ctrl + N 切换到下一个书签 Ctrl + K, Ctrl + L 删除所有书签(会有对话框提示的,不怕误按)

如果配合书签面板,那么可以在调查问题的时候很方便在找到的各种关键代码处跳转,避免每次都寻找。

配合书签面板

另外,还有个任务列表,跟书签列表差不多的功能:

Ctrl + K, Ctrl + H 将当前代码加入到任务列表中或者从列表中删除(效果类似编写 // TODO

任务列表

显示

Ctrl + R, Ctrl + W 显示空白字符
Alt + Z 切换自动换行和单行模式

显示空白字符

在使用 .NET Remoting 技术开发跨进程通信时可能遇到的各种异常

dotnet 职业技术学院 发布于 2019-09-05

在使用 .NET Remoting 开发跨进程应用的时候,你可能会遇到一些异常。因为这些异常在后验的时候非常简单但在一开始有各种异常烦扰的时候却并不清晰,所以我将这些异常整理到此文中,方便小伙伴们通过搜索引擎查阅。


连接到 IPC 端口失败: 系统找不到指定的文件

System.Runtime.Remoting.RemotingException:“连接到 IPC 端口失败: 系统找不到指定的文件。”

或者英文版:

System.Runtime.Remoting.RemotingException: Failed to connect to an IPC Port: The system cannot find the file specified.

出现此异常时,说明你获取到了一个远端对象,但是在使用此对象的时候,甚至还没有注册 IPC 端口。

比如,下面的代码是注册一个 IPC 端口的一种比较粗暴的写法,传入的 portName 是 IPC 的 Uri 路径前缀。例如我可以传入 walterlv,这样一个 IPC 对象的格式大约类似 ipc://walterlv/xxx

private static void RegisterChannel(string portName)
{
    var serverProvider = new BinaryServerFormatterSinkProvider
    {
        TypeFilterLevel = TypeFilterLevel.Full,
    };
    var clientProvider = new BinaryClientFormatterSinkProvider();
    var properties = new Hashtable
    {
        ["portName"] = portName
    };
    var channel = new IpcChannel(properties, clientProvider, serverProvider);
    ChannelServices.RegisterChannel(channel, false);
}

当试图访问 ipc://walterlv/foo 对象并调用其中的方法的时候,如果连 walterlv 端口都没有注册,就会出现 连接到 IPC 端口失败: 系统找不到指定的文件。 异常。

如果你已经注册了 walterlv 端口,但是没有 foo 对象,则会出现另一个错误 找不到请求的服务,请看下一节。

找不到请求的服务

System.Runtime.Remoting.RemotingException:“找不到请求的服务”

或者英文版:

System.Runtime.Remoting.RemotingException: Requested Service not found

当出现此异常时,可能的原因有三个:

  1. 要查找的远端对象尚未创建;
  2. 要查找的远端对象已被回收;
  3. 没有使用匹配的方法创建和访问对象。

更具体来说,对于第一种情况,就是当你试图跨进程访问某对象的时候,此对象还没有创建。你需要做的,是控制好对象创建的时机,创建对象的进程需要比访问它的进程更早完成对象的创建和封送。也就是下面的代码需要先调用。

RemotingServices.Marshal(@object, typeof(TObject).Name, typeof(TObject));

而对于第二种情况,你可能需要手动处理好封送对象的生命周期。重写 InitializeLifetimeService 方法并返回 null 是一个很偷懒却有效的方法。

namespace Walterlv.Remoting.Framework
{
    public abstract class RemoteObject : MarshalByRefObject
    {
        public sealed override object InitializeLifetimeService() => null;
    }
}

而对于第三种情况,你需要检查你是如何注册 .NET Remoting 通道的,创建和访问方式必须匹配。

信道“ipc”已注册

System.Runtime.Remoting.RemotingException:“信道“ipc”已注册。”

在同一个进程中,IpcChannel 类的默认信道名称 IpcChannel.ChannelName 值是字符串 "ipc"。如果你不通过它的参数 properties 来指定 ["name"] = "另一个名称",那么你就不能重复调用 ChannelServices.RegisterChannel 来调用这个信道。

说简单点,就是上面的方法 RegisterChannel 你不能在一个进程中调用两次,即便 "portName" 不同也不行。通常你也不需要去调用两次,如果一定要,请通过 HashTable 修改 name 属性。


参考资料

.NET/C# 阻止屏幕关闭,阻止系统进入睡眠状态

dotnet 职业技术学院 发布于 2019-09-05

在 Windows 系统中,一段时间不操作键盘和鼠标,屏幕便会关闭,系统会进入睡眠状态。但有些程序(比如游戏、视频和演示文稿)在运行过程中应该阻止屏幕关闭,否则屏幕总是关闭,会导致体验会非常糟糕。

本文介绍如何编写 .NET/C# 代码临时阻止屏幕关闭以及系统进入睡眠状态。


Windows API

我们需要使用到一个 Windows API:

/// <summary>
/// Enables an application to inform the system that it is in use, thereby preventing the system from entering sleep or turning off the display while the application is running.
/// </summary>
[DllImport("kernel32")]
private static extern ExecutionState SetThreadExecutionState(ExecutionState esFlags);

使用到的枚举用 C# 类型定义是:

[Flags]
private enum ExecutionState : uint
{
    /// <summary>
    /// Forces the system to be in the working state by resetting the system idle timer.
    /// </summary>
    SystemRequired = 0x01,

    /// <summary>
    /// Forces the display to be on by resetting the display idle timer.
    /// </summary>
    DisplayRequired = 0x02,

    /// <summary>
    /// This value is not supported. If <see cref="UserPresent"/> is combined with other esFlags values, the call will fail and none of the specified states will be set.
    /// </summary>
    [Obsolete("This value is not supported.")]
    UserPresent = 0x04,

    /// <summary>
    /// Enables away mode. This value must be specified with <see cref="Continuous"/>.
    /// <para />
    /// Away mode should be used only by media-recording and media-distribution applications that must perform critical background processing on desktop computers while the computer appears to be sleeping.
    /// </summary>
    AwaymodeRequired = 0x40,

    /// <summary>
    /// Informs the system that the state being set should remain in effect until the next call that uses <see cref="Continuous"/> and one of the other state flags is cleared.
    /// </summary>
    Continuous = 0x80000000,
}

以上所有的注释均照抄自微软的官方 API 文档:

API 封装

如果你擅长阅读英文,那么以上的 API 函数、枚举和注释足够你完成你的任务了。

不过,我这里提供一些封装,以应对一些常用的场景。

using System;
using System.Runtime.InteropServices;

namespace Walterlv.Windows
{
    /// <summary>
    /// 包含控制屏幕关闭以及系统休眠相关的方法。
    /// </summary>
    public static class SystemSleep
    {
        /// <summary>
        /// 设置此线程此时开始一直将处于运行状态,此时计算机不应该进入睡眠状态。
        /// 此线程退出后,设置将失效。
        /// 如果需要恢复,请调用 <see cref="RestoreForCurrentThread"/> 方法。
        /// </summary>
        /// <param name="keepDisplayOn">
        /// 表示是否应该同时保持屏幕不关闭。
        /// 对于游戏、视频和演示相关的任务需要保持屏幕不关闭;而对于后台服务、下载和监控等任务则不需要。
        /// </param>
        public static void PreventForCurrentThread(bool keepDisplayOn = true)
        {
            SetThreadExecutionState(keepDisplayOn
                ? ExecutionState.Continuous | ExecutionState.SystemRequired | ExecutionState.DisplayRequired
                : ExecutionState.Continuous | ExecutionState.SystemRequired);
        }

        /// <summary>
        /// 恢复此线程的运行状态,操作系统现在可以正常进入睡眠状态和关闭屏幕。
        /// </summary>
        public static void RestoreForCurrentThread()
        {
            SetThreadExecutionState(ExecutionState.Continuous);
        }

        /// <summary>
        /// 重置系统睡眠或者关闭屏幕的计时器,这样系统睡眠或者屏幕能够继续持续工作设定的超时时间。
        /// </summary>
        /// <param name="keepDisplayOn">
        /// 表示是否应该同时保持屏幕不关闭。
        /// 对于游戏、视频和演示相关的任务需要保持屏幕不关闭;而对于后台服务、下载和监控等任务则不需要。
        /// </param>
        public static void ResetIdle(bool keepDisplayOn = true)
        {
            SetThreadExecutionState(keepDisplayOn
                ? ExecutionState.SystemRequired | ExecutionState.DisplayRequired
                : ExecutionState.SystemRequired);
        }
    }
}

如果你对这段封装中的 keepDisplayOn 参数,也就是 ExecutionState.DisplayRequired 枚举不了解,看看下图直接就懂了。一个指的是屏幕关闭,一个指的是系统进入睡眠。

电源和睡眠

此封装后,使用则相当简单:

// 阻止系统睡眠,阻止屏幕关闭。
SystemSleep.PreventForCurrentThread();

// 恢复此线程曾经阻止的系统休眠和屏幕关闭。
SystemSleep.RestoreForCurrentThread();

或者:

// 重置系统计时器,临时性阻止系统睡眠和屏幕关闭。
// 此效果类似于手动使用鼠标或键盘控制了一下电脑。
SystemSleep.ResetIdle();

在使用 PreventForCurrentThread 这个 API 的时候,你需要避免程序对空闲时机的控制不好,导致屏幕始终不关闭。

如果你发现无论你设置了多么短的睡眠时间和屏幕关闭时间,屏幕都不会关闭,那就是有某个程序阻止了屏幕关闭,你可以:


参考资料

git fetch 失败,因为 unable to resolve reference 'refs/remotes/origin/xxx': reference broken

dotnet 职业技术学院 发布于 2019-09-05

我在使用 git fetch 命令的时候,发现竟然会失败,提示错误 error: cannot lock ref 'refs/remotes/origin/xxx': unable to resolve reference 'refs/remotes/origin/xxx': reference broken

本文介绍如何修复这样的错误,并探索此错误产生的原因。


错误

在使用 git fetch 命令之后,发现竟然出现了错误,错误输出如下:

$ git fetch --all --prune
Fetching origin
error: cannot lock ref 'refs/remotes/origin/next/release': unable to resolve reference 'refs/remotes/origin/next/release': reference broken
From git***.***.com:walterlv/demo-project
 ! [new branch]            next/release        -> origin/next/release  (unable to update local ref)
error: cannot lock ref 'refs/remotes/origin/feature/ai': unable to resolve reference 'refs/remotes/origin/feature/ai': reference broken
 ! [new branch]            feature/ai          -> origin/feature/ai  (unable to update local ref)
error: cannot lock ref 'refs/remotes/origin/release': unable to resolve reference 'refs/remotes/origin/release': reference broken
 ! [new branch]            release             -> origin/release  (unable to update local ref)
error: Could not fetch origin

修复

前往仓库路径,然后删除这些分支对应的文件。

  1. 前往仓库所在的本地文件夹;
  2. 进入子目录 .git\refs\remotes
  3. 一个个对着上面失败的分支,将其删除。

删除错误的分支

比如在我的错误例子中,要删除的文件分别是:

  • .git\refs\remotes\origin\next\release
  • .git\refs\remotes\origin\feature\ai
  • .git\refs\remotes\origin\release

随后,重新尝试 git fetch,git 会重新生成这些分支文件,因此不用担心会删出问题:

$ git fetch --all --prune
Fetching origin
From git***.***.com:walterlv/demo-project
   a1fd2551f7..cfb662e870  next/release  -> origin/next/release
 * [new branch]            feature/ai    -> origin/feature/ai
   97d72dfc8f..ceb346c8e2  release       -> origin/release

WPF 不要给 Window 类设置变换矩阵(分析篇):System.InvalidOperationException: 转换不可逆。

dotnet 职业技术学院 发布于 2019-09-02

最近总是收到一个异常 “System.InvalidOperationException: 转换不可逆。”,然而看其堆栈,一点点自己写的代码都没有。到底哪里除了问题呢?

虽然异常堆栈信息里面没有自己编写的代码,但是我们还是找到了问题的原因和解决方法。


异常堆栈

这就是抓到的此问题的异常堆栈:

System.InvalidOperationException: 转换不可逆。
    System.Windows.Media.Matrix.Invert()
    MS.Internal.PointUtil.TryApplyVisualTransform(Point point, Visual v, Boolean inverse, Boolean throwOnError, Boolean& success)
    MS.Internal.PointUtil.TryClientToRoot(Point point, PresentationSource presentationSource, Boolean throwOnError, Boolean& success)
    System.Windows.Input.MouseDevice.LocalHitTest(Boolean clientUnits, Point pt, PresentationSource inputSource, IInputElement& enabledHit, IInputElement& originalHit)
    System.Windows.Input.MouseDevice.GlobalHitTest(Boolean clientUnits, Point pt, PresentationSource inputSource, IInputElement& enabledHit, IInputElement& originalHit)
    System.Windows.Input.StylusWisp.WispStylusDevice.FindTarget(PresentationSource inputSource, Point position)
    System.Windows.Input.StylusWisp.WispLogic.PreNotifyInput(Object sender, NotifyInputEventArgs e)
    System.Windows.Input.InputManager.ProcessStagingArea()
    System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
    System.Windows.Input.StylusWisp.WispLogic.InputManagerProcessInput(Object oInput)
    System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
    System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)

可以看到,我们的堆栈结束点是 ExceptionWrapper.TryCatchWhen 可以得知此异常是通过 Dispatcher.UnhandledException 来捕获的。也就是说,此异常直接通过 Windows 消息被我们间接触发,而不是直接通过我们编写的代码触发。而最顶端是对矩阵求逆,而此异常是试图对一个不可逆的矩阵求逆。

分析过程

如果你不想看分析过程,可以直接移步至本文的最后一节看原因和解决方案。

源代码

因为 .NET Framework 版本的 WPF 是开源的,.NET Core 版本的 WPF 目前还处于按揭开源的状态,所以我们看 .NET Framework 版本的代码来分析原因。

我按照调用堆栈从顶到底的顺序,将前面三帧的代码贴到下面。

PointUtil.TryApplyVisualTransform

public static Point TryApplyVisualTransform(Point point, Visual v, bool inverse, bool throwOnError, out bool success)
{
    success = true;

    if(v != null)
    {
        Matrix m = GetVisualTransform(v);

        if (inverse)
        {
            if(throwOnError || m.HasInverse)
            {
                m.Invert();
            }
            else
            {
                success = false;
                return new Point(0,0);
            }
        }

        point = m.Transform(point);
    }

    return point;
}

PointUtil.TryClientToRoot

[SecurityCritical,SecurityTreatAsSafe]
public static Point TryClientToRoot(Point point, PresentationSource presentationSource, bool throwOnError, out bool success)
{
    if (throwOnError || (presentationSource != null && presentationSource.CompositionTarget != null && !presentationSource.CompositionTarget.IsDisposed))
    {
        point = presentationSource.CompositionTarget.TransformFromDevice.Transform(point);
        point = TryApplyVisualTransform(point, presentationSource.RootVisual, true, throwOnError, out success);
    }
    else
    {
        success = false;
        return new Point(0,0);
    }

    return point;
}

你可能会说,在调用堆栈上面看不到 PointUtil.ClientToRoot 方法。但其实如果我们看一看 MouseDevice.LocalHitTest 的代码,会发现其实调用的是 PointUtil.ClientToRoot 方法。在调用堆栈上面看不到它是因为方法足够简单,被内联了。

[SecurityCritical,SecurityTreatAsSafe]
public static Point ClientToRoot(Point point, PresentationSource presentationSource)
{
    bool success = true;
    return TryClientToRoot(point, presentationSource, true, out success);
}

求逆的矩阵

下面我们一步一步分析异常的原因。

我们先看看是什么代码在做矩阵求逆。下面截图中的方法是反编译的,就是上面我们在源代码中列出的 TryApplyVisualTransform 方法。

矩阵求逆的调用

先获取了传入 Visual 对象的变换矩阵,然后根据参数 inverse 来对其求逆。如果矩阵可以求逆,即 HasInverse 属性返回 true,那么代码可以继续执行下去而不会出现异常。但如果 HasInverse 返回 false,则根据 throwOnError 来决定是否抛出异常,在需要抛出异常的情况下会真实求逆,也就是上面截图中我们看到的异常发生处的代码。

那么接下来我们需要验证三点:

  1. 这个 Visual 是哪里来的;
  2. 这个 Visual 的变换矩阵什么情况下不可求逆;
  3. throwOnError 确定传入的是 true 吗。

于是我们继续往上层调用代码中查看。

应用变换的调用 1

应用变换的调用 2

可以很快验证上面需要验证的两个点:

  1. throwOnError 传入的是 true
  2. VisualPresentationSourceRootVisual

PresentationSourceRootVisual 是什么呢?PresentationSource 是承载 WPF 可视化树的一个对象,对于窗口 Window,是通过 HwndSourcePresentationSource 的子类)承载的;对于跨线程 WPF UI,可以通过自定义的 PresentationSource 子类来完成。这部分可以参考我之前的一些博客:

不管怎么说,这个指的就是 WPF 可视化树的根:

  • 如果你使用 Window 来显示 WPF 窗口,那么根就是 Window 类;
  • 如果你是用 Popup 来承载一个弹出框,那么根就是 PopupRoot 类;
  • 如果你使用了一些跨线程/跨进程 UI 的技术,那么根就是自己写的可视化树根元素。

对于绝大多数 WPF 开发者来说,只会碰到前面第一种情况,也就是仅仅有 Window 作为可视化树的根的情况。一般人很难直接给 PopupRoot 设置变换矩阵,一般 WPF 程序的代码也很少做跨线程或跨进程 UI。

于是我们几乎可以肯定,是有某处的代码让 Window 的变换矩阵不可逆了。

矩阵求逆

什么样的矩阵是不可逆的?

异常代码

发生异常的代码是 WPF 中 Matrix.Invert 方法,其发生异常的代码如下:

Matrix.Invert

首先判断矩阵的行列式 Determinant 是否为 0,如果为 0 则抛出矩阵不可逆的异常。

Matrix.Determinant

行列式

WPF 的 2D 变换矩阵 \(M\) 是一个 \(3\times{3}\) 的矩阵:

\[\begin{bmatrix} M11 & M12 & 0 \\ M21 & M22 & 0 \\ OffsetX & OffsetY & 1 \end{bmatrix}\]

其行列式 \(det(M)\) 是一个标量:

\[\left | A \right | = \begin{vmatrix} M11 & M12 & 0 \\ M21 & M22 & 0 \\ OffsetX & OffsetY & 1 \end{vmatrix} = M11 \times M22 - M12 \times M21\]

因为矩阵求逆的时候,行列式的值会作为分母,于是会无法计算,所以行列式的值为 0 时,矩阵不可逆。

前面我们计算 WPF 的 2D 变换矩阵的行列式的值为 \(M11 \times M22 - M12 \times M21\),因此,只要使这个式子的值为 0 即可。

那么 WPF 的 2D 变换的时候,如何使此值为 0 呢?

  • 平移?平移只会修改 \(OffsetX\) 和 \(OffsetY\),因此对结果没有影响
  • 缩放?缩放会将原矩阵点乘缩放矩阵
  • 旋转?旋转会将旋转矩阵点乘原矩阵

其中,原矩阵在我们的场景下就是恒等的矩阵,即 Matrix.Identity

\[\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\]

接下来缩放和旋转我们都不考虑变换中心的问题,因为变换中心的问题都可以等价为先进行缩放和旋转后,再单纯进行平移。由于平移对行列式的值没有影响,于是我们忽略。

缩放矩阵

缩放矩阵。如果水平和垂直分量分别缩放 \(ScaleX\) 和 \(ScaleY\) 倍,则缩放矩阵为:

\[\begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix}\]

原矩阵点乘缩放矩阵结果为:

\[\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix}\]

于是,只要 \(ScaleX\) 和 \(ScaleY\) 任何一个为 0 就可以导致新矩阵的行列式必定为 0。

旋转矩阵

旋转矩阵。假设用户设置的旋转角度为 angle,那么换算成弧度为 angle * (Math.PI/180.0),我们将弧度记为 \(\alpha\),那么旋转矩阵为:

\[\begin{bmatrix} \cos{\alpha} & \sin{\alpha} & 0 \\ -\sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 1 \end{bmatrix}\]

旋转矩阵点乘原矩阵的结果为:

\[\begin{bmatrix} \cos{\alpha} & \sin{\alpha} & 0 \\ -\sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} \cos{\alpha} & \sin{\alpha} & 0 \\ -\sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 1 \end{bmatrix}\]

对此矩阵的行列式求值:

\[\cos^{2}{\alpha} + \sin^{2}{\alpha} = 1\]

也就是说其行列式的值恒等于 1,因此其矩阵必然可求逆。

WPF 2D 变换矩阵求逆小结

对于 WPF 的 2D 变换矩阵:

  1. 平移和旋转不可能导致矩阵不可逆;
  2. 缩放,只要水平和垂直方向的任何一个分量缩放量为 0,矩阵就会不可逆。

寻找问题代码

现在,我们寻找问题的方向已经非常明确了:

  • 找到设置了 ScaleTransformWindow,检查其是否给 ScaleX 或者 ScaleY 属性赋值为了 0

然而,真正写一个 demo 程序来验证这个问题的时候,就发现没有这么简单。因为:

不能给 Window 设置变换矩阵

我们发现,不止是 ScaleXScaleY 属性不能设为 0,实际上设成 0.5 或者其他值也是不行的。

唯一合理值是 1

那么为什么依然有异常呢?难道是 ScaleTransform 的值一开始正常,然后被修改?

编写 demo 验证,果然如此。而只有变换到 0 才会真的引发本文一开始我们提到的异常。一般会开始设为 1 而后设为 0 的代码通常是在做动画。

一定是有代码正在为窗口的 ScaleTransform 做动画。

结果全代码仓库搜索 ScaleTransform 真的找到了问题代码。

<Window x:Name="WalterlvDoubiWindow"
        x:Class="Walterlv.Exceptions.Unknown.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.RenderTransform>
        <ScaleTransform ScaleX="1" ScaleY="1" />
    </Window.RenderTransform>
    <Window.Resources>
        <Storyboard x:Key="Storyboard.Load">
            <DoubleAnimation Storyboard.TargetName="WalterlvDoubiWindow"
                             Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
                             From="0" To="1" />
            <DoubleAnimation Storyboard.TargetName="WalterlvDoubiWindow"
                             Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
                             From="0" To="1" />
        </Storyboard>
    </Window.Resources>
    <Grid>
        <!-- 省略的代码 -->
    </Grid>
</Window>

不过,这段代码并不会导致每次都出现异常,而是在非常多次尝试中偶尔能出现一次异常。

原因和解决方案

原因

  1. Window 类是不可以设置 RenderTransform 属性的,但允许设置恒等(Matrix.Identity)的变换;
  2. 如果让 Window 类缩放分量设置为 0,就会出现矩阵不可逆异常。

解决方案

不要给 Window 类设置变换,如果要做,请给 Window 内部的子元素设置。比如上面的例子中,我们给 Grid 设置就没有问题(而且可以做到类似的效果。

WPF 不要给 Window 类设置变换矩阵(应用篇)

dotnet 职业技术学院 发布于 2019-09-02

WPF 的 Window 类是不允许设置变换矩阵的。不过,总会有小伙伴为了能够设置一下试图绕过一些验证机制。

不要试图绕过,因为你会遇到更多问题。


试图设置变换矩阵

当你试图给 Window 类设置变换矩阵的时候,会出现异常:

System.InvalidOperationException:“转换对于 Window 无效。”

无论是缩放还是旋转,都一样会出现异常。

转换对于 Window 无效 - 缩放

转换对于 Window 无效 - 旋转

我们在 WPF 不要给 Window 类设置变换矩阵(分析篇) 一文中已经证明在 WPF 的 2D 变换中,旋转一定不会造成矩阵不可逆,因此此验证是针对此属性的强验证。

只有做设置的变换是恒等变换的时候,才可以完成设置。

this.RenderTransform = new TranslateTransform(0, 0);
this.RenderTransform = new ScaleTransform(1, 1);
this.RenderTransform = new RotateTransform(0);
this.RenderTransform = new MatrixTransform(Matrix.Identity);

绕过验证

然而你可以通过先设置变换,再修改变换值的方式绕过验证:

var scaleTransform = new ScaleTransform(1, 1);
this.RenderTransform = scaleTransform;
scaleTransform.ScaleX = 0.5;
scaleTransform.ScaleY = 0.5;

实际上,你绕过也没有关系,可是这样的设置实际上是没有任何效果的。

不过为什么还是会有小伙伴这么设置呢?

是因为小伙伴同时还设置了窗口透明 AllowsTransparency="True"WindowStyle="None"Background="Transparent",导致看起来好像这个变换生效了一样。

小心异常

此设置不仅没有效果,还会引发异常,请阅读我的另一篇博客了解:

通过设置启用 Visual Studio 默认关闭的大量强大的功能提升开发效率

dotnet 职业技术学院 发布于 2019-08-29

使用 Visual Studio 开发 C#/.NET 应用程序,以前有 ReSharper 来不足其各项功能短板,后来不断将 ReSharper 的功能一点点搬过来稍微好了一些。不过直到 Visual Studio 2019,才开始渐渐可以和 ReSharper 拼一下了。

如果你使用 Visual Studio 2019,那么像本文这样配置一下,可以大大提升你的开发效率。


工具选项

打开菜单 “工具” -> “选项”,然后你就打开了 Visual Studio 的选项窗口。接下来本文的所有内容都会在这里进行。

打开选项窗口

文本编辑器

在 “文本编辑器” -> “常规” 分类中,我们关心这些设置:

  • 使鼠标单击可执行转到定义 这样按住 Ctrl 键点击标识符的时候可以转到定义(开启此选项之后,后面有其他选项可以转到反编译后的源码)

文本编辑器 -> 常规

当然也有其他可以打开玩的:

  • 查看空白 专治强迫症,可以把空白字符都显示出来,这样你可以轻易看到对齐问题以及多于的空格了

C#

在 “文本编辑器” -> “C#” -> “IntelliSense” 分类中,我们关心这些设置:

  • 键入字符后显示完成列表 删除字符后显示完成列表 突出显示完成列表项的匹配部分 显示完成项筛选器 打开这些选项可以让智能感知列表更容易显示出来,而我们也知道智能感知列表的强大
  • 推荐 显示 unimported 命名空间中的项(实验) 这一项默认不会勾选,但强烈建议勾选上;它可以帮助我们直接输入没有 using 的命名空间中的类型,这可以避免记住大量记不住的类名

IntelliSense

C# 高级

在 “文本编辑器” -> “C#” -> “高级” 分类中,我们关心大量设置:

  • 支持导航到反编译源(实验) 前面我们说可以 Ctrl + 鼠标导航到定义,如果打开了这个就可以看反编译后的源码了
  • 启用可为 null 的引用分析 IDE 功能 这个功能可能还没有完成,暂时还是无法开启的

高级

当然也有其他可以打开玩的:

  • 启用完成解决方案分析 这是基于 Roslyn 的分析,Visual Studio 的大量重构功能都依赖于它;默认关闭也可以用,只是仅分析当前正在编辑的文件;如果打开则分析整个解决方案,你会在错误列表中看到大量的编译警告

代码样式

在 “文本编辑器” -> “C#” -> “代码样式” 分类,如果你关心代码的书写风格,那么这个分类底下的每一个子类别都可以考虑一个个检查一下。

代码样式

人工智能 IntelliCode

Visual Studio 2019 默认安装了 IntelliCode 可以充分利用微软使用 GitHub 上开源项目训练出来的模型来帮助编写代码。这些强烈建议开启。

  • C# 基础模型 微软利用 GitHub 开源项目训练的基础模型
  • XAML 基础模型 微软利用 GitHub 开源项目训练的基础模型
  • C# 参数完成
  • C# 自定义模型 如果针对单个项目训练出来了模型,那么可以使用专门针对此项目训练的模型
  • EditorConfig 推理 可以根据项目推断生成 EditorConfig 文件 可以参见在 Visual Studio 中使用 EditorConfig 统一代码风格
  • 自定义模型训练提示 如果开启,那么每个项目的规模如果达到一定程度就会提示训练一个自定义模型出来

IntelliCode

IntelliCode English

训练模型会上传一部分数据到 IntelliCode 服务器,你可以去 %TEMP%\Visual Studio IntelliCode 目录来查看到底上传了哪些数据。

快捷键

当然,设置好快捷键也是高效编码的重要一步,可以参考:

自动完成

在你点击 “确定” 关闭了以上窗口之后,我们还需要设置一项。

确保下图中的这个按钮处于 “非选中” 状态:

建议完成模式

这样,当出现智能感知列表的时候,我们直接就可以按下回车键输入这个选项了;否则你还需要按上下选中再回车。

建议完成和标准完成

WPF 的 Application.Current.Dispatcher 中,为什么 Current 可能为 null

dotnet 职业技术学院 发布于 2019-08-27

在 WPF 程序中,可能会存在 Application.Current.Dispatcher.Xxx 这样的代码让一部分逻辑回到主 UI 线程。因为发现在调用这句代码的时候出现了 NullReferenceException,于是就有三位小伙伴告诉我说 CurrentDispatcher 属性都可能为 null

然而实际上这里只可能 Currentnull 而此上下文的 Dispatcher 是绝对不会为 null 的。(当然我们这里讨论的是常规编程手段,如果非常规手段,你甚至可以让实例的 thisnull 呢……)


当你的应用程序退出时,所有 UI 线程的代码都不再会执行,因此这是安全的;但所有非 UI 线程的代码依然在继续执行,此时随时可能遇到 Application.Current 属性为 null。

由于本文所述的两个部分都略长,所以拆分成两篇博客,这样更容易理解。

Application.Current 静态属性

源代码

Application 类型的源代码会非常长,所以这里就不贴了,可以前往这里查看:

其中,Current 返回的是 _appInstance 的静态字段。因此 _appInstance 字段为 null 的时机就是 Application.Currentnull 的时机。

/// <summary>
///     The Current property enables the developer to always get to the application in
///     AppDomain in which they are running.
/// </summary>
static public Application Current
{
    get
    {
        // There is no need to take the _globalLock because reading a
        // reference is an atomic operation. Moreover taking a lock
        // also causes risk of re-entrancy because it pumps messages.

        return _appInstance;
    }
}

由于 _appInstance 字段是私有字段,所以仅需调查这个类本身即可找到所有的赋值时机。(反射等非常规手段需要排除在外,因为这意味着开发者是逗比——自己闯的祸不能怪 WPF 框架。)

赋值时机

_appInstance 的赋值时机有两处:

  1. Application 的实例构造函数(注意哦,是实例构造函数而不是静态构造函数);
  2. Application.DoShutdown 方法。

Application 的实例构造函数中:

  • _appInstance 的赋值是线程安全的,这意味着多个 Application 实例的构造不会因为线程安全问题导致 _appInstance 字段的状态不正确。
  • 如果 _appCreatedInThisAppDomaintrue 那么,将抛出异常,组织此应用程序域中创建第二个 Application 类型的实例。
/// <summary>
///     Application constructor
/// </summary>
/// <SecurityNote>
///    Critical: This code posts a work item to start dispatcher if in the browser
///    PublicOk: It is ok because the call itself is not exposed and the application object does this internally.
/// </SecurityNote>
[SecurityCritical]
public Application()
{
    // 省略了一部分代码。
    lock(_globalLock)
    {
        // set the default statics
        // DO NOT move this from the begining of this constructor
        if (_appCreatedInThisAppDomain == false)
        {
            Debug.Assert(_appInstance == null, "_appInstance must be null here.");
            _appInstance = this;
            IsShuttingDown    = false;
            _appCreatedInThisAppDomain = true;
        }
        else
        {
            //lock will be released, so no worries about throwing an exception inside the lock
            throw new InvalidOperationException(SR.Get(SRID.MultiSingleton));
        }
    }
    // 省略了一部分代码。
}

也就是说,此类型实际上是设计为单例的。在第一个实例构造出来之后,单例的实例即可开始使用。

后续赋值

此单例实例的唯一结束时机就是 Application.DoShutdown 方法。这是唯一将 _appInstance 赋值为 null 的代码。

/// <summary>
/// DO NOT USE - internal method
/// </summary>
///<SecurityNote>
///     Critical: Calls critical code: Window.InternalClose
///     Critical: Calls critical code: HwndSource.Dispose
///     Critical: Calls critical code: PreloadedPackages.Clear()
///</SecurityNote>
[SecurityCritical]
internal virtual void DoShutdown()
{
    // 省略了一部分代码。

    // Event handler exception continuality: if exception occurs in ShuttingDown event handler,
    // our cleanup action is to finish Shuttingdown.  Since Shuttingdown cannot be cancelled.
    // We don't want user to use throw exception and catch it to cancel Shuttingdown.
    try
    {
        // fire Applicaiton Exit event
        OnExit(e);
    }
    finally
    {
        SetExitCode(e._exitCode);

        // By default statics are shared across appdomains, so need to clear
        lock (_globalLock)
        {
            _appInstance = null;
        }

        _mainWindow = null;
        _htProps = null;
        NonAppWindowsInternal = null;

        // 省略了一部分代码。
    }
}

可以调用到此代码的公共 API 有:

  • Application.Shutdown 实例方法
  • 导致 Window 关闭的若干方法(InternalDispose
  • IBrowserHostServices.PostShutdown 接口方法

因此,所有直接或间接调用到以上方法的地方都会导致 Application.Current 属性被赋值为 null

对所写代码的影响

从以上的分析可以得知,只要你还能在 Application.DoShutdown 执行之后继续执行代码,那么这部分的代码都将面临着 Application.Currentnull 风险。

那么,到底有哪些时机可能遇到 Application.Currentnull 呢?这部分就与读者项目中所用的业务代码强相关了。

但是这部分业务代码会有一些公共特征帮助你判定你是否可能写出遭遇 Application.Currentnull 的代码。

此特征是:此代码与 Application.Current 不在同一线程

Application.Current 不在同一线程

对于 WPF 程序,你的多数代码可能是由用户交互产生,即便有后续代码的执行,也依然是从 UI 交互产生。这样的代码不会遇到 Application.Currentnull 的情况。

但是,如果你的代码由非 UI 线程触发,例如在 Usb 设备改变、与其他端的通信、某些异步代码的回调等等,这些代码不受 Dispatcher 是否调度影响,几乎一定会执行。因此 Application.Current 就算赋值为 null 了,它们也不知道,依然会继续执行,于是就会遭遇 Application.Currentnull

这本质上是一个线程安全问题。

使用 Invoke/BeginInvoke/InvokeAsync 的代码不会出问题

Application.DoShutdown 方法被 ShutdownImpl 包装,且所有调用均从此包装进入,因此,所有可能导致 Application.Currentnull 的代码,均会调用此方法,也就是说,会调用 Dispatcher.CriticalInvokeShutdown 实例方法。

/// <summary>
/// This method gets called on dispatch of the Shutdown DispatcherOperationCallback
/// </summary>
///<SecurityNote>
///  Critical: Calls critical code: DoShutdown, Dispatcher.CritcalInvokeShutdown()
///</SecurityNote>
[SecurityCritical]
private void ShutdownImpl()
{
    // Event handler exception continuality: if exception occurs in Exit event handler,
    // our cleanup action is to finish Shutdown since Exit cannot be cancelled. We don't
    // want user to use throw exception and catch it to cancel Shutdown.
    try
    {
        DoShutdown();
    }
    finally
    {
        // Quit the dispatcher if we ran our own.
        if (_ownDispatcherStarted == true)
        {
            Dispatcher.CriticalInvokeShutdown();
        }

        ServiceProvider = null;
    }
}

所有的关闭 Dispatcher 的调用有两类,Application 关闭时调用的是内部方法 CriticalInvokeShutdown

  1. 立即关闭 CriticalInvokeShutdown,即以 Send 优先级 Invoke 关闭方法,而 Send 优先级调用 Invoke 几乎等同于直接调用(为什么是等同而不是直接调用?因为还需要考虑回到 Dispatcher 初始化时所在的线程)。
  2. 开始关闭 BeginInvokeShutdown,即以指定的优先级 InvokeAsync 关闭方法。

而关闭 Dispatcher 意味着所有使用 Invoke/BeginInvoke/InvokeAsync 的任务将终止。

//<SecurityNote>
//  Critical - as it accesses security critical data ( window handle)
//</SecurityNote>
[SecurityCritical]
private void ShutdownImplInSecurityContext(Object state)
{
    // 省略了一部分代码。

    // Now that the queue is off-line, abort all pending operations,
    // including inactive ones.
    DispatcherOperation operation = null;
    do
    {
        lock(_instanceLock)
        {
            if(_queue.MaxPriority != DispatcherPriority.Invalid)
            {
                operation = _queue.Peek();
            }
            else
            {
                operation = null;
            }
        }

        if(operation != null)
        {
            operation.Abort();
        }
    } while(operation != null);

    // 省略了一部分代码。
}

由于此终止代码在 Dispatcher 所在的线程执行,而所有 Invoke/BeginInvoke/InvokeAsync 代码也都在此线程执行,因此这些代码均不会并发。已经执行的代码会在此终止代码之前,而在此终止代码之后也不会再执行任何 Invoke/BeginInvoke/InvokeAsync 的任务了。

  • 所有通过 Invoke/BeginInvoke/InvokeAsync 或间接通过此方法(如 WPF 控件相关事件)调用的代码,均不会遭遇 Application.Currentnull
  • 所有在 UI 线程使用 async / await 并使用默认上下文执行的代码,均不会遭遇 Application.Currentnull。(这意味着你没有使用 .ConfigureAwait(false),详见在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁 - walterlv。)

最简示例代码

最简例子

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace Walterlv.Demo.ApplicationDispatcher
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            var app = new Application();
            Task.Delay(1000).ContinueWith(t =>
            {
                app.Dispatcher.InvokeAsync(() => app.Shutdown());
            });
            Task.Delay(2000).ContinueWith(t =>
            {
                Application.Current.Dispatcher.InvokeAsync(() => { });
            });
            app.Run();
            Thread.Sleep(2000);
        }
    }
}

结论

总结以上所有的分析:

  1. 任何与 Application 不在同一个线程的代码,都可能遭遇 Application.Currentnull
  2. 任何与 Application 在同一个线程的代码,都不可能遇到 Application.Currentnull

这其实是一个线程安全问题。用所有业务开发者都可以理解的说法描述就是:

当你的应用程序退出时,所有 UI 线程的代码都不再会执行,因此这是安全的;但所有非 UI 线程的代码依然在继续执行,此时随时可能遇到 Application.Current 属性为 null。

因此,记得所有非 UI 线程的代码,如果需要转移到 UI 线程执行,记得判空:

private void OnUsbDeviceChanged(object sender, EventArgs e)
{
    // 记得这里需要判空,因为此上下文可能在非 UI 线程。
    Application.Current?.InvokeAsync(() => { });
}

Application.Dispatcher 实例属性

关于 Application.Dispatcher 是否可能为 null 的分析,由于比较长,请参见我的另一篇博客:


参考资料