dotnet 职业技术学院 发布于 2019-08-27
在 WPF 程序中,可能会存在 Application.Current.Dispatcher.Xxx
这样的代码让一部分逻辑回到主 UI 线程。因为发现在调用这句代码的时候出现了 NullReferenceException
,于是就有三位小伙伴告诉我说 Current
和 Dispatcher
属性都可能为 null
。
然而实际上这里只可能 Current
为 null
而此上下文的 Dispatcher
是绝对不会为 null
的。(当然我们这里讨论的是常规编程手段,如果非常规手段,你甚至可以让实例的 this
为 null
呢……)
由于本文所述的两个部分都略长,所以拆分成两篇博客,这样更容易理解。
Application.Dispatcher
实例属性Application.Dispatcher
实例属性来自于 DispatcherObject
。
为了分析此属性是否可能为 null
,我现在将 DispatcherObject
的全部代码贴在下面:
using System;
using System.Windows;
using System.Threading;
using MS.Internal.WindowsBase; // FriendAccessAllowed
namespace System.Windows.Threading
{
/// <summary>
/// A DispatcherObject is an object associated with a
/// <see cref="Dispatcher"/>. A DispatcherObject instance should
/// only be access by the dispatcher's thread.
/// </summary>
/// <remarks>
/// Subclasses of <see cref="DispatcherObject"/> should enforce thread
/// safety by calling <see cref="VerifyAccess"/> on all their public
/// methods to ensure the calling thread is the appropriate thread.
/// <para/>
/// DispatcherObject cannot be independently instantiated; that is,
/// all constructors are protected.
/// </remarks>
public abstract class DispatcherObject
{
/// <summary>
/// Returns the <see cref="Dispatcher"/> that this
/// <see cref="DispatcherObject"/> is associated with.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
public Dispatcher Dispatcher
{
get
{
// This property is free-threaded.
return _dispatcher;
}
}
// This method allows certain derived classes to break the dispatcher affinity
// of our objects.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void DetachFromDispatcher()
{
_dispatcher = null;
}
// Make this object a "sentinel" - it can be used in equality tests, but should
// not be used in any other way. To enforce this and catch bugs, use a special
// sentinel dispatcher, so that calls to CheckAccess and VerifyAccess will
// fail; this will catch most accidental uses of the sentinel.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void MakeSentinel()
{
_dispatcher = EnsureSentinelDispatcher();
}
private static Dispatcher EnsureSentinelDispatcher()
{
if (_sentinelDispatcher == null)
{
// lazy creation - the first thread reaching here creates the sentinel
// dispatcher, all other threads use it.
Dispatcher sentinelDispatcher = new Dispatcher(isSentinel:true);
Interlocked.CompareExchange<Dispatcher>(ref _sentinelDispatcher, sentinelDispatcher, null);
}
return _sentinelDispatcher;
}
/// <summary>
/// Checks that the calling thread has access to this object.
/// </summary>
/// <remarks>
/// Only the dispatcher thread may access DispatcherObjects.
/// <p/>
/// This method is public so that any thread can probe to
/// see if it has access to the DispatcherObject.
/// </remarks>
/// <returns>
/// True if the calling thread has access to this object.
/// </returns>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public bool CheckAccess()
{
// This method is free-threaded.
bool accessAllowed = true;
Dispatcher dispatcher = _dispatcher;
// Note: a DispatcherObject that is not associated with a
// dispatcher is considered to be free-threaded.
if(dispatcher != null)
{
accessAllowed = dispatcher.CheckAccess();
}
return accessAllowed;
}
/// <summary>
/// Verifies that the calling thread has access to this object.
/// </summary>
/// <remarks>
/// Only the dispatcher thread may access DispatcherObjects.
/// <p/>
/// This method is public so that derived classes can probe to
/// see if the calling thread has access to itself.
/// </remarks>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public void VerifyAccess()
{
// This method is free-threaded.
Dispatcher dispatcher = _dispatcher;
// Note: a DispatcherObject that is not associated with a
// dispatcher is considered to be free-threaded.
if(dispatcher != null)
{
dispatcher.VerifyAccess();
}
}
/// <summary>
/// Instantiate this object associated with the current Dispatcher.
/// </summary>
protected DispatcherObject()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
private Dispatcher _dispatcher;
private static Dispatcher _sentinelDispatcher;
}
}
代码来自:DispatcherObject.cs。
Dispatcher
属性仅仅是在获取 _dispatcher
字段的值,因此我们只需要看 _dispatcher
字段的赋值时机,以及所有给 _dispatcher
赋值的代码。
由于 _dispatcher
字段是私有字段,所以仅需调查这个类本身即可找到所有的赋值时机。(反射等非常规手段需要排除在外,因为这意味着开发者是逗比——自己闯的祸不能怪 WPF 框架。)
先来看看 dispatcher
字段的赋值时机。
DispatcherObject
仅有一个构造函数,而这个构造函数中就已经给 _dispatcher
赋值了,因此其所有的子类的初始化之前,_dispatcher
就会被赋值。
protected DispatcherObject()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
那么所赋的值是否可能为 null
呢,这就要看 Dispatcher.CurrentDispatcher
是否可能返回一个 null
了。
以下是 Dispatcher.CurrentDispatcher
的属性获取代码:
public static Dispatcher CurrentDispatcher
{
get
{
// Find the dispatcher for this thread.
Dispatcher currentDispatcher = FromThread(Thread.CurrentThread);;
// Auto-create the dispatcher if there is no dispatcher for
// this thread (if we are allowed to).
if(currentDispatcher == null)
{
currentDispatcher = new Dispatcher();
}
return currentDispatcher;
}
}
可以看到,无论前面的方法得到的值是否是 null
,后面都会再给 currentDispatcher
局部变量赋值一个新创建的实例的。因此,此属性是绝对不会返回 null
的。
由此可知,DispatcherObject
自构造起便拥有一个不为 null
的 Dispatcher
属性,其所有子类在初始化之前便会得到不为 null
的 Dispatcher
属性。
现在我们来看看在初始化完成之后,后面是否有可能将 _dispatcher
赋值为 null。
给 _dispatcher
字段的赋值代码仅有两个:
// This method allows certain derived classes to break the dispatcher affinity
// of our objects.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void DetachFromDispatcher()
{
_dispatcher = null;
}
// Make this object a "sentinel" - it can be used in equality tests, but should
// not be used in any other way. To enforce this and catch bugs, use a special
// sentinel dispatcher, so that calls to CheckAccess and VerifyAccess will
// fail; this will catch most accidental uses of the sentinel.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void MakeSentinel()
{
_dispatcher = EnsureSentinelDispatcher();
}
第一个 DetachFromDispatcher
很好理解,让 DispatcherObject
跟 Dispatcher
无关。在整个 WPF 的代码中,使用此方法的仅有以下 6 处:
Freezable.Freeze
实例方法BeginStoryboard.Seal
实例方法Style.Seal
实例方法TriggerBase.Seal
实例方法StyleHelper
在 SealTemplate
静态方法中对 FrameworkTemplate
类型的实例调用此方法ResourceDictionary
在构造函数中为 DispatcherObject
类型的 DummyInheritanceContext
属性调用此方法而 Application
类型不是以上任何一个类型的子类(Application
类的直接基类是 DispatcherObject
),因此 Application
类中的 Dispatcher
属性不可能因为 DetachFromDispatcher
方法的调用而被赋值为 null
。
接下来看看 MakeSentinel
方法,此方法的作用不如上面方法那样直观,实际上它的作用仅仅为了验证某个方法调用时所在的线程是否是符合预期的(给 VerifyAccess
和 CheckAccess
使用)。
使用此方法的仅有 1 处:
ItemsControl
所用的 ItemInfo
类的静态构造函数internal static readonly DependencyObject SentinelContainer = new DependencyObject();
internal static readonly DependencyObject UnresolvedContainer = new DependencyObject();
internal static readonly DependencyObject KeyContainer = new DependencyObject();
internal static readonly DependencyObject RemovedContainer = new DependencyObject();
static ItemInfo()
{
// mark the special DOs as sentinels. This helps catch bugs involving
// using them accidentally for anything besides equality comparison.
SentinelContainer.MakeSentinel();
UnresolvedContainer.MakeSentinel();
KeyContainer.MakeSentinel();
RemovedContainer.MakeSentinel();
}
所有这些使用都与 Application
无关。
总结以上所有的分析:
Application
类型的实例在初始化之前,Dispatcher
属性就已经被赋值且不为 null
;_dispatcher
属性的常规方法均与 Application
类型无关;因此,所有常规手段均不会让 Application
类的 Dispatcher
属性拿到 null
值。如果你还说拿到了 null
,那就检查是否有逗比程序员通过反射或其他手段将 _dispatcher
字段改为了 null
吧……
Application.Current
静态属性关于 Application.Current
是否可能为 null
的分析,由于比较长,请参见我的另一篇博客:
参考资料
dotnet 职业技术学院 发布于 2019-08-27
在微软的官方文档中,说 SetParent
可以在进程内设置,也可以跨进程设置。当使用跨进程设置窗口的父子关系时,你需要注意本文提到的一些问题,避免踩坑。
SetParent
关于 SetParent
函数设置窗口父子关系的文档可以看这个:
在这篇文章的 DPI 感知一段中明确写明了在进程内以及跨进程设置父子关系时的一些行为。虽然没有明确说明支持跨进程设置父子窗口,不过这段文字就几乎说明 Windows 系统对于跨进程设置窗口父子关系还是支持的。
但 Raymond Chen 在 Is it legal to have a cross-process parent/child or owner/owned window relationship? 一文中有另一段文字:
If I remember correctly, the documentation for
SetParent
used to contain a stern warning that it is not supported, but that remark does not appear to be present any more. I have a customer who is reparenting windows between processes, and their application is experiencing intermittent instability.
如果我没记错的话,SetParent
的文档曾经包含一个严厉的警告表明它不受支持,但现在这段备注似乎已经不存在了。我就遇到过一个客户跨进程设置窗口之间的父子关系,然后他们的应用程序间歇性不稳定。
这里表明了 Raymond Chen 对于跨进程设置父子窗口的一些担忧,但从文档趋势来看,还是支持的。只是这种担忧几乎说明跨进程设置 SetParent
存在一些坑。
那么本文就说说跨进程设置父子窗口的一些坑。
我们会感觉到 Windows 中某个窗口有响应(比如鼠标点击有反应),是因为这个窗口在处理 Windows 消息。窗口进行消息循环不断地处理消息使得各种各样的用户输入可以被处理,并正确地在界面上显示。
一个典型的消息循环大概像这样:
while(GetMessage(ref msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
对于显示了窗口的某个线程调用了 GetMessage
获取了消息,Windows 系统就会认为这个线程有响应。相反,如果长时间不调用 GetMessage
,Windows 就会认为这个线程无响应。TranslateMessage
则是翻译一些消息(比如从按键消息翻译成字符消息)。真正处理 GetMessage
中的内容则是后面的调度消息 DispatchMessage
,是这个函数的调用使得我们 UI 界面上的内容可以有可见的反映。
一般来说,每个创建了窗口的线程都有自己独立的消息循环,且不会互相影响。然而一旦这些窗口之间建立了父子关系之后就会变得麻烦起来。
Windows 会让具有父子关系的所有窗口的消息循环强制同步。具体指的是,所有具有父子关系的窗口消息循环,其消息循环会串联成一个队列(这样才可以避免消息循环的并发)。
也就是说,如果你有 A、B、C、D 四个窗口,分属不同进程,A 是 B、C、D 窗口的父窗口,那么当 A 在处理消息的时候,B、C、D 的消息循环就会卡在 GetMessage
的调用。同样,无论是 B、C 还是 D 在处理消息的时候,其他窗口也会同样卡在 GetMessage
的调用。这样,所有进程的 UI 线程实际上会互相等待,所有通过消息循环执行的代码都不会同时执行。然而实际上 Windows GUI 应用程序的开发中基本上 UI 代码都是通过消息循环来执行的,所以这几乎等同于所有进程的 UI 线程强制同步成类似一个 UI 线程的效果了。
带来的副作用也就相当明显,任何一个进程卡了 UI,其他进程的 UI 将完全无响应。当然,不依赖消息循环的代码不会受此影响,比如 WPF 应用程序的动画和渲染。
对于 SetParent
造成的这些问题,实际上没有官方的解决方案,你需要针对你不同的业务采用不同的解决办法。
正如 Raymond Chen 所说:
(It’s one of those “if you don’t already know what the consequences are, then you are not smart enough to do it correctly” things. You must first become the master of the rules before you can start breaking them.)
正如有些人说的“如果你不知道后果,那么你也不足以正确地完成某件事情”。在开始破坏规则之前,您必须先成为规则的主人。
你必须清楚跨进程设置父子窗口带来的各种副作用,然后针对性地给出解决方案:
参考资料
[Is it legal to have a cross-process parent/child or owner/owned window relationship? | The Old New Thing](https://devblogs.microsoft.com/oldnewthing/?p=4683) |
dotnet 职业技术学院 发布于 2019-08-19
古老的程序员们有时会纠结命名问题,而现在,程序员们的命名已经开创了数个流派。本文整理了程序员们命名会使用到的各种流派,当然一些编程语言会同时使用数个流派。
有很多个名字,除了 PascalCase 还有 UpperCamelCase, BumpyCase。
所有单词直接连接,连接的每个单词的首字母大写。
WelcomeToReadWalterlvBlog
所有单词直接连接,连接的每个单词的首字母大写。
walterlvIsADoubi
单词的所有字母小写,单词之间通过下划线 _
连接起来。
welcome_to_read_walterlv_blog
单词的所有字母小写,单词之间通过连字符(hyphen,-
)连接起来。
walterlv-is-a-doubi
参考资料
dotnet 职业技术学院 发布于 2019-08-14
当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现错误:“BuildWindowCore 无法返回寄宿的子窗口句柄。
”。
这是很典型的 Win32 错误,本文介绍如何修复此错误。
我们在 MainWindow
中嵌入一个其他的窗口来承载新的 WPF 控件。一般情况下我们当然不会这么去做,但是如果我们要跨越进程边界来完成 WPF 渲染内容的融合的时候,就需要嵌入一个新的窗口了。
WPF 中可以使用 HwndSource
来包装一个 WPF 控件到 Win32 窗口,使用自定义的继承自 HwndHost
的类可以把 Win32 窗口包装成 WPF 控件。由于窗口句柄是可以跨越进程边界传递的,所以这样的方式可以完成跨进程的 WPF 控件显示。
你有可能在调试嵌入窗口代码的时候遇到错误:
System.InvalidOperationException:“BuildWindowCore 无法返回寄宿的子窗口句柄。”
英文是:
BuildWindowCore failed to return the hosted child window handle.
此异常的原因非常简单,是 HwndSource
的 BuildWindowCore
的返回值有问题。具体来说,就是子窗口的句柄返回了 0。
也就是下面这段代码中 return new HandleRef(this, IntPtr.Zero)
这句,第二个参数是 0。
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
const int WS_CHILD = 1073741824;
const int WS_CLIPCHILDREN = 33554432;
var parameters = new HwndSourceParameters("demo")
{
ParentWindow = hwndParent.Handle,
WindowStyle = (int)(WS_CHILD | WS_CLIPCHILDREN),
TreatAncestorsAsNonClientArea = true,
};
var source = new HwndSource(parameters);
source.RootVisual = new Button();
return new HandleRef(this, _handle);
}
要解决,就需要传入正确的句柄值。当然上面的代码为了示例,故意传了一个不知道哪里的 _handle
,实际上应该传入 source.Handle
才是正确的。
dotnet 职业技术学院 发布于 2019-08-14
当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现错误:“System.InvalidOperationException:“寄宿 HWND 必须是子窗口。”
”。
这是很典型的 Win32 错误,本文介绍如何修复此错误。
我们在 MainWindow
中嵌入一个其他的窗口来承载新的 WPF 控件。一般情况下我们当然不会这么去做,但是如果我们要跨越进程边界来完成 WPF 渲染内容的融合的时候,就需要嵌入一个新的窗口了。
WPF 中可以使用 HwndSource
来包装一个 WPF 控件到 Win32 窗口,使用自定义的继承自 HwndHost
的类可以把 Win32 窗口包装成 WPF 控件。由于窗口句柄是可以跨越进程边界传递的,所以这样的方式可以完成跨进程的 WPF 控件显示。
下面是最简单的一个例子,为了简单,没有跨进程传递 Win32 窗口句柄,而是直接创建出来。
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace Walterlv.Demo.HwndWrapping
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Content = new HwndWrapper();
}
}
public class HwndWrapper : HwndHost
{
private HwndSource _source;
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
var parameters = new HwndSourceParameters("walterlv");
_source = new HwndSource(parameters);
// 这里的 ChildPage 是一个继承自 UseControl 的 WPF 控件,你可以自己创建自己的 WPF 控件。
_source.RootVisual = new ChildPage();
return new HandleRef(this, _source.Handle);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
_source?.Dispose();
}
}
}
当运行此代码的时候,会提示错误:
System.InvalidOperationException:“寄宿 HWND 必须是子窗口。”
或者英文版:
System.InvalidOperationException:”Hosted HWND must be a child window.”
这是一个 Win32 错误,因为我们试图将一个普通的窗口嵌入到另一个窗口中,而实际上要完成嵌入需要子窗口才行。
那么如何设置一个 Win32 窗口为子窗口呢?使用 SetWindowLong
来设置 Win32 窗口的样式是可以的。不过我们因为使用了 HwndSource
,所以可以通过 HwndSourceParameters
来更方便地设置窗口样式。
我们需要将 HwndSourceParameters
那一行改成这样:
++ const int WS_CHILD = 0x40000000;
-- var parameters = new HwndSourceParameters("walterlv");
++ var parameters = new HwndSourceParameters("walterlv")
++ {
++ ParentWindow = hwndParent.Handle,
++ WindowStyle = WS_CHILD,
++ };
最关键的是两点:
WindowStyle
为 WS_CHILD
;ParentWindow
为 hwndParent.Handle
(我们使用参数中传入的 hwndParent
作为父窗口)。现在再运行,即可正常显示此嵌套窗口:
另外,WindowStyle
属性最好加上 WS_CLIPCHILDREN
,详情请阅读:
参考资料
dotnet 职业技术学院 发布于 2019-08-14
当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现错误:“寄宿的 HWND 必须是指定父级的子窗口。
”。
这是很典型的 Win32 错误,本文介绍如何修复此错误。
我们在 MainWindow
中嵌入一个其他的窗口来承载新的 WPF 控件。一般情况下我们当然不会这么去做,但是如果我们要跨越进程边界来完成 WPF 渲染内容的融合的时候,就需要嵌入一个新的窗口了。
WPF 中可以使用 HwndSource
来包装一个 WPF 控件到 Win32 窗口,使用自定义的继承自 HwndHost
的类可以把 Win32 窗口包装成 WPF 控件。由于窗口句柄是可以跨越进程边界传递的,所以这样的方式可以完成跨进程的 WPF 控件显示。
你有可能在调试嵌入窗口代码的时候遇到错误:
System.InvalidOperationException:“寄宿的 HWND 必须是指定父级的子窗口。”
英文是:
Hosted HWND must be a child window of the specified parent.
出现此错误,是因为同一个子窗口被两次设置为同一个窗口的子窗口。
具体来说,就是 A 窗口使用 HwndHost
设置成了 B 的子窗口,随后 A 又通过一个新的 HwndHost
设置成了新子窗口。
要解决,则必须确保一个窗口只能使用 HwndHost
设置一次子窗口。
dotnet 职业技术学院 发布于 2019-08-04
我们做的公共库可能通过 nuget.org 发布,也可能是自己搭建 NuGet 服务器。但是,如果某个包正在开发中,需要快速验证其是否解决掉一些诡异的 bug 的话,除了单元测试这种间接的测试方法,还可以在本地安装未发布的 NuGet 包的方法来快速调试。
本文介绍如何本地打包发布 NuGet 包,然后通过 mklink 收集所有的本地包达到快速调试的目的。
我有另一篇博客介绍如何将本地文件夹设置称为 NuGet 包源:
在 Visual Studio 中打开 工具
-> 选项
-> NuGet 包管理器
-> 包源
可以直接将一个本地文件夹设置称为 NuGet 包源。
其他设置方法可以去那篇博客当中阅读。
如下图,是我通过 mklink 将散落在各处的 NuGet 包的调试输出目录收集了起来:
比如,点开其中的 Walterlv.Packages
可以看到 Walterlv.Packages
仓库中输出的 NuGet 包:
由于我的每一个文件夹都是指向的 Visual Studio 编译后的输出目录,所以,只需要使用 Visual Studio 重新编译一下项目,文件夹中的 NuGet 包即会更新。
于是,这相当于我在一个文件夹中,包含了我整个计算机上所有库项目的 NuGet 包,只需要将这个文件夹设置称为 NuGet 包源,即可直接调试本地任何一个公共组件库打出来的 NuGet 包。
如下图,是我将那个收集所有 NuGet 文件夹的目录设置成为了 NuGet 源:
于是,我可以在 Visual Studio 的包管理器中看到所有还没有发布的,依然处于调试状态的各种库:
基于此,我们可以在包还没有编写完的时候调试,验证速度非常快。
dotnet 职业技术学院 发布于 2019-08-04
你可以使用 dynamic
来定义一个变量或者字段,随后你可以像弱类型语言一样调用这个实例的各种方法,就像你一开始就知道这个类型的所有属性和方法一样。
但是,使用不当又会遇到各种问题,本文收集使用过程中可能会遇到的各种问题,帮助你解决掉它们。
dynamic
可以这么用:
dynamic foo = GetSomeInstance();
foo.Run("欢迎访问吕毅(lvyi)的博客:blog.walterlv.com");
object GetSomeInstance()
{
return 诡异的东西;
}
我们的 GetSomeInstance
明明返回的是 object
,我们却可以调用真实类中的方法。
接下来讲述使用 dynamic
过程中可能会遇到的问题和解决方法。
你初次在你的项目中引入 dynamic
关键字后,会出现编译错误,提示 “缺少编译器要求的成员”。
error CS0656: 缺少编译器要求的成员“Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create”
需要为你的项目安装以下两个 NuGet 包:
于是你的项目里面会多出两个引用:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net48</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
++ <PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
++ <PackageReference Include="System.Dynamic.Runtime" Version="4.3.0" />
</ItemGroup>
</Project>
你需要引用 Microsoft.CSharp
:
于是你的项目里面会多出一项引用:
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
<ItemGroup>
++ <Reference Include="Microsoft.CSharp" />
</ItemGroup>
</Project>
{0}
是类型名称,而 {1}
是使用 dynamic
访问的属性或者方法的名称。
比如,我试图从某个 Attribute
中访问到 Key
属性的时候会抛出以下异常:
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:““System.Attribute”未包含“Key”的定义”
出现此异常的原因是:
dynamic
所引用的对象里面,没有签名相同的 public
的属性或者方法于是,如果你确认你的类型里面是有这个属性或者方法的话,那么就需要注意需要将此成员改成 public
才可以访问。
参考资料
dotnet 职业技术学院 发布于 2019-08-04
我们有弱引用 WeakReference<T>
可以用来保存可被垃圾回收的对象,也有可以保存键值对的 ConditionalWeakTable
。
我们经常会考虑制作缓存池。虽然一般不推荐这么设计,但是你可以使用本文所述的方法和代码作为按垃圾回收缓存的缓存池的设计。
既然现有 WeakReference<T>
和 ConditionalWeakTable
可以帮助我们实现弱引用,那么我们可以考虑封装这两个类中的任何一个或者两个来帮助我们完成弱引用集合。
ConditionalWeakTable
类型仅仅在 internal
级别可以访问到集合中的所有的元素,对外开放的接口当中是无法拿到集合中的所有元素的,仅仅能根据 Key 来找到 Value 而已。所以如果要根据 ConditionalWeakTable
来实现弱引用集合那么需要自己记录集合中的所有的 Key,而这样的话我们依然需要自己实现一个用来记录所有 Key 的弱引用集合,相当于鸡生蛋蛋生鸡的问题。
所以我们考虑直接使用 WeakReference<T>
来实现弱引用集合。
自己维护一个列表 List<WeakReference<T>>
,对外开放的 API 只能访问到其中未被垃圾回收到的对象。
在设计此类型的时候,有一个非常大的需要考虑的因素,就是此类型中的元素个数是不确定的,如果设计不当,那么此类型的使用者可能写出的每一行代码都是 Bug。
你可以参考我的另一篇博客了解设计这种不确定类型的 API 的时候的一些指导:
总结起来就是:
那么这个原则怎么体现在此弱引用集合的类型设计上呢?
IList<T>
我们来看看 IList<T>
接口是否可行:
public class WeakCollection<T> : IList<T> where T : class
{
public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public int Count => throw new NotImplementedException();
public bool IsReadOnly => throw new NotImplementedException();
public void Add(T item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public bool Contains(T item) => throw new NotImplementedException();
public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
public int IndexOf(T item) => throw new NotImplementedException();
public void Insert(int index, T item) => throw new NotImplementedException();
public bool Remove(T item) => throw new NotImplementedException();
public void RemoveAt(int index) => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}
this[]
、Count
、IsReadOnly
、Contains
、CopyTo
、IndexOf
、GetEnumerator
这些都是在获取状态,Add
、Clear
、Remove
是在修改状态,而 Insert
、RemoveAt
会在修改状态的同时读取状态。
这么多的获取和修改状态的方法,如果提供出去,还指望使用者能够正常使用,简直是做梦!违背以上两个原则。
ICollection<T>
那我们看看 IList<T>
的底层集合 ICollection<T>
,实际上并没有解决问题,所以依然排除不能用!
public class WeakCollection<T> : ICollection<T> where T : class
{
-- public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public int Count => throw new NotImplementedException();
public bool IsReadOnly => throw new NotImplementedException();
public void Add(T item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public bool Contains(T item) => throw new NotImplementedException();
public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
-- public int IndexOf(T item) => throw new NotImplementedException();
-- public void Insert(int index, T item) => throw new NotImplementedException();
public bool Remove(T item) => throw new NotImplementedException();
-- public void RemoveAt(int index) => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}
不过,Add
和 Remove
方法可能我们会考虑留下来,但这就不能是继承自 ICollection<T>
了。
IEnumerable<T>
IEnumerable<T>
里面只有两个方法,看起来少多了,那么我们能用吗?
public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
这个方法仅供 foreach
使用,本来如果只是如此的话,问题还不是很大,但针对 IEnumerator<T>
有一大堆的 Linq 扩展方法,于是这相当于给此弱引用集合提供了大量可以用来读取状态的方法。
这依然非常危险!
使用者随时可能使用其中一个扩展方法得到了其中一个状态,随后使用另一个扩展方法得知其第二个状态,例如:
// 判断集合中是否存在 IFoo 类型以及是否存在 IBar 类型。
var hasFoo = weakList.OfType<IFoo>().Any();
var hasBar = weakList.OfType<IBar>().Any();
对具有并发开发经验的你来说,以上方法第一眼就能识别出这是不正确的写法。然而类型既然已经开放出去给大家使用了,那么这就非常危险。关键是这不是一个并发场景,于是开发者可能更难感受到在同一个上下文中调用两个方法将得到不确定的结果。对于并发可以使用锁,但对于弱引用,没有可以使用的相关方法来快速解决问题。
因此,IEnumerable<T>
也是不能继承的。
object
看来,我们只能继承自单纯的 object
基类了。此类型没有对托管来说可见的状态,于是谁也不会多次读取状态造成状态不确定了。
因此,我们需要自行实现所有场景下的 API。
弱引用集合我们需要这些使用场景:
此场景下仅仅修改集合而不需要读取任何状态。
既然可以在参数中传入元素,说明此元素一定没有会垃圾回收;因此只要集合中还存在此元素,一定可以确定地移除,不会出现不确定的状态。
一旦满足要求,必须得到完全确定的结果,且在此结果保存的过程中一直生效。
可选考虑下面这些场景:
通常是为了复用某个缓存池的实例。
一定不能实现下面这些方法:
因为判断是否存在通常不是单独的操作,通常会使用此集合继续下一个操作,因此一定不能直接提供。
于是,我们的 API 设计将是这样的:
public class WeakCollection<T> where T : class
{
public void Add(T item) => throw new NotImplementedException();
public bool Remove(T item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public T[] TryGetItems(Func<T, bool> filter) => throw new NotImplementedException();
}
此类型已经以源代码包的形式发布到了 NuGet 上,你可以安装以下 NuGet 包阅读和使用其源代码:
安装后,你可以在你的项目中使用其源代码,并且可以直接使用 Ctrl + 鼠标点击的方式打开类型的源代码,而不需要进行反编译。
dotnet 职业技术学院 发布于 2019-08-03
.NET 中提供了一些线程安全的类型,如 ConcurrentDictionary<TKey, TValue>
,它们的 API 设计与常规设计差异很大。如果你对此觉得奇怪,那么正好阅读本文。本文介绍为这些非常不确定的行为设计 API 时应该考虑的原则,了解这些原则之后你会体会到为什么会有这些 API 设计上的差异,然后指导你设计新的类型。
像并发集合一样,如 ConcurrentDictionary<TKey, TValue>
、ConcurrentQueue<T>
,其设计为线程安全,于是它的每一个对外公开的方法调用都不会导致其内部状态错误。但是,你在调用其任何一个方法的时候,虽然调用的方法本身能够保证其线程安全,能够保证此方法涉及到的状态是确定的,但是一旦完成此方法的调用,其状态都将再次不确定。你只能依靠其方法的返回值来使用刚刚调用那一刻确定的状态。
我们来看几段代码:
var isRunning = Interlocked.CompareExchange(ref _isRunning, 1, 0);
if (isRunning is 1)
{
// 当前已经在执行队列,因此无需继续执行。
}
private ConcurrentDictionary<string, object> KeyValues { get; }
= new ConcurrentDictionary<string, object>();
object Get(string key)
{
var value = KeyValues.TryGetValue(key, out var v) ? v : null;
return value;
}
这两段代码都使用到了可能涉及线程安全的一些代码。前者使用 Interlocked
做原则操作,而后者使用并发字典。
无论写上面哪一段代码,都面临着问题:
比如前者的 Interlocked.CompareExchange(ref _isRunning, 1, 0)
我们得到一个返回值 isRunning
,然后判断这个返回值。但是我们绝对不能够判断 _isRunning
这个字段,因为这个字段非常易变,在你的任何一个代码上下文中都可能变成你不希望看到的值。Interlocked
是原子操作,所以才确保安全。
而后者,此时访问得到的字典数据,和下一时刻访问得到的字典数据将可能完全不匹配,两次的数据不能通用。
如果你正在为一个易变的状态设计 API,或者说你需要编写的类型带有很强的不确定性(类型状态的变化可能发生在任何一行代码上),那么你需要遵循一些设计原则才能确保安全。
比如要为缓存设计一个获取可用实例的方法,可以使用:
private ConcurrentDictionary<string, object> KeyValues { get; }
= new ConcurrentDictionary<string, object>();
void Get(string key)
{
// CreateCachedInstance 是一个工厂方法,所有 GetOrAdd 的地方都是用此工厂方法创建。
var value = KeyValues.GetOrAdd(key, CreateCachedInstance);
return value;
}
但是绝对不能使用:
if(!KeyValues.TryGetValue(key, out var v))
{
KeyValues.TryAdd(key, CreateCachedInstance(key));
}
这一段代码就是对并发的状态 KeyValues
做了两次访问。
ConcurrentDictionary
也正是考虑到了这种设计场景,于是才提供了 API GetOrAdd
方法。让你在获取对象实例的时候可以通过工厂方法去创建实例。
如果你需要设计这种状态极易变的 API,那么需要针对一些典型的设计场景提供一次调用就能获取此时此刻所有状态的方法。就像上文的 GetOrAdd
一样。
另一个例子,WeakReference<T>
弱引用对象的管理也是在一个方法里面可以获取到一个绝对确定的状态,而避免使用方进行两次判断:
if (weak.TryGetTarget(out var value))
{
// 一旦这里拿到了对象,这个对象一定会存在且可用。
}
一定不能提供两个方法调用来完成这样的事情(比如先判断是否存在再获取对象的实例,就像 .NET Framework 4.0 和早期版本弱引用的 API 设计一样)。
比如以下方法,是试图一个接一个地依次执行 _queue
中的所有任务。
虽然我们使用 Interlocked.CompareExchange
原子操作,但因为后面依然涉及到了多次状态的获取,导致不得不加锁才能确保安全。我们依然使用原则操作是为了避免单纯 lock
带来的性能损耗。
private volatile int _isRunning;
private readonly object _locker = new object();
private readonly ConcurrentQueue<TaskWrapper> _queue = new ConcurrentQueue<TaskWrapper>();
private async void Run()
{
var isRunning = Interlocked.CompareExchange(ref _isRunning, 1, 0);
if (isRunning is 1)
{
lock (_locker)
{
if (_isRunning is 1)
{
// 当前已经在执行队列,因此无需继续执行。
return;
}
}
}
var hasTask = true;
while (hasTask)
{
// 当前还没有任何队列开始执行,因此需要开始执行队列。
while (_queue.TryDequeue(out var wrapper))
{
// 内部已包含异常处理,因此外面可以无需捕获或者清理。
await wrapper.RunAsync().ConfigureAwait(false);
}
lock (_locker)
{
hasTask = _queue.TryPeek(out _);
if (!hasTask)
{
_isRunning = 0;
}
}
}
}
这段代码的完全解读:
Run
方法的时候,先判断当前是否已经在跑其他的任务:
isRunning
为 0
表示当前一定没有在跑其他任务,我们使用原则操作立刻将其修改为 1
;isRunning
为 1
表示当前不确定是否在跑其他任务;isRunning
为 1
的时候状态不确定,于是我们加锁来判断其是否真的有任务在跑:
lock
环境中确认 _isRunning
字段而非变量为 1
则说明真的有任务在跑,此时等待任务完成即可,这里就可以退出了;lock
环境中发现 _isRunning
字段而非变量为 0
则说明实际上是没有任务在跑的(刚刚判断为 1
只是因为这两次判断之间,并发的任务刚刚在结束的过程中),于是需要跟一开始判断为 0
一样,进入到后面的循环中;while
循环第一次是一定能进去的,于是我们暂且不谈;while
内循环中,我们依次检查并发队列 _queue
中是否还有任务要执行,如果有要执行的,就执行:
queue
中的所有任务执行完毕,我们将进入一个 lock
区间:
lock
区间里面我们再次确认任务是否已经完成,如果没有完成,我们靠最外层的 while
循环重新回到内层 while
循环中继续任务;lock
区间里面我们发现任务已经完成了,就设置 _isRunning
为 0
,表示任务真的已经完成,随后退出 while
循环;你可以注意到我们的 lock
是用来确认一开始 isRunning
为 1
时的那个不确定的状态的。因为我们需要多次访问这个状态,所以必须加锁来确认状态是同步的。
在了解了上面的用法指导后,API 设计指导也呼之欲出了:
对于多线程并发导致的不确定性,使用方虽然可以通过 lock
来规避以上第二条问题,但设计方最好在设计之初就避免问题,以便让 API 更好使用。
关于通用 API 设计指导,你可以阅读我的另一篇双语博客:
dotnet 职业技术学院 发布于 2019-08-01
WPF 框架自己实现了一套触摸机制,但同一窗口只能支持一套触摸机制,于是这会禁用系统的触摸消息(WM_TOUCH
)。这能够很大程度提升 WPF 程序的触摸响应速度,但是很多时候又会产生一些 Bug。
如果你有需要,可以考虑禁用 WPF 的内置的实时触摸(RealTimeStylus)。本文介绍禁用方法,使用 AppSwitch,而不是网上广为流传的反射方法。
在你的应用程序的 app.config 文件中加入 Switch.System.Windows.Input.Stylus.DisableStylusAndTouchSupport=true
开关,即可关闭 WPF 内置的实时触摸,而改用 Windows 触摸消息(WM_TOUCH
)。
<configuration>
<runtime>
<AppContextSwitchOverrides value="Switch.System.Windows.Input.Stylus.DisableStylusAndTouchSupport=true" />
</runtime>
</configuration>
如果你的解决方案中没有找到 app.config 文件,可以创建一个:
然后,把上面的代码拷贝进去即可。
微软的官方文档也有提到使用放射禁用的方法,但一般不推荐这种调用内部 API 的方式,比较容易在 .NET 的版本更新中出现问题:
参考资料
dotnet 职业技术学院 发布于 2019-07-30
上一讲我们介绍了如何获取文本字符,这一讲介绍文本的布局
我们之前在2019-7-29-WPF文本(1)-当显示文本时我们到底在做什么(1) - huangtengxiao介绍过,文本渲染需要经历找字符、measure、arrange、render过程。这里我们统一介绍measure和arrange过程(Layout过程)
首先我们思考下一群文本字符应该怎么对齐呢?上对齐?下对齐?
并不是,这群文字采用了一种奇妙的对齐方式,按照红线的那种不上不下的对齐,Baseline
对齐
什么是baseline呢,简单来说就是用于文本对齐的基准。毕竟不是所有的文字都和汉字一样写的方方正正的,还是有一些语言的布局为了整体的美观需要各个字符能够上下调节。所以基线能够控制字符的对齐。
有了基线还有什么呢?有的同学马上说到width和height。
有同学说这个这么简单,需要讲么?需要。
我们看看下面的图片。f的选择宽度要远小于字符的呈现宽度,而f的呈现大小“入侵”了其他字符的区域。因此简单的width和height肯定不能满足这些需求。
我们看下软件中字符的各个尺寸表示,以WPF为例。下图的black box指的是字符的显示图像的大小,或者说这个字符的Geometry.Bounds
。中间的横线就是字符的Baseline,用于字符的对齐。而Advance Width则是我们选择时看到的宽度,或者说是整个字符在文本中布局使用的宽度。而字符图形在渲染时的相对位置则是由left side bearing, Right side bearing, Upright baseline offset共同确定的。
由此,我们可以确定:
那么这么多的数据应该放在哪里呢?当然是放在字体文件的元数据中咯,这样就可以大幅减少软件层面的计算量。
OK,花了大力气介绍单个字符的布局,那么多行,多段落就会相对简单。他们和我们普通元素布局类似,只要指定位置将字符一个个”码”上去就行了。
参考链接:
dotnet 职业技术学院 发布于 2019-07-29
本文介绍在使用 Visual Studio 2019 或者命令行执行 MSBuild
dotnet build
命令时,决定是否使用 .NET Core SDK 预览版的全局配置文件。
指定是否使用 .NET Core 预览版 SDK 的全局配置文件在:
%LocalAppData%\Microsoft\VisualStudio\16.0_xxxxxxxx\sdk.txt
其中 %LocalAppData%
是 Windows 系统中自带的环境变量,16.0_xxxxxxxx
在不同的 Visual Studio 版本下不同。
比如,我的路径就是 C:\Users\lvyi\AppData\Local\Microsoft\VisualStudio\16.0_0b1a4ea6\sdk.txt
。
这个文件的内容非常简单,只有一行:
UsePreviews=True
你一定觉得奇怪,我们在 Visual Studio 2019 中设置了使用 .NET Core SDK 预览版之后,这个配置是全局生效的,即便在命令行中运行 MSBuild
或者 dotnet build
也是会因此而使用预览版或者正式版的。但是这个路径明显看起来是 Visual Studio 的私有路径。
虽然这很诡异,但确实如此,不信,可以看我是如何确认这个文件就是 .NET Core SDK 预览版的全局配置的:
另外,如果你想知道如何在 Visual Studio 2019 中指定使用 .NET Core SDK 的预览版,可以参考我的另外一篇博客:
dotnet 职业技术学院 发布于 2019-07-29
文本显示是任何软件最重要的功能之一。但是很少有同学去关注文本的底层运作原理。这个系列将会介绍什么是文本的一些逻辑,以及如何利用我们的WPF现有接口,对文本进行最大程度定制化。
首先我们要明确,对于软件布局渲染来说,无论是文本,图片还是其他控件,其根本操作都是在指定区域显示指定大小的特定图像。由此我们可以推出,软件要完成某个特定对象布局渲染,需要1、测量对象的指定大小以及可以用于布局渲染的空间大小;2、确认对象放置的位置;3、将对象信息交给渲染线程进行绘制。对于WPF来说,有一套特定的模板方法去处理该流程:Measure、Arrange、Render。而对于文本来说,在此之前还需要有一个过程——找到对应字体的字符。
我们知道对于特定的文字,只要其编码方式相同,对应的编码数值就是确定的。但是虽然文本的编码相同,但是其显示图像却可以多种多样。以“黄腾霄好瘦”为例,下图分别是宋体、楷体、华文行楷、隶书字体下的显示状态。我们可以看到,同样的文字,显示样式各不相同,因为其对应的字体不同。
字体可以理解为一个图形字典,它提供了指定“字符编码”到“字符图形”的映射关系。在windows的字符映射表中,我们可以看到这种映射关系。所以我们就能够通过字体和字符编码找到对应文本的形状。
那么加粗和斜体是怎么做的呢?我们看下宋体情况下”黄腾霄好瘦”这些文字的常规、加粗和斜体的显示状态。这里为了大家能够看清楚,特地调大了字号。
看出些情况了么?这里揭晓答案。首先是加粗。加粗很简单,你可以理解就是在常规字体上使用同样前景色的笔迹进行了一次描边,于是它就变粗了。
那么斜体呢?我们加两个背景框看看。看出来了吧,实际上一个就是一个skewtransform的事情嘛。
真的这么简单么?当然不是,我们来看一个反例,下图是Minion Pro的常规和斜体呈现的字符。最明显的是其中的f字母,根本就是两个不同的图好吧,怎么可能使用简单的transform变过来。
OK,那怎么办呢?easy,再制造一套字符,专门表示斜体时的图案。
我们在字体预览和相关设置中,可以找到Minion Pro字体,可以看到,其常规和斜体实际对应着不同的字体文件
那么粗体加斜体怎么办?造!粗体有不同的粗细程度,bold,semibold,medium?造!字体还有FontStretch控制字体横向伸展……造!造!造!
所以你可以看到Minion Pro有10个字体文件组成。当然如果你愿意,可以针对每一种字体粗细(FontWeight),斜体(FontStyle)都造出一个字体。如果你不愿意,当然也可以通过一种字体,让系统通过描边和SkewTransform的方式造出其他字体。
所以FontFamily,FontWeight,FontStyle,FontStretch可以确定一个字符的形状
听着很美妙了是不是,那么有没有想过如果这个编码的字符没有在字体中找到怎么办?
都没有还玩个锤子呀,显示个框框!
如果这么做,一定会被骂死掉。我们期望是用户的输入最大可能正确呈现。而且每个字体开发者尽可能少地开发重复的字体。
以Microsoft Himalaya为例,这款字体是针对藏文显示的字体。但是里面没有包含中文、日文、俄文等语言的字符。那么如果在这种字体下显示中文字符会怎么样呢?
除了一点点向上的偏移,其余都显示正常。
这是为什么呢?实际上,当我们在一种字体中没有找到对应的字符时,我们会采用FallBack 方案,在包含个字符编码的字体中选择一个,作为备用字体进行渲染。所以如果你当前字体没有包含该编码字符,没有关系,只要你的系统中,有其他字体能够显示就OK。但是如果所有字体都无法显示的话,那就GG了
参考链接:
dotnet 职业技术学院 发布于 2019-07-27
You might just add some simple APIs in your library and you’ll not think that will break down your compatibility. But actually, it might, that is – the source-code compatibility.
This post is written in multiple languages. Please select yours:
Assume that we’ve written a project P which references another two libraries A and B. And we have a Walterlv.A.Diagnostics.Foo
class in library A.
using Walterlv.A;
using Walterlv.B;
namespace Walterlv.Demo
{
class Hello
{
Run(Diagnostics.Foo foo)
{
}
}
}
And now we add a new class Walterlv.B.Diagnostics.Bar
class into the B library. That is adding a new API only.
Unfortunately, the code above would fail to compile because of the ambiguity of Diagnostics
namespace. The Foo
class cannot be found in an ambiguity namespace.
I write this post down to tell you that there may be source-code compatibility issue even if you only upgrade your library by simply adding APIs.