dotnet 职业技术学院 发布于 2019-09-18
弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。网上有很多弱事件的 NuGet 包,不过仅仅支持定义事件的时候写成弱事件而不支持让任意事件变成弱事件,并且存在性能问题。
本文将设计一套弱事件机制,不止可以让任意一个 CLR 事件成为弱事件,还具有近乎原生事件的性能。
系列博客:
本文主要为了设计一套弱事件机制而编写,因此如果你感兴趣,应该已经理解了我试图做什么事情。
当然,如果并不理解,可以阅读这个机制的应用篇,里面有具体的应用场景:
在我进行此设计之前,已有如下种类的弱事件机制:
WeakEventManager
WeakEventManager<TEventSource, TEventArgs>
Weak Event
的高赞回答
由于我希望编写的弱事件机制尽可能减少对非预期框架的依赖,而且具有很高的性能,所以我打算自己实现一套。
这三个原则,从上到下优先级依次降低。
要支持所有类型的 CLR 事件,意味着我的设计中必须要能够直接监听到任意事件,而不能所有代码都从我自己编写的代码开始。
要有很高的性能,就意味着我几乎不能使用“反射”,也不能使用委托的 DynamicInvoke
方法,还不能生成 IL 代码(首次生成很慢),也不能使用表达式树(首次编译很慢)。那么可以使用的也就只剩下两个了,一个是纯 C#/.NET 带的编译期就能确定执行的代码,另一个是使用 Roslyn 编译期在编译期间进行特殊处理。
类的使用者要编写极少量的代码,意味着能够抽取到框架中的代码就尽量抽取到框架中。
俗话说,一个好的名字是成功的一半。
因为我希望为任意 CLR 事件添加弱事件支持,所以其职责有点像“代理、中间人、中继、中转”,对应英文的 Proxy
Agent
Relay
Transfer
。最终我选择名称 Relay
(中继),因为好听。
对于 API 的设计,我有一个小原则:
我总结了好的 API 设计的一些原则:
不得不说,此类型设计的技术难度还是挺大的。虽然我们知道有 WeakReference<T>
可用,但依然存在很多的技术难点。于是 API 的设计可能要退而求其次优先满足前两个优先级更高的目标。
我们期望 API 足够简单,因此在几个备选方案中选择:
WeakEventRelay.Subscribe("Changed", OnChanged)
WeakEventRelay.Subscribe(o => o.Changed, OnChanged)
Action
来做,会遇到 o.Changed
必须出现在 +=
左边的编译错误o.Changed
必须出现在 +=
左边的编译错误,同时还会出现少量性能问题因此,直接一个方法就能完成事件注册是不可能的了,我们改用其他方法——继承自某个基类:
internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
public event FileSystemEventHandler Changed
{
add => /*实现弱事件订阅*/;
remove => /*实现弱事件注销*/;
}
}
那么实现的难点就都在 add
和 remove
方法里面了。
我们究竟需要哪些信息才可以完成弱事件机制呢?
object sender
的形式出现在你的代码中)FileSystemWatcher.Changed
事件)add
和 remove
方法中的 value
)然而事情并没有那么简单:
一
在框架通用代码中,我不可能获取到要订阅的事件。因为事件要求只能出现在 +=
的左边,不能以任何其他形式使用(包括但不限于通过参数传递,伪装成 Lambda 表达式,伪装成表达式树)。这意味着 o.Changed += OnChanged
这样的事件订阅完全写不出来通用代码(除非牺牲性能)。
那么还能怎么做呢?只能将这段写不出来的代码留给业务编写者来编写了。
也就是说,类似于 o.Changed += OnChanged
这样的代码只能交给业务开发者来实现。与此同时也注定了 OnChanged
必须由业务开发者编写(因为无法写出通用的高性能的事件处理函数,并且还能在 +=
和 -=
的时候保持同一个实例。
二
我没有办法通过抽象的办法引发一个事件。具体来说,无法在抽象的通用代码中写出 Changed.Invoke(sender, e)
这样代码。因为委托的基类 Delegate
MultiCastDelegate
没有 Invoke
方法可以使用,只有耗性能的 DynamicInvoke
方法。各种不同的委托定义虽然可以有相同的参数和返回值类型,但是却不能相互转换,因此我也不能将传入的委托转换成 Action<TSender, TArgs>
这样的通用委托。
庆幸的是,C# 提供了将方法组隐式转换委托的方法,可以让两个参数和返回值类型相同的委托隐式转换。但注意,这是隐式转换,没有运行时代码可以高性能地完成这件事情。
在 add
和 remove
方法中,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) => /* 引发弱事件 */;
}
这已经开始让业务方的代码变得复杂起来了。
我们还需要能够注册、注销和引发弱事件,而这部分就没那么坑了。因为:
o.Changed += OnChanged
,value
,value.Invoke
都传进来了;我写了一个 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 上开源:
注意开源协议:
参考资料
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
而一直存在。
一个可行的解决办法是调用 FileSystemWatcher
的 Dispose
方法。不过有些时候很难决定到底在什么时机调用 Dispose
合适。
现在,我们希望有一种方法,能够在 WalterlvDemo
的实例失去作用域后被回收,最好 FileSystemWatcher
也能够自动被 Dispose
释放掉。
如果你试图解决的是类似这样的问题,那么本文就可以帮到你。
总结一下:
FileSystemWatcher
);Dispose
);demo
变量脱离作用域。)。目前有 WPF 自带的 WeakEventManager
机制,网上也有很多可用的 NuGet 包,但是都有限制:
而 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 编译器生成,我可能接下来就会做这件事情避免你写出这些代码)。
FileSystemWatcherWeakEventRelay
,继承自库 Walterlv.WeakEvents 中的 WeakEventRelay<FileSystemWatcher>
类型。带上的泛型参数表明是针对 FileSystemWatcher
类型做弱事件中继。public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }
。这个构造函数是可以用 Visual Studio 生成的,快捷键是 Ctrl + .
或者 Alt + Enter
(快捷键功效详见:提高使用 Visual Studio 开发效率的键盘快捷键)WeakEvent<FileSystemEventArgs>
,名为 _changed
,这个就是弱事件的核心。泛型参数是事件参数的类型(注意,为了极致的性能,这里的泛型参数是事件参数的名称,而不是大多数弱事件框架中提供的事件处理委托类型)。public event FileSystemEventHandler Changed
。
add
方法固定调用 Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
。其中 Changed
是 FileSystemWatcher
中的事件,OnChanged
是我们即将定义的事件处理函数,_changed
是前面定义好的弱事件字段,而后面的 value
和 value.Invoke
是固定写法。remove
方法固定调用弱事件的 Remove
方法,即 _changed.Remove(value);
。OnChanged
,并在里面固定调用 TryInvoke(_changed, sender, e)
。OnReferenceLost
方法,用于在对象已被回收后反注册 FileSystemWatcher
中的事件。希望看了上面这 6 点之后你还能理解这些代码都是在做啥。如果依然不能理解,可以考虑:
FileSystemWatcherWeakEventRelay
的完整代码来理解哪些是可变部分哪些是不可变部分,自己替换就好;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
来监视这个文件的改变。
可以看到,在回收内存之后,将不会再监视文件的改变。当然,如果你期望一直可以监视改变,当然也不希望用到本文的弱事件。
一句话解答:为了高性能!
请参见我的另一篇博客:
参考资料
dotnet 职业技术学院 发布于 2019-09-17
在 .NET 中创建进程时,可以传入 ProcessStartInfo
类的一个新实例。在此类型中,有一个 UseShellExecute
属性。
本文介绍 UseShellExecute
属性的作用,设为 true
和 false
时,分别有哪些进程启动行为上的差异。
Process.Start
本质上是启动一个新的子进程,不过这个属性的不同,使得启动进程的时候会调用不同的 Windows 的函数。
UseShellExecute = true
UseShellExecute = false
当然,如果你知道这两个函数的区别,那你自然也就了解此属性设置为 true
和 false
的区别了。
ShellExecute
的用途是打开程序或者文件或者其他任何能够打开的东西(如网址)。
也就是说,你可以在 Process.Start
的时候传入这些:
PATH
环境变量中的各种程序不过,此方法有一些值得注意的地方:
而 CreateProcess
则会精确查找路径来执行,不支持各种非可执行程序的打开。但是:
UseShellExecute
在 .NET Framework 中的的默认值是 true
,在 .NET Core 中的默认值是 false
。
如果有以下需求,那么建议设置此值为 false
:
如果你有以下需求,那么建议设置此值为 true
或者保持默认:
参考资料
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:
你可以前往官方文档:
在其中,你可以找到这样一段话:
用户筛选的子句的表达式不受任何限制。 如果在执行用户筛选的表达式期间发生异常,则将放弃该异常,并视筛选表达式的值为 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
出来的 FileNotFoundException
的 FileName
属性会保持为 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 中甚至连 End
都没有输出!几乎可以确定,程序在 .NET Framework 4.8 中出现了致命的崩溃!
如果我们以 Visual Studio 调试启动此程序,可以看到抛出了 CLR 异常:
以下是在 Visual Studio 中单步跟踪的步骤:
由于本人金鱼般的记忆力,我竟然给微软报了三次这个 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]
请参见:
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
还可能是其他一些可能延迟计算值的提供者。因此,你不能在这里获取到常规方法获取到的依赖项属性的真实类型的值。
但是,此枚举拿到的所有依赖项属性的值都是此依赖对象已经赋值过的依赖项属性的本地值。如果没有赋值过,将不会在这里的遍历中出现。
参考资料
dotnet 职业技术学院 发布于 2019-09-12
本文介绍如何在 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);
参考资料
dotnet 职业技术学院 发布于 2019-09-07
突然间要编写或者调试几个 C++ 的小程序,动用 Visual Studio 创建一个解决方案显得大了些。如果能够利用随时随地就方便打开的 Visual Studio Code 来开发,则清爽很多。
本文教你一分钟在 Visual Studio Code 中搭建好 C++ 开发环境。
本文总共分为三个步骤,每个步骤都非常简单。
你需要在 Visual Studio Code 中安装 C/C++ 扩展。
注意,安装完成后,要通过 Visual Studio 自带的 Developer Command Prompt for VS 2019
来启动 Visual Studio Code。这样才可以获得 Visual Studio 2019 自带的各种编译工具路径的环境变量。Visual Studio Code 就可以无缝使用 Visual Studio 2019 附带的那些工具。
然后,在新启动的命令行工具中启动 Visual Studio Code。
输入 code
即可启动:
> code
如果已有线程的路径,可以带上路径的命令行参数:
> code C:\Users\lvyi\Desktop\Walterlv.CppDemo
随便在目录中新建一个文件,写上 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
。
接下来 Visual Studio Code 就会生成一些编译所需的配置文件。
再次按下 F5 就可以直接编译 example.cpp
文件然后运行调试了。
输出在 Debug Console 里面:
如果你给 Visual Studio 设置了非默认的终端,那么需要注意:
pwsh
)不能使用 bash 系列的终端。因为 Windows 下工具使用的路径格式是反斜杠 \
,而 bash 系列终端使用的路径是斜杠 /
。如果使用 bash 终端,编译工具会因为路径问题导致编译失败。另外,不要怪我说我是这么编写教程的:
首先,我们已知 1+1=2
于是可以推导出……
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 |
你可以不记住本文的其他任何快捷键,但这个你一定要记住,那就是:
当然,因为中文输入法会占用这个快捷键,所以我更喜欢将这个快捷键修改一下,改成:
修改方法可以参见:如何快速自定义 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 以前有个漂亮的中文名字,叫做“智能感知”,不过现在大多数的翻译已经与以前的另一个平淡无奇的功能结合到了一起,叫做“自动完成列表”。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 + U
或 Ctrl + 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
切换自动换行和单行模式
dotnet 职业技术学院 发布于 2019-09-05
在使用 .NET Remoting 开发跨进程应用的时候,你可能会遇到一些异常。因为这些异常在后验的时候非常简单但在一开始有各种异常烦扰的时候却并不清晰,所以我将这些异常整理到此文中,方便小伙伴们通过搜索引擎查阅。
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
当出现此异常时,可能的原因有三个:
更具体来说,对于第一种情况,就是当你试图跨进程访问某对象的时候,此对象还没有创建。你需要做的,是控制好对象创建的时机,创建对象的进程需要比访问它的进程更早完成对象的创建和封送。也就是下面的代码需要先调用。
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 通道的,创建和访问方式必须匹配。
System.Runtime.Remoting.RemotingException:“信道“ipc”已注册。”
在同一个进程中,IpcChannel
类的默认信道名称 IpcChannel.ChannelName
值是字符串 "ipc"
。如果你不通过它的参数 properties
来指定 ["name"] = "另一个名称"
,那么你就不能重复调用 ChannelServices.RegisterChannel
来调用这个信道。
说简单点,就是上面的方法 RegisterChannel
你不能在一个进程中调用两次,即便 "portName"
不同也不行。通常你也不需要去调用两次,如果一定要,请通过 HashTable
修改 name
属性。
参考资料
dotnet 职业技术学院 发布于 2019-09-05
在 Windows 系统中,一段时间不操作键盘和鼠标,屏幕便会关闭,系统会进入睡眠状态。但有些程序(比如游戏、视频和演示文稿)在运行过程中应该阻止屏幕关闭,否则屏幕总是关闭,会导致体验会非常糟糕。
本文介绍如何编写 .NET/C# 代码临时阻止屏幕关闭以及系统进入睡眠状态。
我们需要使用到一个 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 函数、枚举和注释足够你完成你的任务了。
不过,我这里提供一些封装,以应对一些常用的场景。
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 的时候,你需要避免程序对空闲时机的控制不好,导致屏幕始终不关闭。
如果你发现无论你设置了多么短的睡眠时间和屏幕关闭时间,屏幕都不会关闭,那就是有某个程序阻止了屏幕关闭,你可以:
参考资料
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
前往仓库路径,然后删除这些分支对应的文件。
.git\refs\remotes
;比如在我的错误例子中,要删除的文件分别是:
.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
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
来决定是否抛出异常,在需要抛出异常的情况下会真实求逆,也就是上面截图中我们看到的异常发生处的代码。
那么接下来我们需要验证三点:
Visual
是哪里来的;Visual
的变换矩阵什么情况下不可求逆;throwOnError
确定传入的是 true
吗。于是我们继续往上层调用代码中查看。
可以很快验证上面需要验证的两个点:
throwOnError
传入的是 true
;Visual
是 PresentationSource
的 RootVisual
。而 PresentationSource
的 RootVisual
是什么呢?PresentationSource
是承载 WPF 可视化树的一个对象,对于窗口 Window
,是通过 HwndSource
(PresentationSource
的子类)承载的;对于跨线程 WPF UI,可以通过自定义的 PresentationSource
子类来完成。这部分可以参考我之前的一些博客:
不管怎么说,这个指的就是 WPF 可视化树的根:
Window
来显示 WPF 窗口,那么根就是 Window
类;Popup
来承载一个弹出框,那么根就是 PopupRoot
类;对于绝大多数 WPF 开发者来说,只会碰到前面第一种情况,也就是仅仅有 Window
作为可视化树的根的情况。一般人很难直接给 PopupRoot
设置变换矩阵,一般 WPF 程序的代码也很少做跨线程或跨进程 UI。
于是我们几乎可以肯定,是有某处的代码让 Window
的变换矩阵不可逆了。
什么样的矩阵是不可逆的?
发生异常的代码是 WPF 中 Matrix.Invert
方法,其发生异常的代码如下:
首先判断矩阵的行列式 Determinant
是否为 0
,如果为 0
则抛出矩阵不可逆的异常。
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 呢?
其中,原矩阵在我们的场景下就是恒等的矩阵,即 Matrix.Identity
:
接下来缩放和旋转我们都不考虑变换中心的问题,因为变换中心的问题都可以等价为先进行缩放和旋转后,再单纯进行平移。由于平移对行列式的值没有影响,于是我们忽略。
缩放矩阵。如果水平和垂直分量分别缩放 \(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} \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 变换矩阵:
现在,我们寻找问题的方向已经非常明确了:
ScaleTransform
的 Window
,检查其是否给 ScaleX
或者 ScaleY
属性赋值为了 0
。然而,真正写一个 demo 程序来验证这个问题的时候,就发现没有这么简单。因为:
我们发现,不止是 ScaleX
和 ScaleY
属性不能设为 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>
不过,这段代码并不会导致每次都出现异常,而是在非常多次尝试中偶尔能出现一次异常。
Window
类是不可以设置 RenderTransform
属性的,但允许设置恒等(Matrix.Identity
)的变换;Window
类缩放分量设置为 0
,就会出现矩阵不可逆异常。不要给 Window
类设置变换,如果要做,请给 Window
内部的子元素设置。比如上面的例子中,我们给 Grid
设置就没有问题(而且可以做到类似的效果。
dotnet 职业技术学院 发布于 2019-09-02
WPF 的 Window
类是不允许设置变换矩阵的。不过,总会有小伙伴为了能够设置一下试图绕过一些验证机制。
不要试图绕过,因为你会遇到更多问题。
当你试图给 Window
类设置变换矩阵的时候,会出现异常:
System.InvalidOperationException:“转换对于 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"
,导致看起来好像这个变换生效了一样。
此设置不仅没有效果,还会引发异常,请阅读我的另一篇博客了解:
dotnet 职业技术学院 发布于 2019-08-29
使用 Visual Studio 开发 C#/.NET 应用程序,以前有 ReSharper 来不足其各项功能短板,后来不断将 ReSharper 的功能一点点搬过来稍微好了一些。不过直到 Visual Studio 2019,才开始渐渐可以和 ReSharper 拼一下了。
如果你使用 Visual Studio 2019,那么像本文这样配置一下,可以大大提升你的开发效率。
打开菜单 “工具” -> “选项”,然后你就打开了 Visual Studio 的选项窗口。接下来本文的所有内容都会在这里进行。
在 “文本编辑器” -> “常规” 分类中,我们关心这些设置:
使鼠标单击可执行转到定义
这样按住 Ctrl 键点击标识符的时候可以转到定义(开启此选项之后,后面有其他选项可以转到反编译后的源码)当然也有其他可以打开玩的:
查看空白
专治强迫症,可以把空白字符都显示出来,这样你可以轻易看到对齐问题以及多于的空格了在 “文本编辑器” -> “C#” -> “IntelliSense” 分类中,我们关心这些设置:
键入字符后显示完成列表
删除字符后显示完成列表
突出显示完成列表项的匹配部分
显示完成项筛选器
打开这些选项可以让智能感知列表更容易显示出来,而我们也知道智能感知列表的强大显示 unimported 命名空间中的项(实验)
这一项默认不会勾选,但强烈建议勾选上;它可以帮助我们直接输入没有 using 的命名空间中的类型,这可以避免记住大量记不住的类名在 “文本编辑器” -> “C#” -> “高级” 分类中,我们关心大量设置:
支持导航到反编译源(实验)
前面我们说可以 Ctrl + 鼠标导航到定义,如果打开了这个就可以看反编译后的源码了启用可为 null 的引用分析 IDE 功能
这个功能可能还没有完成,暂时还是无法开启的当然也有其他可以打开玩的:
启用完成解决方案分析
这是基于 Roslyn 的分析,Visual Studio 的大量重构功能都依赖于它;默认关闭也可以用,只是仅分析当前正在编辑的文件;如果打开则分析整个解决方案,你会在错误列表中看到大量的编译警告在 “文本编辑器” -> “C#” -> “代码样式” 分类,如果你关心代码的书写风格,那么这个分类底下的每一个子类别都可以考虑一个个检查一下。
Visual Studio 2019 默认安装了 IntelliCode 可以充分利用微软使用 GitHub 上开源项目训练出来的模型来帮助编写代码。这些强烈建议开启。
C# 基础模型
微软利用 GitHub 开源项目训练的基础模型XAML 基础模型
微软利用 GitHub 开源项目训练的基础模型C# 参数完成
C# 自定义模型
如果针对单个项目训练出来了模型,那么可以使用专门针对此项目训练的模型EditorConfig 推理
可以根据项目推断生成 EditorConfig 文件 可以参见在 Visual Studio 中使用 EditorConfig 统一代码风格自定义模型训练提示
如果开启,那么每个项目的规模如果达到一定程度就会提示训练一个自定义模型出来训练模型会上传一部分数据到 IntelliCode 服务器,你可以去 %TEMP%\Visual Studio IntelliCode
目录来查看到底上传了哪些数据。
当然,设置好快捷键也是高效编码的重要一步,可以参考:
在你点击 “确定” 关闭了以上窗口之后,我们还需要设置一项。
确保下图中的这个按钮处于 “非选中” 状态:
这样,当出现智能感知列表的时候,我们直接就可以按下回车键输入这个选项了;否则你还需要按上下选中再回车。
dotnet 职业技术学院 发布于 2019-08-27
在 WPF 程序中,可能会存在 Application.Current.Dispatcher.Xxx
这样的代码让一部分逻辑回到主 UI 线程。因为发现在调用这句代码的时候出现了 NullReferenceException
,于是就有三位小伙伴告诉我说 Current
和 Dispatcher
属性都可能为 null
。
然而实际上这里只可能 Current
为 null
而此上下文的 Dispatcher
是绝对不会为 null
的。(当然我们这里讨论的是常规编程手段,如果非常规手段,你甚至可以让实例的 this
为 null
呢……)
当你的应用程序退出时,所有 UI 线程的代码都不再会执行,因此这是安全的;但所有非 UI 线程的代码依然在继续执行,此时随时可能遇到 Application.Current
属性为 null。
由于本文所述的两个部分都略长,所以拆分成两篇博客,这样更容易理解。
Application.Current
静态属性Application
类型的源代码会非常长,所以这里就不贴了,可以前往这里查看:
其中,Current
返回的是 _appInstance
的静态字段。因此 _appInstance
字段为 null
的时机就是 Application.Current
为 null
的时机。
/// <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
的赋值时机有两处:
Application
的实例构造函数(注意哦,是实例构造函数而不是静态构造函数);Application.DoShutdown
方法。在 Application
的实例构造函数中:
_appInstance
的赋值是线程安全的,这意味着多个 Application
实例的构造不会因为线程安全问题导致 _appInstance
字段的状态不正确。_appCreatedInThisAppDomain
为 true
那么,将抛出异常,组织此应用程序域中创建第二个 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.Current
为 null
风险。
那么,到底有哪些时机可能遇到 Application.Current
为 null
呢?这部分就与读者项目中所用的业务代码强相关了。
但是这部分业务代码会有一些公共特征帮助你判定你是否可能写出遭遇 Application.Current
为 null
的代码。
此特征是:此代码与 Application.Current
不在同一线程。
Application.Current
不在同一线程对于 WPF 程序,你的多数代码可能是由用户交互产生,即便有后续代码的执行,也依然是从 UI 交互产生。这样的代码不会遇到 Application.Current
为 null
的情况。
但是,如果你的代码由非 UI 线程触发,例如在 Usb
设备改变、与其他端的通信、某些异步代码的回调等等,这些代码不受 Dispatcher
是否调度影响,几乎一定会执行。因此 Application.Current
就算赋值为 null
了,它们也不知道,依然会继续执行,于是就会遭遇 Application.Current
为 null
。
这本质上是一个线程安全问题。
Invoke/BeginInvoke/InvokeAsync
的代码不会出问题Application.DoShutdown
方法被 ShutdownImpl
包装,且所有调用均从此包装进入,因此,所有可能导致 Application.Current
为 null
的代码,均会调用此方法,也就是说,会调用 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
。
CriticalInvokeShutdown
,即以 Send
优先级 Invoke
关闭方法,而 Send
优先级调用 Invoke
几乎等同于直接调用(为什么是等同而不是直接调用?因为还需要考虑回到 Dispatcher
初始化时所在的线程)。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.Current
为 null
。async
/ await
并使用默认上下文执行的代码,均不会遭遇 Application.Current
为 null
。(这意味着你没有使用 .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);
}
}
}
总结以上所有的分析:
Application
不在同一个线程的代码,都可能遭遇 Application.Current
为 null
。Application
在同一个线程的代码,都不可能遇到 Application.Current
为 null
。这其实是一个线程安全问题。用所有业务开发者都可以理解的说法描述就是:
当你的应用程序退出时,所有 UI 线程的代码都不再会执行,因此这是安全的;但所有非 UI 线程的代码依然在继续执行,此时随时可能遇到 Application.Current
属性为 null。
因此,记得所有非 UI 线程的代码,如果需要转移到 UI 线程执行,记得判空:
private void OnUsbDeviceChanged(object sender, EventArgs e)
{
// 记得这里需要判空,因为此上下文可能在非 UI 线程。
Application.Current?.InvokeAsync(() => { });
}
Application.Dispatcher
实例属性关于 Application.Dispatcher
是否可能为 null
的分析,由于比较长,请参见我的另一篇博客:
参考资料