dotnet 职业技术学院

dotnet

dotnet 职业技术学院

| dotnet

按类别查找文章:dotnet


uwp dotnet-core dotnet-standard csharp 技术 gif解析 wpf windows C# WPF D2D DirectX UWP DirectComposition win2d SharpDX VisualStudio dotnetremoting rpc visualstudio Win10 编程 slack web team msbuild linux xamarin ios resharper git nuget xaml algorithm powershell ime vscode directx roslyn markdown ui ux miscellaneous cpp sharpdx algorithms JavaScript Unicode c# 工具 系统 架构 编译 数学 几何 C#入门 原理 Powershell 性能测试 命令行 asp aspdotnetcore dotnetcore 控制台 WMI .net framework .net源代码 源代码分析 Latex C 算法 Emit Roslyn MSBuild 编译器 调试 渲染 Resharper 打包 Pandoc PowerShell VisualStudio插件 TotalCommander 软件 Jenkins gif await 安装包 InnoSetup 触摸 WPF调试 图片处理 黑科技 UI sublimetext usb 笔迹 输入法 数据库 sqlite Framework dotnetframework remoting 布局 mvvm frp Avalonia 设计规范 规范 反射 jekyll DevOps AzureDevOps 源代码 多线程 VisualStudio调试 doevents 性能优化 水印 uwp文件 pip python 软件设计 文档 docfx 资源分享 p2p 爬虫 SublimeText AE dotpeek 反编译 btsync pandoc Telegram 聊天软件 微信 P2P PPT v8 .NET JVM Direct2D MobaXterm 代理 ssh vps 代理服务器 mock 单元测试 NuGet dnc 进程通信 IPC pipe ScrollViewer WPF源代码 ink dotnettool tool Github GithubAction Diagnostics DUMP Xamarin GTK
2020
12-04 2020

dotnet OpenXML 从文档生成创建文档的代码的库

本文和大家介绍 Serialize.OpenXml.CodeGen 这个支持从某个文档生成用于创建出这个文档的 C# 或 VB 代码的库。作用就是可以让小伙伴在拿到一份模版文件之后,可以通过 Serialize.OpenXml.CodeGen 生成能创建出这份文档的 C# 或 VB 的代码,用于在这份代码上面更改功能,做到创建定制 Docx 或 PPTX 或 Xlsx 文档的功能

12-03 2020

dotnet C# 使用阿里函数计算服务

现在有很多函数计算服务,通过函数计算服务可以在服务上运行自己的代码,而不需要理会服务器的部署以及运行代码存在的并发问题。这些问题都会在云进行计算。使用阿里的函数计算服务可以使用 dotnet core 2.1 的框架,同时测试使用免费。本文告诉搭建如何在阿里的函数计算服务上运行自己的代码

12-03 2020

dotnet C# 图片等比限制最大和最小大小缩放算法

本文只是告诉大家如何计算缩放之后的宽度和高度,不包含实际的图片缩放方法

12-03 2020

dotnet C# 如何不联网下快速获取一段url链接里面读取文件名

在不联网情况下,从 url 获取文件名也就是只能靠 url 本身了,如果是 get 的链接很多链接后面都是带上了文件名,那么如何用最短的代码获取链接里面的文件名

12-03 2020

dotnet C# 如何让 Json 序列化数组时序列化继承类的属性

如果我使用的是具体的数组而我的数组是基类数组,而我传入子类的元素进行 json 序列化,可能发现 Json.NET 序列化没有包含子类元素的属性。如果要包含子类的属性或字段,可以在序列化的类数组定义为 object 数组的方式

12-03 2020

dotnet CBB 为什么决定推送 Tag 才能打包

通过推送 Tag 才打 NuGet 包的方法的作用不仅仅是让打包方便,让打包这个动作可以完全在本地执行,无需关注其他系统的使用步骤。更重要的是可以强制每个可能被安装的 NuGet 包版本都能有一个和他对应的 Tag 号,原因是为了解决回退到某个版本发现有一个坑,这个坑是因为某个依赖库的版本问题,此时我期望最小改动,我虽然能拿到这个库的代码,但是我很难知道我这个版本安装的 NuGet 库对应依赖库的哪个 commit 的代码

12-03 2020

asp dotnet core 不正经的提升效率的单元测试方法

在写 asp dotnet core 时,如果没有单元测试保证,需要每个方法都从 web api 的入口开始运行,此时的执行效率是很低的。而如果写单元测试,又有一个坑的问题是写单元测试也是需要时间的。本文告诉大家一些提高效率的方法,这些方法不是正经的用法,但是能提升效率。至于能不能用好不好用就请观众老爷自己决定

12-03 2020

asp dotnet core 从零开始创建一个 WebApi 服务

本文将从最简单开始,新从全控制台开始创建一个 WebApi 服务,然后再使用 VisualStudio 创建服务

12-03 2020

聊聊 2020 的 dotnet 各大开源项目仓库的情况

现在是 2020.06.21 我来聊聊我所关注的 dotnet 的几个大开源项目的活跃以及定位情况。包括 WPF 和 WinForms 和 Xamarin 和 ASP.NET Core 和 Blazor 仓库

12-03 2020

dotnet 配合 GitHub 的 Action 做自动推 Tag 时打包 NuGet 包

被微软收购的 GitHub 越来越好用,拥有大量免费的工具资源和构建服务器资源,再加上私有项目的无限制使用,我有大量的项目都在向 GitHub 迁移。通过 GitHub 的 Action 的自动构建,可以用上微软土豪的服务器资源,进行自动化测试和构建。对于 CBB 来说,发布就是打出 NuGet 包然后上传到内部 NuGet 服务器。此时遇到的问题是,如何在 GitHub 上执行打包,打包的时候如何指定 NuGet 包的版本号。因为 CBB 的特殊性,我要求每个 NuGet 正式发布的包都应该有一个对应的 Tag 号,这样将 NuGet 库安装到项目里面,之后发现问题了还能找到对应版本的代码

12-03 2020

dotnet 使用 OpenXML SDK 解析 Office 的项目符号 buNone 含义

在解析 PPT 文档的时候,文本是比较复杂的一部分,而因为很多有趣的坑,即使是微软大法也有一些诡异的设计,例如项目符号的是否显示

12-03 2020

dotnet 配合 Gitlab 做自动推 Tag 时打包 NuGet 包

我现在的团队内部用的是 Gitlab 工具,在此工具上提供了 Gitlab CI CD 用于做自动化测试和构建。对于 CBB 来说,发布就是打出 NuGet 包然后上传到内部 NuGet 服务器。此时遇到的问题是,如何在 Gitlab 上执行打包,打包的时候如何指定 NuGet 包的版本号。因为 CBB 的特殊性,我要求每个 NuGet 正式发布的包都应该有一个对应的 Tag 号,这样将 NuGet 库安装到项目里面,之后发现问题了还能找到对应版本的代码 本文告诉大家如何配合 Gitlab 做自动推 Tag 时打包 NuGet 包。也就是本地打一个 Tag 号,推送到 Gitlab 上,就会出发 Gitlab 的自动构建,自动构建里面将会获取 Tag 版本号,然后打出 NuGet 包推送到服务器

12-03 2020

程序猿修养 从安装 dotnet 开始

本来程序员的开始应该是从命令行开始,但是能看到博客的小伙伴,预计都了解命令行了。但是如果是一个空命令行,能做的事情实在不够清真,能提升的效率也有限。如何站在巨人的键盘(没写错)上,用大佬写的各个工具提升日常逗比的效率?答案是从安装 dotnet 开始

12-03 2020

dotnet tool 判断博客文档链接是否可用的工具

本文来和大家安利一个好用的工具,通过这个工具可以找到自己博客文档里面,是否存在有链接已经失效了

12-03 2020

dotnet 在 GitHub 的 Action 上部署自动代码编码规范机器人

我们的项目中会包含有很多文件,但是可能我们没有注意到的,我们的文件的编码不一定是 UTF-8 编码,这就可能让构建出来的应用程序在别人电脑运行时出现乱码,或者别人拉下来代码,却发现代码里面的中文都是乱码。为了解决文件编码的问题,咱需要一个编码规范工具,本文将告诉大家在 GitHub 上仓库,可以利用 GitHub 的 Action 部署自动代码文件编码规范的机器人,这个机器人可以自动协助咱规范文件的编码规范。可以设置为每次上传代码的时候,自动帮忙设置文件编码为 UTF-8 编码。或者在每次代码合并到主分支之后,机器人将会尝试修复文件的编码,如存在文件需要修复的,那机器人将会创建一个代码审查

12-03 2020

dotnet tool 文件编码规范命令行工具

在咱项目里面,大家是否有关注过文件的编码,一个文件是作为 Ascii 编码保存的,还是作为 GBK 编码保存的,还是 UTF8 编码保存的?不同的编码是否对应用有影响?其实是会有影响的,在 C# 里面的字符串常量等都会受到文件编码的影响。例如我的应用主输出是 UTF8 编码,此时我在二进制可执行文件里面保存的输出字符串的二进制是采用 GBK 编码的内容,在一些用户的设备上也许就会显示出乱码 本文来安利大家一个 dotnet 工具,这个工具可以用来协助大家找到项目里面的编码不规范文件

12-03 2020

dotnet 基于 dotnet format 的 GitHub Action 自动代码格式化机器人

是不是大家也会觉得代码审查里面审查代码格式化问题是无意义的,但是不审查又觉得过不去?是否有个专门的工具人,用来协助修复代码格式化的问题?本文来安利大家一个特别好用的方法,使用 dotnet 完全开源的专业格式化工具 dotnet format 配合 GitHub 的自动构建 Action 做的自动代码格式化机器人,这个机器人可以被指定到特定时机,如每天晚上或者每次代码合并等,进行代码格式化,格式化完成之后,可以选择直接推送或者提代码审查

12-03 2020

dotnet 让 C# 可以通过动态生成 HLSL 使用 DX12 的 GPU 并行计算库 ComputeSharp 的简介

本文来安利大家一个超强的库,这个库可以让你的 C# 代码利用上 GPU 显卡的性能,进行一些并行计算。这个库是基于 DirectX12GameEngine 的 ComputeSharp 库。在这个库里面将会动态生成 HLSL 代码,使用着色器的方式在 GPU 上跑起来

12-03 2020

dotnet tool 工具安装提示 Could not find a part of the path 安装失败

我在安装 dotnet tool 工具时发现所有的工具都安装失败,全部都提示 Could not find a part of the path 安装失败。我重新安装了 dotnet SDK 也没有用,更新到了3.1.402 版本也没有修复

12-03 2020

asp dotnet core 记一次应用拒绝响应调试 开启线程等待同步用光线程池

我有一个上古的库,我使用这个库用来上报日志,而刚才日志服务挂了。然后我就发现了我的应用拒绝响应了,通过 VisualStudio 断点调试可以发现线程池的线程全部被占用了。因为没有可用线程因此所有对 asp dotnet core 应用的访问全部都不会收到响应,为什么我的另一个应用日志服务挂了会让我的业务应用拒绝响应?为什么我的业务应用会使用线程池所有的线程,为什么线程池的所有线程被占用将会让应用拒绝响应

12-03 2020

dotnet 双缓存数据结构设计 下载库的文件写入缓存框架

我在写一个文件下载库,这个下载库利用断点续传机制,支持多线程下载一个文件。但是文件写入只能支持单线程,我不想让网络下载需要等待磁盘写入,因此我需要先在内存做缓存,然后让磁盘写入。配合 DirectX 渲染的设计方法,采用双缓存数据结构设计,也就是有两个集合,其中一个集合用来被其他模块写入,另一个集合用来作为当前使用。此时能做到网络下载使用的集合和文件写入的集合不是相同的一个集合,因此两部分的速度差异将不会相互影响

12-03 2020

dotnet OpenXML 颜色变换

颜色变换是对基础颜色的相关属性的修改。 例如,透明度是与颜色相关的属性。 颜色转换被指定为任何颜色模型标签的子标签,可以在任何颜色加上颜色变换

12-03 2020

dotnet 在国产 UOS 系统利用 dotnet tool 工具做文件传输

我在一台设备上安装了 UOS 系统,但是我如何在我的主开发设备上和 UOS 系统传输文件?通过 dotnet tool 工具可以完成大部分的工作,当然,使用 dotnet tool 不仅做文件传输,还能做很多特别强大的应用。本文就使用文件传输作为例子告诉大家如何使用 dotnet tool 在多个平台里面使用相同的一套技术和命令行作为工具

12-03 2020

dotnet 在 UOS 国产系统上安装 dotnet sdk 的方法

本文告诉大家如何在 UOS 国产系统上安装 dotnet sdk 的方法

12-03 2020

dotnet 在 UOS 国产系统上使用 Xamarin Forms 创建 xaml 界面的 GTK 应用

在前面几篇博客告诉大家如何部署 GTK 应用,此时的应用是特别弱的,大概只是到拖控件级。尽管和 WinForms 一样也能写出特别强大的应用,但是为了提升一点开发效率,咱开始使用 xaml 神器写界面。本文告诉大家如何在 UOS 国产系统上,通过 Xamarin.Forms 使用 XAML 写界面逻辑,构建出 GTK 应用

12-03 2020

dotnet 在 UOS 国产系统上安装 MonoDevelop 开发工具

本文告诉大家如何在 UOS 上安装 MonoDevelop 开发工具。本文使用的 UOS 是 UOS 20 x64 版本,这个系统版本是基于 debian 10 的,默认的 MonoDevelop 开发工具是没有提供 UOS 版本,但是有提供 debian 10 版本

12-03 2020

dotnet 在 UOS 国产系统上使用 MonoDevelop 进行拖控件开发 GTK 应用

先从一个 Hello World 应用开始,试试和古老的 WinForms 一样的拖控件式开发

12-03 2020

dotnet 在 UOS 国产系统上使用 MonoDevelop 创建 GTK 全平台带界面应用

本文告诉大家如何在 UOS 国产系统上开始使用 MonoDevelop 开发,通过创建 GTK# 应用,进入界面开发的第一步

12-03 2020

dotnet 执行 docker 容器 error MSB4018 CreateAppHost 任务意外失败可能原因

在使用 ASP.NET Core 的 docker 调试的时候,在生成的这一步提示 C:\Program Files\dotnet\sdk\3.1.201\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.targets(424,5): error MSB4018: “CreateAppHost”任务意外失败 可能的原因是 docker 内之前的容器没有关闭

12-03 2020

dotnet 在 UOS 国产系统上安装 Mono 开发工具的方法

本文使用的 UOS 是 UOS 20 x64 版本,这个系统版本是基于 debian 10 的。而默认 mono-devel 是没有提供 UOS 版本,但是有提供 debian 10 版本,而 mono-devel 有很多依赖包,因此手动安装不是很靠谱

12-03 2020

dotnet OpenXML 解析 PPT 文本字体获取详解

在 OpenXML 的 PPT 的文本的字体是一个比较复杂的概念,即使在 OpenXML SDK 的辅助下,也需要自己写很多的逻辑。通过 ECMA 376 文档里面散落在各地的描述,和 dotnetCampus.OfficeDocumentZipper 工具的实验帮助之下,我摸到了本文将要告诉大家的规则,以及逻辑的实现

12-03 2020

asp dotnet core 提供大文件下载的测试

本文仅仅是提供测试使用的代码

12-03 2020

dotnet OpenXML 简单聊聊 PPT 文本解析

在 Office 里面的文本解析最全的范围是 Word 文本,就是属性数量本身就特别多。本文只是简单和大家聊聊 Office 里面的 PPT 的文本的解析入门。尽管 PPT 的文本也是采用 DrawingDL 的文本属性为主,不过会用到的属性将比 Word 少很多。本文将和小伙伴介绍 PPT 的文本存放的方式

12-03 2020

dotnet OpenXML 的 spcPct 和 spcPts 表示距离的不同

在 OpenXML 里面的文本排版里面使用到 spcPct (Spacing Percent) 和 spcPts(Spacing Points)两个不同的单位用来表示段前空白和段后空白以及行间距

12-03 2020

dotnet 通过依赖注入的 Scoped 给工作流注入相同的上下文信息

本文将来聊聊 Microsoft.Extensions.DependencyInjection 这个依赖注入框架的 Scoped 功能的一个应用,这个框架是默认 ASP.NET Core 的核心库将会默认被引用。而其他 .NET 的应用如 WPF 或 Xamarin 等也可以使用这个库。因此本文标题就是 dotnet 而不是具体哪个框架 在开发的时候,咱会有一些复杂的逻辑需要多个类合作进行执行,而在使用多个类进行执行的时候,就涉及到上下文信息的传递。例如最简单的追踪 Id 的值,假定在多个类组成的多个步骤里面,因为存在多线程调用的问题,咱在定位问题的时候需要在日志里面输出当前步骤所使用的追踪 Id 是哪个,这样就运行进行并行多次任务同时执行,同时日志不会乱

12-03 2020

C# dotnet 使用 FileStream 随机文件读写

本文说的随机文件读写的随机的反义词是顺序,这里的随机文件读写对应顺序文件读写。表示文件可以不按照顺序进行读写

12-03 2020

dotnet OpenXML 解析 PPT 页面元素文档格式

本文告诉大家在 OpenXML 里面的 PPT 页面 Slide 的元素文档格式

12-03 2020

dotnet 找到博客中引用已失败的链接地址

在我的博客里面会添加很多引用,但是有一大部分的链接失修,访问的时候访问不到了,或者需要更新。于是我写了一个工具,可以协助找到所有的已失败的链接

12-03 2020

C# dotnet 解决 Path 获取文件名路径在 Windows 构建在 Linux 执行问题

最近需要支持国产的 UOS 系统,这个系统我采用了 Xamarin 加上 GTK 开发,而我的日志系统有一个功能是记录日志的时候传入当前的文件路径,此时 NuGet 包是在 Windows 下构建的,因此传入的路径是 Window 构建服务器路径。此时在 Linux 上尝试获取文件名就炸了

12-03 2020

C# dotnet Thread.GetCurrentProcessorId 和 CurrentProcess.Id 的区别

使用 Thread.GetCurrentProcessorId 可以获取当前线程处理器的 Id 是哪个,而通过 Process.GetCurrentProcess().Id 可以获取当前进程的 Id 号,这两个的差别从上面描述就能看出

12-03 2020

C# dotnet 从后向前删除列表元素提升性能的原理

如果要从一个列表里面删除一些元素,如何做才能让性能比较高?答案是从列表的后面开始删起,从后到前删除

12-03 2020

C# dotnet 一个看上去还能用的二进制序列化帮助类

这仅是一个辅助方法帮助类,可以协助小伙伴写二进制序列化的效率,代码也还看的过去

12-03 2020

C# dotnet 一个还看的过去的 B 树实现

我尝试找到一个 B 树的实现,尽管这个东西十分简单,但是依然网上有很多不同的版本。我在 justcoding121 的版本上魔改了一下,就是本文可以用来给大家的版本

12-03 2020

C# dotnet WeakLazy 弱引用的延迟初始化实现方法

本文来告诉大家如何实现一个 WeakLazy 方法

12-03 2020

dotnet 字典类找不到 TryAdd 方法

我在给 dotnet 的 runtime 仓库提PR时,小伙伴告诉我可以使用 TryAdd 方法减少判断,但是我修改这个代码发现 100 个自动化测试都失败了,都告诉我没有找到这个方法

12-03 2020

C# dotnet TypeForwarding 的用法

在 CLR 中提供这样的支持,某个类从程序集 A 放到了程序集 B 里面,可以通过一些黑科技让类型就像原本就在程序集 A 一样。用这个方法可以比较好的解决兼容性的问题

12-03 2020

C# dotnet 获取某个字符所在 Unicode 字符平面映射

在 dotnet 里面可以通过安装 System.Text.Encodings.Web 库拿到 UnicodeRanges 这个包含了 Unicode 标准的平面映射。但是我还没有找到如何判断一个字符是落在哪个平面的逻辑,本文就告诉大家一个可以使用的方法,这个方法同时稍微兼顾了性能

12-03 2020

Azure 无服务器 Function 函数计算服务 dotnet core 3.1 创建和部署入门

本文用的是 世纪互联 的 Azure.cn 版本,这个版本因为是在国内,所以网速会快超级超级多。使用 世纪互联 的版本需要一块钱哦,用一块钱就能进入一个月的免费试用。本文主要告诉小伙伴如何使用 Azure 无服务器 Function 函数计算服务

12-03 2020

dotnet 用 Microsoft.Diagnostics.Runtime 库写代码解析 DUMP 文件

在分析 DUMP 进行自动化调试的时候,很多时候只能通过 WinDbg 和命令行调用的方式,这样的方式很难做到灵活。同时编写各个命令行的难度也特别高,这在需要对命令行的输出进行不同的分支的判断时候,难度会更大。于是找到了 Microsoft.Diagnostics.Runtime 库,这个库提供了简单的方式,可以在 C# 里面用代码写分析 DUMP 的代码

12-03 2020

dotnet 在 OpenXML SDK 的 HasValue 是什么含义

在 OpenXML SDK 里面对于很多值,如字符串等的值,都包含一个 HasValue 属性。刚入门的小伙伴会认为这就是一个和可空类型一样的属性,表示这个属性是不是为空。其实这句话只是对一半,其实这个属性表示的是在重新定义的字符串参数里面的属性是否为空

12-03 2020

dotnet 用 ASP.NET Core 制作一个可以上传库文件的 NuGet 服务器

我在写一个有趣的 WPF 应用,我想要测试这个 WPF 应用的一个功能,这个功能就是一键点击自动推送 NuGet 包到服务器。我想要做一点自动化的测试,我需要有某个假装是 NuGet 的服务器用来接收我这个应用推送的 NuGet 包。用 ASP.NET Core 写一个假装的 NuGet 服务器,支持被 NuGet 推送包是特别简单的,本文就来和大家说说这个后台如何写

12-03 2020

C# dotnet WPF 使用 OpenXml 解析 Excel 文件

在 2013 微软开源了 OpenXml 解析库,在微软的 Excel 文档,使用的文档格式就是国际规范的 OpenXml 格式。这个格式有很多版本,详细请看百度。因为表格文稿使用的是 OpenXml 在 .NET 开发可以非常简单将 Excel 文档进行解析,大概只需要两句话

12-03 2020

C# dotnet 高性能多线程工具 AsyncAutoResetEvent 异步等待使用方法和原理

在 C# 里面配合 dotnet 的 Task 可以作出 AsyncAutoResetEvent 高性能多线程工具,从命名可以看到 AsyncAutoResetEvent 的意思就是支持异步的自动线程等待事件,用于多线程竞争访问执行权,可以用在消费队列或用在限制有限线程执行的业务上

12-03 2020

C# dotnet 使用 OpenXml 解析 PPT 元素的坐标和宽度高度

在阅读本文之前,我期望你能了解基础的 PPT 解析内容,或看我的入门级博客。本文将告诉大家如何从 PPT 里面解析出通用元素的 x 和 y 的值,以及元素的宽度和高度的值

12-03 2020

C# dotnet 使用 OpenXml 解析 PPT 里面的视频

本文告诉大家如何从 PPTX 文件里面解析出视频

12-03 2020

dotnet 基于 debian 创建一个 docker 的 sdk 镜像

我不能用官方的镜像,因为我需要在小伙伴构建的 debian 镜像上面安装 dotnet sdk 用来做构建,其实在 docker 里面需要找到一个个文件,然后复制代码

12-03 2020

dotnet 用 NuGet 将自己的工具作为 dotnet tool 分发

我写了一个有趣的工具,我如何将这个工具给到小伙伴予力众生呢?只需要设定这个工具是 dotnet tool 工具就可以通过 NuGet 分发出去啦。几乎所有的 dotnet 开发者都能用上 NuGet 服务,也就是此工具可以被几乎所有的 dotnet 开发者下载使用。那么制作难度有多大呢?基本上有一个现成的项目前提下,只需两句代码,一句命令行,就能完成制作

12-03 2020

dotnet tool 安装失败因为对应的库不仅包含工具

在开发 dotnet tool 时,我将规范编码的库作为 dotnet tool 发布,但是在发布的时候本地进行安装提示DotnetToolReference 项目类型仅可包含 DotnetTool 类 型的引用

12-03 2020

dotnet 通过 dotnetCampus.YamlToCsharp 将 YAML 多语言文件构建为代码

我在团队内的几乎所有 dotnet 项目,包括 UWP 和 WPF 桌面端以及 Xamarin 移动端和 ASP.NET Core 后端等需要用到多语言的项目,我的多语言都是通过 YAML 写的,这样相对来说在项目比较小的时候维护方便。但是 YAML 写的文件要读取需要用到 YAML 解析等,这部分的解析速度不够快,于是我就写了一个工具,用于在软件构建的时候自动将 YAML 多语言文件构建为代码。这样不仅能提升软件的执行速度,还能减少软件发布时需要带出去 YAML 解析库

12-03 2020

dotnet serve 一句话开启文件服务器 通过 HTTP 将文件共享给其他设备

在当前的 IT 领域,我推荐每个开发设备都应该安装 dotnet 这个工具,在 dotnet 这个工具上有大量开发者提供了无数好用的工具。本文要告诉小伙伴的工具是 natemcmaster 大佬提供了 serve 工具,可以用来开启本地文件服务器,使用非常简单。这个方案适合在 Windows 系统和 Linux 系统和 MAC 上使用,发布的 HTTP 服务可以在任何能访问到的设备上使用

12-03 2020

dotnet 修复 C# 8.0 语法编译失败

在使用 using 等新语法时,在 VisualStudio 2019 会自动判断框架版本,如在 net 45 就不会自动使用最新版本的语法,需要修改项目文件

12-03 2020

asp dotnet core 提示 Cannot access a disposed object 解决方法

我在写一个简单的文件服务器,想要用来做客户端下载器的测试服务器,但是返回的方法提示 ObjectDisposedException: Cannot access a disposed object. Object name: ‘Cannot access a closed file.’ 原因是我的文件被释放

12-03 2020

dotnet 用 gcdump 调试应用程序内存占用

在 Linux 等系统下,没有和 Windows 下这么好的 VisualStudio 支持。在客户端的环境,也不太好在用户端安装一个 VisualStudio 调试。在遇到需要在服务器端或客户端调试应用程序的内存占用时,可以尝试使用 dotnet 的 gcdump 工具进行调试,这个工具使用十分简单,功能也很强大

12-03 2020

dotnet 进行二进制差分压缩文件

我需要对一个文件做二进制差分压缩,我有一个文件的起始点,在之后的每次更改我都记录文件的二进制的差分,这样就可以通过起始点和差分文件计算修改后的文件。通过二进制差分可以用来提高文件保存磁盘读写速度,也可以减少软件自动更新需要的文件大小

12-03 2020

dotnet 单元测试注入文件读写

在进行文件读写时,如果进行单元测试,是需要很多设计,因为本地的文件可能因为单元测试之间的占用,以及还原数据,会影响业务。本文告诉大家使用注入的方式,让文件读写一个抽象的对象

12-03 2020

WPF 从 dotnet core 3 到 dotnet 5 的变更

本文收藏我所了解的从 dotnet core 3 到 2020.11.10 发布的 dotnet 5 的 WPF 的变更

12-03 2020

Roslyn 解决 dotnet core 应用进程间引用找不到 runtimeconfig 依赖文件

我有一个强大的功能,这个功能就是在 Linux 下使用 GDI 转换 EMF 格式图片为 png 图片,但是有一些有趣的图片会让转换的进程炸掉。因此我就想让转换服务放在独立的进程,通过进程间调用,也就是命令行调用传入参数的方式,让另一个进程转换图片。而此时就会遇到一个问题,如何让这个进程也被构建,然后输出到输出路径

12-03 2020

dotnet 日志上报的 TracerId 和 SessionId 的意义

在做日志库设计的时候,我会特别考虑日志里面需要带上时间和 TracerId 和 SessionId 两个属性,本文告诉大家带上这两个属性的意义和优势

12-03 2020

dotnet OpenXML 文本 Text Line Break 的作用

本文主要和小伙伴聊 a:br 这个标记的作用

12-03 2020

dotnet OpenXML 文本字体的选择规则

在 Office 的文本排版里面,会根据字符选择使用哪个字体插槽。也就是实际上在 Office 里面可以在一个文本段里面指定多个字体,会根据实际的字符使用不同的字体

12-03 2020

dotnet 使用完全对象引用相等判断

默认在列表以及字典哈希这些都会先看对象是否有自己实现的等于判断,如果有就调用对象的。因此对象可以做到两个不同的对象返回相等。而如果需要判断对象引用相等,也就是只有相同的对象才返回相等,此时需要用到 ReferenceEquals 方法。这个判断方法是最快的判断相等的方法,只有在传入的两个参数是相同的对象的时候才会返回 true 的值

12-03 2020

C# dotnet 提示找不到 CompositionContainer 类的解决方法

在构建提示 Error CS0012 和 Error CS0246 说找不到 CompositionContainer 类,原因是没有引用 System.ComponentModel.Composition 库

12-03 2020

C# dotnet 本地代码构建没问题,但 CI 自动构建失败可能的原因

本地构建能通过至少代码上的问题不大,本文列举了一些可能的原因,小伙伴可以按照顺序依次查看代码和配置

12-03 2020

WPF dotnet core 的 Blend SDK Behaviors 库

之前版本是通过安装 Blend SDK 支持 Behaviors 库的,但是这个方法都是通过引用 dll 的方式,不够优雅。在升级到 dotnet core 3.0 的时候就需要使用 WPF 官方团队开源的 Microsoft.Xaml.Behaviors.Wpf 库代替

12-03 2020

WPF dotnet 使用本机映像 native 优化 dotnet framework 二进制文件

在 2017 我在社区问了一个问题,如何让 .NET Framework 的 WPF 等程序使用 .NET Native 构建以提升速度。在 2019.06 的时候,强大的微软提供了一个好用的库,支持将 .NET Framework 的桌面应用构建时添加 native images 本机映像支持

12-03 2020

Mac 升级到 dotnet 5 构建 Xamarin 应用失败 error MSB4186 静态方法调用语法无效

我昨天将 Mac 构建机器也升级到了 dontet 5 最新版。但是在升级之后,所有的 Xamarin 项目都在 Mac 版本的 VisualStudio 构建不通过,提示 error MSB4186: 静态方法调用语法无效。解决方法就是将 VisualStudio 更新到最新版本

12-03 2020

dotnet 开发的单代码仓库和多代码仓库的优劣

在很多大团队开发的时候,将需要用到很多项目的组合开发一个软件,一个软件需要用到的项目有很多个,很少会用到一个项目就能做到。但多个项目一起开发,在配置管理和团队管理有不同的策略,一个就是让项目拆分为多个代码仓库,另一个就是将这些项目合在一个代码仓库。两个策略不能说哪个更好,本文和大家分享我所在的团队和我参与的其他团队的策略

12-03 2020

读书笔记 dotnet 什么时候进行垃圾回收

是否有小伙伴好奇如果没有在代码调用垃圾回收,那么框架会在什么时候调用垃圾回收。本文是读还没出版的伟民哥翻译的 .NET内存管理宝典 - 提高代码质量、性能和可扩展性 这本书的笔记

12-03 2020

C# dotnet 使用判断文件夹存在的方法判断一个文件路径会怎样

假定我有一个文件的路径,我将这个文件路径放在文件夹判断方法里面,请问此时返回的是存在还是不存在?答案是返回不存在

12-03 2020

C# dotnet 使用 TaskCompletionSource 让事件转异步方法

咱今天来聊聊一个毁灭世界的故事,哦,不,是一个使用 TaskCompletionSource 让偷核武器,哦,又说错了,是让事件转换为异步的方法,让咱可以在一个方法里面顺序写下毁灭世界的逻辑

12-03 2020

dotnet 配置 Gitlab 的 CI 找不到 Runner 或找错的可能原因

使用 Gitlab 的 CI 但是任务没有执行,提示找不到 Runner 或者找错了 Runner 服务器,请看本文,从上到下看,是否有坑

12-03 2020

C# dotnet 在内存中的 double 的 NAN 和正负无穷二进制是如何存

我就好奇无穷大和 NAN 在 C# 的二进制是如何表示的

12-03 2020

C# dotnet 创建对象附加属性定义 支持附加任意类型

在 dotnet 中,通过 dotnetCampus.ClrAttachedProperty 库,可以实现给任意对象附加任意属性。以及实现创建对象的附加属性定义,使用相同的附加属性定义才能访问相同的附加属性值。在使用过 WPF 的小伙伴一定对附加属性不陌生,在 WPF 框架中很强大的一个功能就是附加属性,而针对于 WPF 框架外的 dotnet 其实也能做到相同的设计,支持定义对象附加属性

12-03 2020

C# dotnet 使用 TaskCompletionSource 实现暂停功能

在 C# 里面,可以使用 dotnet 的 TaskCompletionSource 方法自己实现一个异步任务,这个任务可以方便设置任务是否完成等做到让等待的过程继续或等待。根据这个功能可以解决在复杂的功能里面通过异步等待的方法实现暂停。做法就是等待一个异步任务,如果这个功能处于暂停,那么只需要让这个异步任务不结束,此时整个功能就会在等待,直到设置了异步任务完成

12-03 2020

C# dotnet 分割换行

我在写一个 UWP 文本阅读器,我需要提升性能,需要将文本按行绘制但是文本里面的换行分割规则有点坑,本文写了一个辅助的方法用于分割换行

12-03 2020

C# dotnet 将 Stream 保存到文件的方法

在拿到一个 Stream 如何优雅将这个 Stream 保存到代码

12-03 2020

dotnet 配置 Gitlab 的 Runner 做 CI 自动构建

今天在少珺小伙伴的协助下,使用了 gitlab 的 runner 给全组的项目做自动的构建。为什么需要使用 Gitlab 的 Runner 做自动构建,原因是之前是用的是 Jenkins 而新建一个底层库项目想要接入自动构建等,需要来回在 Gitlab 和 Jenkins 上配置,大概步骤差不多有 20 步,同时还有一堆 Jenkins 的坑。另外服务器是共有的,有其他组的小伙伴安装了诡异的工具让我的打包不断炸掉。于是我就和头像大人商量使用虚拟机环境的方法,我在空闲的服务器上安装了 VirtualBox 虚拟机,然后在虚拟机部署 Runner 接着在项目接入,这样就可以确定打包的环境,同时迁移服务器也比较方便

12-03 2020

dotnet 将C#编译为wasm让前端html使用

其实 dotnet 是全栈的首选,原因是因为可以开发的方向太多,比如大本营PC端,以及后台。还有移动端,包括 IOS 和安卓端。现在还能用来写前端,本文就来告诉大家如何在前端使用现有的C#代码,通过 WebAssembly 使用 C# 的代码支持完全静态的网页,也就是不需要任何后台的存在。同时使用 C# 编写的 WebAssembly 可以省去 js 编译时间,同时使用二进制的本地指令,运行效率也有极大的提升。兼顾了开发的友好以及更高的性能

12-03 2020

刚刚我从服务器回滚了 dotnet 5 的环境

今天是 2020.11.13 我在 CI 服务器上更新 dotnet 到 dotnet 5 以及 VS 到 16.8.1 最新版本,但是我在刚刚不得不回滚了环境…… 因为构建不通过

12-03 2020

dotnet 使用 Interlocked 实现一个无锁的快速无序仅写集合

在 dotnet 里面,可以使用 Interlocked 进行原子命令更改 int 等的值,利用这个特性可以在一个固定足够长长度的数组里面,让多线程无锁等待写入值。因为没有锁的存在,无法保证读取时的安全,因此这样的集合只能被设计为只写的集合,只有在业务上完成了所有的写之后,才能作为可读的集合取出来

12-03 2020

dotnet core 进行 XML 序列化抛出 XmlSerializers dll 文件找不到

在将原本的 dotnet framework 版本的 WPF 项目迁到 dotnet core 版本,在进行单元测试,发现在 XmlSerializer 抛出了 System.Private.CoreLib.XmlSerializers.dll 找不到的异常,其实这个只是在 XmlSerializer 的内部异常,可以忽略

12-03 2020

dotnet OpenXML SDK 形状的翻转与旋转

在 OpenXML 的 PPT 元素,形状的翻转与旋转是有逻辑关系,本文来和大家聊聊形状的翻转和形状的旋转的关系

12-03 2020

dotnet 使用 AsyncQueue 创建高性能内存生产者消费者队列

在本机相同进程中创建生产者消费者队列,可以解决很多线程安全以及高性能需求问题。本文告诉大家如何通过在 GitHub 完全开源的 AsyncWorkerCollection 库的 AsyncQueue 类创建在内存中的高性能低资源占用的生产者消费者队列

12-03 2020

dotnet 为什么开源的运行时仓库代码减少使用 Linq 语句

在 dotnet 开源的 runtime 运行时仓库里面,有微软的大佬说运行时仓库的代码应该减少使用 Linq 语句,那这又是为什么呢

12-03 2020

制作的 dotnet tool 运行失败提示依赖缺失

小伙伴做了一个很好用的 dotnet tool 工具,但是这个工具仅在他的设备上能运行,在我的设备上运行就会退出提示 An assembly specified in the application dependencies manifest (LindexiDoubi.deps.json) was not found 找不到依赖

12-03 2020

dotnet OpenXML 为什么资源使用 Relationship 引用

在 OpenXML 文档格式里面,所有的资源以及页面之间的引用等,都是通过 Relationship 的引用,如资源需要通过 GetReferenceRelationship 的方法才能拿到。那为什么要这样设计呢

12-03 2020

dotnet 为什么每个项目都会输出一个 NuGet 包而不是一个包带所有项目

在使用 dotnet 打 NuGet 包的时候会发现是每个项目都生成独立的一个 NuGet 包,通过 NuGet 引用依赖指向依赖的项目。那为什么不是我最终只打出一个 NuGet 包,这个 NuGet 包,包含了所有的项目的输出文件?每个项目独立输出是为了解决什么问题?下面让我为你解答

12-03 2020

dotnet 一些代码审查套路

本文记录一些代码审查套路,在看到小伙伴写出某些代码的时候可以告诉他这样写有锅

12-03 2020

dotnet Microsoft.Recognizers.Text 超强大的自然语言关键词提取库

本文和大家介绍一个使用超级简单,但是功能特别强大的自然语言关键词提取库,可以根据输入的自然语言提取出里面的信息。例如我在一句话里面说了哪些数值变量或者说了手机号码等

12-03 2020

dotnet OpenXml SDK 形状填充渐变色的主题色

在 Office 文档的一些有趣的设计,颜色和画刷是可以继承的,这个继承包括了属性的继承。在形状填充里面使用的渐变色是可以一部分属性放在主题里面,主要找到主题里面的画刷,替换掉形状自己定义的内容,才是形状的画刷

12-03 2020

dotnet Multi-platform App UI 多平台应用 UI 框架简介

现在可以使用 .NET 构建一切应用。富有生产力的 .NET 是当前数百万的开发者选择这个平台的理由。在 .NET 5 微软统一了整个 .NET 平台,将 .NET Core 和 Mono 以及基于 Mono 的 Xamarin 放在一起,于是就可以使用相同的一个 BCL 基础库和 SDK 工具链

12-03 2020

dotnet OpenXML SDK 文本占位符解析

在使用 OpenXML SDK 解析 PPT 文档的文本占位符的时候,需要对 PPT 的格式有一定的了解,尽管整个 OpenXML SDK 包括文档等都很详细。但是有一些细节文档上虽然有写,但是没有强调一下,就被我忽略了

12-03 2020

dotnet 使用 Avalonia 开发 UOS 原生应用

最近很火的是国产的操作系统 UOS 系统,这个系统现在存在的问题就是生态没搭建起来。作为 dotnet 开发者,可以通过很多不同方向的技术为 UOS 系统生态贡献应用,如 Xamarin 和 GTK# 或 wine 和 win32 应用,或 mono 和 WinForms 应用等。本文将安利大家使用 Avalonia 框架开发 UOS 上的原生应用

12-03 2020

dotnet core 打包构建提示 MSB3024 可能的原因

如果是在 Linux 下发布,可能是因为发布的可执行文件和文件夹重名

12-03 2020

dotnet 从零开始写一个人工智能 从一个神经元开始

现在小伙伴说的人工智能都是弱智能,可以基于神经网络来做。而神经网络是有多层网络,每一层网络都有多个神经元。那么最简单的神经网络就是只有一层,而这一层只有一个神经元,也就是整个神经网络只是有一个神经元。一个神经元可以用来做什么?可以用来做基础的与或逻辑运算器。在我没有告诉神经元与或的逻辑时,只是通过我传输的输入的值和输出的值,此时神经元经过训练就能自己学会与或的逻辑。本文就在不使用现成的人工智能框架下一步步和大家写这一个神经元和告诉大家这是如何运行的。本文特别适合小伙伴入门神经网络,或者适合小伙伴入手写代码

12-03 2020

dotnet 使用 ClearScript 执行 VBScript 和 JS 代码 无需浏览器

小伙伴都知道,使用 JS 的坑在于执行效率过低,速度过慢。如果是在客户端中,还开启一个浏览器,整个应用程序就会特别重。有没有什么方法可以让 dotnet 作为容器,执行 JScript 代码而不需要浏览器?其实有的,因为 VBScript 和 JS 代码都很好解析,所以有 ClearScript 支持使用 dotnet 解析代码执行

12-03 2020

如何使用 dotnet 5 预览版

我说的是不是 .NET Framework 5 也不是 dotnet core 5 而是 dotnet 5 这个当前是预览版的框架

12-03 2020

在 SublimeText 使用 dotnet 编译 C# 项目

在 SublimeText 搭建 C# 环境可以找到的博客基本都是使用 csc 进行构建,而我期望在 dotnet 下编译整个项目。通过 dotnet 编译整个项目可以解决编译大项目时需要打开一个控制台降低效率

12-03 2020

dotnet 从零开始写一个人工智能

本文将不使用任何人工智能框架,只用简单的 dotnet 的类,自己搭建一个人工智能网络。本文适合小伙伴跟着一步步写

12-03 2020

dotnet 使用 GZipStream 压缩字符串

在 dotnet 提供了 GZipStream 进行方便的 Stream 压缩,可以用这个方法压缩字符串

12-03 2020

dotnet 三句命令行创建运行一个 web 服务程序

现在 dotnet 的服务创建十分具有效率,本文的前提要求是电脑上面已经安装了 dotnet 程序,接下来就是三句命令行的事情

12-03 2020

dotnet 判断其他进程未响应

在 dotnet 有很多方法判断其他软件的其他进程窗口未响应

12-03 2020

dotnet core 在 MAC 系统下删除应用程序自己后调 Process Start 方法将会抛出 Win32 异常

在 MAC 系统下,如果在运行过程中,应用程序的文件被删除了,那么此时如果应用程序执行了 Process.Start 方法时,将会抛出 Win32Exception 异常

12-03 2020

dotnet 删除自身程序的方法

本文告诉大家一个逗比方法可以用来删除程序自身

12-03 2020

dotnet core 2 使用 DispatchProxy 做动态代理 AOP 入门

本文简单告诉大家如何在 .NET Core 里面使用 AOP 切面编程,使用 DispatchProxy 做任意接口的动态代理实现

12-03 2020

dotnet 给任意对象附加任意属性的库

在使用 dotnet 的时候,有时候会期望某个类多添加一个属性,但是这个类可能是放在引用库里面不能直接修改,或者添加一个属性会影响这个类的设计。那么有没有方法和 WPF 一样支持给任意对象附加任意的属性?本文告诉大家一个好用的库,用来给任意的对象附加任意的属性

12-03 2020

dotnet ConditionalWeakTable 的底层原理

在 dotnet 中有一个特殊的类,这个类能够做到附加属性一样的功能。也就是给某个对象附加一个属性,当这个对象被回收的时候,自然解除附加的属性的对象的引用。本文就来聊聊这个类的底层原理

12-03 2020

dotnet core 使用 ef 迁移常见问题

本文记录一些常见的使用 EF Core 的问题

12-03 2020

WPF 通过 dotnet core 发布单文件时 log4net 无法使用

在使用 dotnet core 版本的 WPF 可以将发布文件打包为一个exe文件,但是此时小伙伴发现 log4net 无法运行,因为 log4net 说找不到配置文件

12-03 2020

dotnet 删除文件夹方法

使用 C# 可以在 dotnet 一句话删除文件夹,但是这个方法坑,本文给大家一个好用的方法删除文件夹

12-03 2020

WPF dotnet core 如何开启 Pointer 消息的支持

在 WPF 下,可以使用和 UWP 一样的 Pointer 触摸架构,只是开启的方式和 .NET Framework 版本有细微的差异

12-03 2020

dotnet 编译 Rolsyn 编译器

这是一个套娃,在编译 Rolsyn 编译器之前需要有 Rolsyn 编译器,用 Rolsyn 编译器来编译 Rolsyn 编译器。本文来告诉大家如何构建 Rolsyn 编译器,步骤特别少,很简单

12-03 2020

dotnet 获取进程命令行参数的工具

在 Windows 下,想要获取指定进程或所有进程的命令行参数,此时需要一些工具的辅助。本文安利大家一个好用的 dotnet 工具,用于获取 Win32 进程的命令行参数

12-03 2020

dotnet 更新本地所有 Git 仓库的工具

本文来安利大家一个我做的好用的工具,这个工具可以更新某个文件夹下所有 Git 仓库,通过调用命令行的 Git 命令实现更新。这是一个 dotnet tool 工具,所有代码在 GitHub 完全开源

12-03 2020

dotnet 构建 SourceRoot items must include at least one top-level item when DeterministicSourcePaths is true 失败

在使用 dotnet 构建的时候提示 error : SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true 构建失败

12-03 2020

dotnet 如何调试 SmartSql 的实际执行 SQL 语句

在使用 SmartSql 中的 SQL 语句是支持进行属性替换,在调试时如何拿到实际执行的 SQL 命令

12-03 2020

dotnet 新 SDK Style 项目格式如何使用 InternalsVisibleToAttribute 功能

如果一个项目想要让其他某个指定的项目可以使用到 internal 的类或成员,可以通过标记 InternalsVisibleToAttribute 的方式实现

12-03 2020

dotnet 的 TaskCompletionSource 的 TrySetResult 是线程安全

在创建一个 TaskCompletionSource 期望让等待的逻辑只会被调用一次,而调用的是多线程,可以使用 TrySetResult 方法,这个方法是线程安全,只会让 TaskCompletionSource 被调用一次

12-03 2020

dotnet 定制 ILogger 实现

默认在 dotnet 里面框架提供了 Microsoft.Extensions.Logging 可以和依赖注入做日志框架,而有些业务,如需要自己定制日志行为,此时就需要定制日志

12-03 2020

dotnet 手工打一个 dotnet tool 包

现在依靠 dotnet 平台,可以方便分发工具,利用 NuGet 服务进行分发和使用工具。打一个 dotnet tool 包,可以将这个包上传到 NuGet 上,小伙伴通过和安装 NuGet 相同方式就可以将工具安装在本机上。本文将告诉大家如何手工打一个 dotnet tool 包,方便小伙伴自己写工具用来创建代码

12-03 2020

dotnet 如何在 dotnet test 单元测试控制台里输出日志内容

我在协助小伙伴调试一个只有在 GitHub 的 Action 自动测试时才会炸的问题,而我发现默认的控制台输出是不会在 GitHub 的 Action 显示的,换句话说,在使用 dotnet test 时,代码里面使用的控制台输出不会进行输出

12-03 2020

dotnet 制作 docker 提示 Insufficient space in download directory 磁盘空间不足

随着 .NET 5 的发布,我在给团队内部打一个特殊的 .NET 5 的 docker image 过程,发现了提示 Insufficient space in download directory 磁盘空间不足

12-03 2020

dotnet 关于 SmartSql 的 SQL 语句的属性替换前缀说明

本文将告诉大家在 SmartSql 的 SQL 语句的属性前缀 ParameterPrefix 的默认值和用法以及原理

12-03 2020

dotnet 在 Linux 下的 GDI 库对 EMF 图片格式的支持

我想要在 UOS 上支持上古的图片格式,也就是差不多废弃了 20 年的 EMF 和 WMF 增强图形格式,这两个格式十分古老,而在 Windows 下也存在一些不兼容的图片。我在 Windows 下是使用 GDI+ 的方法支持的,可以将 EMF 转 PNG 或 jpg 等格式。而在 UOS 下,因为 GDI+ 是跨平台的,可以使用跨平台的 System.Drawing.Common 库进行转换

12-03 2020

dotnet 在 NuGet 上搜寻好用的 dotnet tool 工具

使用 dotnet tool 可以方便分发给小伙伴很多有趣的工具,而小伙伴们也可以通过 dotnet tool 一句命令安装上强大的工具。但有一个问题是,我如何知道有这样的工具? 当前可以在 nuget.org 上搜寻到 dotnet tool 工具

12-03 2020

dotnet 入门到放弃 使用 .NET Core 卸载工具

我从 dotnet core 1 的版本到 3.1 的版本,中间安装了超级多的预览版,此时我的硬盘已经空间不够了。干的漂亮的 dotnet 提供了 .NET Core 卸载工具专门用来卸载 .NET Core 的 SDK 和运行时

12-03 2020

dotnet ValueTask 简单入门

通过 ValueTask 可以用来提升软件整体的性能,使用方法也非常简单,本文将带大家简单的入门使用这个 ValueTask 的功能

12-03 2020

dotnet 使用 Obsolete 特性标记成员过时保持库和框架的兼容性

在开发库以及框架的时候,持续维护会遇到兼容性的问题,如发现了旧版本有一些接口设计不合理,或者方法命名不符合逻辑等。此时如果直接更改原有的属性名或方法名甚至类名等,将会导致上层业务的开发者们在升级库之后构建不通过,因为缺少对应的方法。此时就需要上层业务的开发者们查阅文档才能了解如何应对升级之后带来的变动

12-03 2020

dotnet 使用 SourceLink 将 NuGet 链接源代码到 GitHub 等仓库

在发布 CBB 作为 NuGet 包的时候,我期望开发者在使用我的库进行调试,可以自动链接代码到对应打包的 GitHub 上的代码,可以从本地拿到对应的源代码进行调试。这样的调试方式对于开源项目来说,将会很方便

12-03 2020

dotnet 使用 SemaphoreSlim 可能的内存泄露

在使用 SemaphoreSlim 这个锁,能做到的是指定让任务执行几次,同时提供异步方法,减少线程占用。但异步的方法如果没有用对,会因为异步状态机的引用,而存在内存泄露

12-03 2020

dotnet OpenXML 解压缩文档为文件夹工具

做 Office 解析,是需要进行不断的测试才能了解 OpenXML 里面的属性的作用。根据 Ecma 376 的定义,文档其实只是一个压缩文件,可以使用压缩工具进行解压缩。但是我需要不断进行修改文档里面的属性,然后用 Office 打开,测试属性的效果,此时就需要有一个工具用来提升效率

12-03 2020

dotnet OpenXML 的 Slide Master 和 Slide Layout 是什么

本文来告诉大家在解析 PPT 文档的时候,元素继承以及占位符需要用到的 Slide Master 和 Slide Layout 是什么

12-03 2020

dotnet OpenXML 幻灯片 PPTX 的 Slide Id 和页面序号的关系

在使用 OpenXML SDK 进行 Office 文档的解析时,对幻灯片 PPTX 文档的页面解析也许会遇到页面顺序的问题,本文告诉大家在 Office 文档里面页面的序号和顺序之间的关系以及如何读取页面序号

12-03 2020

dotnet OpenXML 如何获取 schemeClr 颜色

颜色是一个大的主题,在 ECMA 376 里面用了 19 页 A4 描述了颜色,但仅是简单的描述。在 OpenXML 定义了 Scheme Color (schemeClr) 是用来表示主题的颜色,可以跟随主题的更改而更改颜色。例如我的文本设置为主题的文本颜色,那么在我更改文档主题的文本色就可以更改我的文本颜色

12-03 2020

dotnet OpenXML 如何判断是形状还是文本

在 OpenXML 格式里面,其实不存在文本这个元素,所有都是形状。但是在 PPT 界面看到的文本框是什么呢?其实他是特别的形状。而几乎所有的形状都可以输入文本,因此区分形状和文本的意义不会特别大,只是在做解析的时候才会碰到

12-03 2020

dotnet OpenXML 文本 Kerning 字间距的作用

在 PPT 和 Word 排版里面,小伙伴会接触到 Kerning 字间距这个属性,本文将告诉大家这个属性的功能,以及为什么需要在 OpenXml 里面设置这个属性

12-03 2020

dotnet OpenXML 文本 BodyProperties 的属性作用

本文收集 a:bodyPr 文本框属性 BodyProperties 的属性的作用

12-03 2020

dotnet OpenXML 文本 EndParagraphRunProperties 的作用

其实我想要记录的仅仅只是 a:endParaRPr 对文本布局的作用

12-03 2020

dotnet OpenXML 元素 cNvPr NonVisual Drawing Properties 重复 id 标识处理

使用 OpenXML 的格式的 Office 文档的元素,使用 p:cNvPr Non-Visual Drawing Properties 的 Id 属性作为标识,在标准协议这个标识是唯一的,但实际很多文档都存在重复的标识。本文告诉大家在使用 Office 2016 版本测试重复 id 的行为

12-03 2020

dotnet OpenXML 元素 cNvPr NonVisual Drawing Properties 的属性作用

本文收集元素属性 cNvPr Non-Visual Drawing Properties 的属性的作用

12-03 2020

dotnet Open XML 如何判断一份 Office 文档是否被加密

在拿到一份 PPTX 文档,或一份 Word 的 docx 文档,如何判断这份文档是被加密的

12-03 2020

dotnet core 应用是如何跑起来的 通过AppHost理解运行过程

在 dotnet 的输出路径里面,可以看到有一个有趣的可执行文件,这个可执行文件是如何在框架发布和独立发布的时候,找到 dotnet 程序的运行时的,这个可执行文件里面包含了哪些内容

12-03 2020

dotnet ConcurrentDictionary 的 GetOrAdd 性能比 TryGetValue 加 TryAdd 低

我在 Office 的 Open-XML-SDK 库里面找到有代码线程不安全,代码里面使用了 TryGetValue 加 TryAdd 的方法添加对象,而线程安全的方法是通过 GetOrAdd 方法。不过在小伙伴的评论我找到了 GetOrAdd 性能其实在有闭包的时候不如使用 TryGetValue 加 TryAdd 调用这两个方法,但是 GetOrAdd 的优势在于能做到只创建一次对象

12-03 2020

C# dotnet 高性能多线程工具 ExecuteOnceAwaiter 只执行一次的任务

本文将安利大家一个好用的工具,用来解决这样的问题,我有一个任务,要求这个任务在执行过程中不能被重入,只有在任务执行完成之后才能重置状态重新执行一次。换句话说就是在此任务正在执行过程中,不能重复进入此任务。同时在任务执行过程中,不能重置任务状态。在任务执行完成之后,可以保存任务的状态,直接返回任务结果。在任务执行完成之后,可以调用重置状态方法,让任务可以再次重新调用

12-03 2020

C# dotnet 自己实现一个线程同步上下文

昨天鹏飞哥问了我一个问题,为什么在控制台程序的主线程等待某个线程执行完成之后回来,是在其他线程执行的。而 WPF 在等待某个线程执行完成之后,可以回到主线程执行。其实这是因为在 WPF 和 WinForms 和 ASP.NET 框架里面都自己实现了线程同步上下文,通过线程同步上下文做到调度线程执行。本文就来和小伙伴聊一下如何自己实现一个线程同步上下文

12-03 2020

C# dotnet 带编号项目符号在 OpenXML SDK 对应的枚举值

本文告诉大家在 OpenXML SDK 里面文本框的文本带自动编号的项目符号,不同的编号在 OpenXML SDK 上的枚举值

12-03 2020

C# dotnet 使用 AsyncEx 库的 AsyncLock 异步锁

本文来安利大家一个好用的库 AsyncEx 库。这个库有一个强大的功能是支持 AsyncLock 异步锁。小伙伴都知道,在 C# 里面的 lock 关键字,在 2020 年也就是 C# 9.0 都没有让这个关键字能支持锁内部添加异步方法。此时在一些需要做异步线程安全的业务就比较坑了,而 AsyncEx 库提供了 AsyncLock 异步锁的功能,可以支持在锁里面使用异步

12-03 2020

C# dotnet 使用 startIndex 提升 IndexOf 的性能

在代码审查 WPF 仓库的时候,小伙伴告诉我说使用 dotnet core 版本的 WPF 开了 ReadyToRun 的提升还不够大,他准备开始一大波业务无关的优化,其中就包含了 xaml 中的字符串相关优化。我在 davidwrighton 大大的优化代码和 pentp 大大的代码审查里面学到了使用 startIndex 提升 IndexOf 的性能,本文就来和大家分享一下

12-03 2020

asp dotnet core 基于 TestServer 做集成测试

我有一个古老的 dotnet core 3.1 的 asp dotnet core 项目,现在我准备将他升级到 dotnet 5 了。但是我不想和博客园一样翻车,因此我需要做一点集成测试的辅助,尽管依然还是翻车了,但是我要学习博客园伟大的精神,将在这个项目里面所做的所有自动化测试项目的方法写下来

11-18 2020

dotnet 里的那些锁 AutoResetEvent 用法

本文告诉大家在 dotnet 里的 AutoResetEvent 锁的用法

11-18 2020

C# dotnet 的锁 SemaphoreSlim 和队列

本文主要是试验在顺序进入等待 SemaphoreSlim 的任务是否会按照顺序经过锁执行

10-31 2020

dotnet Framework 源代码 · Ink

本文是分析 .NET Framework 源代码的系列,主要告诉大家微软做笔迹用的思路,怎么做的笔迹才是高性能的,用户体验比较好的。我会告诉大家源代码的思想,当然这个文章会比较无聊。如果你是想做笔迹的,即使不是 WPF 开发,不是 C# 开发的,也可以看看,因为这个思想是微软的,相对还是比较好的。

10-31 2020

dotnet Framework 源代码 · ScrollViewer

本文是分析 .net Framework 源代码的系列,主要告诉大家微软做 ScrollViewer 的思路,分析很简单。 看完本文,可以学会如何写一个 ScrollViewer ,如何定义一个 IScrollInfo 或者给他滚动添加动画

10-10 2020

dotnet 多线程禁止同时调用相同的方法 禁止方法重入调用 双检锁的设计

大家在使用多线程的时候,是否有关注过线程安全的问题。如果咱的代码在使用多线程时,在相同的时间有多个线程同时执行相同的方法,此时也许就存在数据安全的问题,如多个线程之间对相同的内存进行同时的读取和修改。而让方法在多线程调用中,相同的时间会被多个线程同时执行某段代码逻辑的技术称为方法重入调用技术,而禁止方法被同时调用也就是禁止方法重入调用。在 dotnet 里面有多个方式可以做到禁止方法重入调用,本文将告诉大家如何做到禁止方法重入调用

09-26 2020

读书笔记 dotnet 大对象堆和小对象堆

本文是读还没出版的伟民哥翻译的 .NET内存管理宝典 - 提高代码质量、性能和可扩展性 这本书的笔记

09-24 2020

dotnet 找不到 PostAsJsonAsync 方法

在网络编程可以使用 PostAsJsonAsync 这个方便的方法将一个对象作为 json 推送到服务器,这个方法是一个扩展方法,在之前的框架,可以在 System.Net.Http.dll 找到这个好用的扩展方法,但是在 4.5 的时候就被移除了。本文告诉大家如何安装这个扩展方法

09-07 2020

C# dotnet 使用 OpenXml 解析 Word 文件

本文聊的 Word 是 docx 格式,这个格式遵循 ECAM 376 全球标准,使用的格式是 OpenXML 格式,在 2013 微软开源了 OpenXml 解析库。这个库里面包含了海量代码,可以使用 MB 计算的代码量,通过这个解析库,咱可以使用几行代码完成对 Word 文件的解析,从文件到内存模型

09-05 2020

dotnet core 发布只有一个 exe 的方法

在 dotnet core 发布的时候,会使用很多文件,这样发给小伙伴使用的时候不是很清真,本文告诉大家一个非官方的方法通过 warp 将多个文件打包为一个文件

[Net.ServicePointManager]::SecurityProtocol = “tls12, tls11, tls” ; Invoke-WebRequest https://github.com/dgiagio/warp/releases/download/v0.3.0/windows-x64.warp-packer.exe -OutFile warp-packer.exe

09-01 2020

asp dotnet core 3.0 接口返回 json 使用 PascalCase 格式

在 asp dotnet core 3.0 默认的 webapi 返回接口都是返回 json 格式,同时这个 json 格式使用的是 CamelCase 属性名风格。如果想要兼容之前的格式,让 webapi 返回的 json 的属性名使用 PascalCase 格式,那么请看本文

08-22 2020

C# dotnet 使用 OpenXml 解析 PPT 文件

在 2013 微软开源了 OpenXml 解析库,在微软的 PPTX 文档,使用的文档格式就是国际规范的 OpenXml 格式。这个格式有很多版本,详细请看百度。因为演示文稿使用的是 OpenXml 在 .NET 开发可以非常简单将 PowerPoint 文档进行解析,大概只需要两句话

08-08 2020

dotnet 动态代理魔法书

看到标题的小伙伴是不是想知道什么是魔法书,如果你需要写一段代码,这段代码是在做神奇的业务,只有你查询到了魔法书你才能找到这个对象,同时你还需要实现自己的接口,通过自己实现的接口调用才能用到有趣的方法

08-01 2020

dotnet 获取指定进程的输入命令行

本文告诉大家如何在 dotnet 获取指定的进程的命令行参数

07-30 2020

dotnet 新项目格式与对应框架预定义的宏

在 sdk style 的项目格式支持使用多框架开发,此时需要在代码里面通过宏判断,在编译的时候执行不同的代码。本文告诉大家在框架里面对应的预定义的条件编译符有哪些

07-29 2020

dotnet 给MatterMost订阅RSS博客

其实MatterMost是有插件可以用来订阅RSS博客的,但是这个订阅插件需要一个个网址输入,而不是一次性输入很多个,于是我去就自己写了一个控制台程序做订阅

07-28 2020

dotnet OpenXML 文本 ParagraphProperties 的属性作用

本文收集 a:pPr 段落属性 ParagraphProperties 的属性的作用

07-04 2020

dotnet 代码调试方法

本文将会从简单到高级,告诉大家如何调试 dotnet 的代码,特别是桌面端。本文将会使用到 VisualStudio 大量的功能,通过各种好用的功能提高调试方法

07-04 2020

WPF 从零开始开发 dotnet Remoting 程序

本文告诉大家如何不使用框架,从零开始开发一个 dotnet remoting 程序

07-02 2020

dotnet core 使用 GBK 编码

本文告诉大家如何在 .NET Core 中使用 GBK 编码

07-02 2020

dotnet 判断程序当前使用管理员运行降低权使用普通权限运行

有一些程序是不想通过管理员权限运行的,因为在很多文件的读写,如果用了管理员权限程序写入的程序,其他普通权限的程序是无法直接访问的。 本文告诉大家如何判断当前的程序是通过管理员权限运行,然后通过资源管理器使用普通权限运行

07-02 2020

dotnet 获取程序所在路径的方法

在 dotnet 有很多方法可以获取当前程序所在的路径,但是这些方法获取到的路径有一点不相同,特别是在工作路径不是当前的程序所在的路径的时候

07-02 2020

dotnet 通过 WMI 获取指定进程的输入命令行

本文告诉大家如何使用 WMI 通过 Process 获取这个进程传入的命令行

06-30 2020

dotnet 列表 Linq 的 Take 用法

在 dotnet 可以使用 Take 获取指定数量的元素,获取顺序是从前向后,而获取到的数量是小于等于传入的指定数量。如数组中元素的数量小于传入的指定数量,则返回数组中的所有元素。如果数组中元素的数量大于等于传入的数量,则按照数组或列表顺序返回指定数量的元素

05-11 2020

一些好用的 dotnet tool 工具

本文收藏一些好用的 dotnet tool 工具

05-10 2020

dotnet 不申请额外数组空间合并多个只读数组列表

我在写一个简单的功能,需要将两个不同的数组合并到一起,但是我的功能只是做只读,如果合并的方法需要申请额外的内存空间,将降低性能。本文写了一个简单的方法,通过判断下标的方法做遍历多个数组组合在一起,通过判断当前获取的下标在对应哪个数组下标范围内,返回对应数组的元素

03-23 2020

推荐 .NET/C# 开发者安装的几款代码分析插件或对应的代码分析 NuGet 包

如果你使用的是旧版本的 Visual Studio,那么默认的代码分析规则集是“最小建议规则集”。基于这个,写出来的代码其实只能说是能跑通过而已。随着 Roslyn 的发布,带来了越来越多更强大的代码分析器,可以为编写高质量的代码带来更多的帮助。

作为 .NET/C# 开发者,强烈建议安装本文推荐的几款代码分析器。


推荐

  1. Visual Studio 2019 自带的分析器
  2. Microsoft Code Analysis
  3. Roslynator
  4. Code Cracker
  5. Meziantou.Analyzer

类型

这里的分析器分为 Visual Studio 扩展形式的分析器和 NuGet 包形式的分析器。

Visual Studio 扩展形式的分析器可以让你一次安装对所有项目生效,但缺点是不能影响编译过程,只能作为在 Visual Studio 中编写代码时给出提示。

NuGet 包形式的分析器可以让某个项目中的所有成员享受到同样的代码分析提示(无论是否安装插件),但缺点是仅针对单个项目生效。

简介

Visual Studio 2019 自带的分析器

重构提示

IDE0051

上图生效的分析器就是 Visual Studio 2019 自带的分析器。在可能有问题的代码上,Visual Studio 的代码编辑器会显示一些文字效果来提醒你代码问题。比如这张图就是提示私有成员 Foo 未使用。

Visual Studio 2019 自带的分析器的诊断 ID 都是以 IDE 开头,因此你可以通过这个前缀来区分是否是 Visual Studio 2019 自带的分析器提示的。

另外,自带的分析器可谓非常强大,除了以上这种提示之外,还可以提示一些重复代码的修改。比如你修改了某段代码,它会提示你相似的代码也可能需要修改。

Microsoft Code Analysis

Microsoft Code Analysis 分为两种用法,一个是 Visual Studio 扩展的形式,你可以去这里下载安装或者去 Visual Studio 的扩展管理界面搜索安装;另一个是 NuGet 包的形式,你可以直接在项目的 NuGet 管理界面安装 Microsoft.CodeAnalysis.FxCopAnalyzers

这款分析器也是微软主推的代码分析器,可以分析 API 设计问题、全球化与本地化问题、稳定性问题、性能问题、安全性问题、代码使用问题等非常多的种类。

比如下图是稳定性的一个问题,直接 catch 了一个 Exception 基类:

catch

配置提示

虽然你可以通过配置规则严重性来消除提示,但是这样写通常代码也比较容易出现一些诡异的问题而难以定位。

Microsoft Code Analysis 分析器的诊断 ID 都是以 CA 开头,因此你可以通过这个前缀来区分是否是 Microsoft Code Analysis 分析器提示的。

Microsoft.CodeAnalysis.FxCopAnalyzers 的 NuGet 包实际上是一组分析器的合集,包括:

如果你想安装这款 NuGet 包,并不需要特别去 NuGet 包管理器中安装,也不需要命令行,只需要去项目的属性页面,选择“安装”就好了。如下图:

安装分析器

Roslynator

是第三方开发者开发的,代码已在 GitHub 上开源,社区非常活跃:

提供了 500 多个代码分析和重构。更值得推荐的一个原因是他为 Visual Studio 原本的很多报告了问题的代码提供了生成解决问题代码的能力。

Code Cracker

Code Cracker 是第三方开发者开发的,代码已在 GitHub 上开源:

由于这款分析器的出现比 Visual Studio 2019 早很多,所以待 Visual Studio 2019 出现的时候,他们已经出现了一些规则的重复(意味着你可能同一个问题会被 Visual Studio 报一次,又被 Code Cracker 报一次)。

虽然部分重复,但 Code Cracker 依然提供了很多 Visual Studio 2019 和 Microsoft Code Analysis 都没有带的代码质量提示。

比如,如果你代码中的文档注释缺少了某个参数的注释,那么它会给出提示:

CC0097

Code Cracker 支持的所有种类的代码分析都可以在这里查得到:

Meziantou.Analyzer

这款插件是对其他几款分析器的重要补充。如果说其他几款分析器可以帮你解决一些基本设计问题或者 Bug 的话,这款分析器可以帮你发现更大范围的问题。

最典型的,也是我推荐这款分析器的最大原因是 —— 区域和本地化!

你的每一个 ToString(),每一个字符串比较,每一个字典的构造……他都提醒你需要考虑区域问题,然后提供给你区域问题的推荐代码!

提醒需要考虑区域问题

提供的建议

配置代码分析严重程度

你的项目中对于某项规则严重性的看法也许跟微软或其他第三方分析器不一样,因此你需要自己配置规则集的严重性。

关于如何配置代码分析严重程度,你可以阅读:

03-17 2020

win10 uwp 使用 asp dotnet core 做图床服务器客户端

本文告诉大家如何在 UWP 做客户端和 asp dotnet core 做服务器端来做一个图床工具

03-06 2020

dotnet core 导出 COM 组件

在 dotnet core 3.0 支持将库导出为COM组件,本文告诉大家如何将代码导出为 COM 组件

03-05 2020

从零开始用 dotnet 做全栈开发

我是一个小白,但我想做全栈开发,我想从桌面端写到移动端,想从后台写到前端。但是我不想学习,怎么破。没关系,用 dotnet 做,这个技术方向的教程文档特别齐全,入门难度超级低。同时各个方向的开发都是差不多的,至少在 VisualStudio 安装完成之后,几乎哪个方向都能在半个小时之内做自己的修改跑起来

03-05 2020

修复 dotnet Core 缺SDK编译失败

在打开一个 sln 项目包含 dotnet core 的时候,可能在打开的时候提示找不到 sdk 一般是没有在安装的时候安装对应的开发

03-05 2020

从 dotnet core 3.0 的特性让 WPF 布局失效讨论 API 兼容

在 dotnet core 3.0 为了支持 IEEE 浮点数计算标准,修改了 Math.Max 的算法,于是在 WPF 的 Track 里面的布局依赖于之前的计算,于是在 dotnet core 3.0 的修改就让布局计算不对了。改动现有 API 的行为会让现有的代码出现不兼容问题,那么要让一个框架能稳定支持升级需要满足什么条件

03-05 2020

win10 uwp 手把手教你使用 asp dotnet core 做 cs 程序

本文是一个非常简单的博客,让大家知道如何使用 asp dot net core 做后台,使用 UWP 或 WPF 等做前台。 本文因为没有什么业务,也不想做管理系统,所以看到起来是很简单。

03-05 2020

win10 uwp 客户端如何发送类到 asp dotnet core 作为参数

本文告诉大家如何在 UWP 或 WPF 客户端通过将类转换为 json 发送到 asp dotnet core 作为方法的参数

03-05 2020

dotnet 新 sdk style 项目格式的一些命名空间和引用

在使用 sdk style 的 csproj 项目格式,会发现右击引用找不到程序集,此时有一些命名空间没有找到。本文收集一些命名空间所在的引用

03-05 2020

dotnet 新 sdk style 项目格式 文件冲突

在使用 dotnet 的 sdk style 项目格式,会默认在项目上引用文件,此时如果是从旧格式迁移,那么会发现文件冲突。多次引用相同文件

03-05 2020

dotnet 文件读写务必注意事项

这是启亮整理的文档,我就帮他发布了

03-05 2020

dotnet 判断特定进程存在方法

本文告诉大家几个方法判断特定的进程是否存在,同时对比这些方法的性能

03-05 2020

dotnet 使用 System.CommandLine 写命令行程序

在写命令行程序的时候,会遇到命令行解析的问题,以及参数的使用和规范化等坑。现在社区开源了命令行项目,可以帮助小伙伴快速开发命令行程序,支持自动的命令行解析和规范的参数

03-05 2020

dotnet 修复找不到 System.ServiceProcess 定义

本文告诉大家如果复制网上一段代码发现 System.ServiceProcess 提示找不到方法或定义,需要手动添加引用

03-05 2020

dotnet 枚举当前设备wifi热点

在 dotnet 程序没有现有的方法可以列举当前设备的无线网卡找到的 WIFI 热点,需要写一点代码才能使用

03-05 2020

dotnet 对 DateTime 排序

在写 DateTime 排序时,按照时间的先后,离现在过去越远的越小。按照从小到大排序,将会先排最过去的时间,最后的值的时间是最大的。将时间按照从1970开始计算秒数,可以算出数值,数值代表值大小

03-05 2020

dotnet 获取本机 IP 地址方法

本文告诉大家如何在 C# .NET 获取本机 IP 地址

03-05 2020

dotnet 获取当前进程方法

本文告诉大家如何在 dotnet 程序获取当前进程

03-05 2020

dotnet 命名管道名字长度限制

在 dotnet 里面可以使用 NamedPipeClientStream 作为命名管道,此时的命名有长度限制,要求在 256 字符之内

03-05 2020

dotnet 非泛型 类型 System.Collections.IEnumerable 不能与类型实参一起使用

如果在开发的时候遇到非泛型 类型“IEnumerable”不能与类型参数一起使用,那么就是变量的命名空间没弄对

03-05 2020

dotnet 通过 WMI 获取设备厂商

本文告诉大家如何通过 WMI 获取设备厂商

03-05 2020

dotnet 通过 WMI 获取系统补丁

本文告诉大家如何通过 WMI 获取补丁

03-05 2020

dotnet 通过 WMI 获取系统安装的驱动

本文告诉大家如何通过 WMI 获取用户已经安装的驱动程序

03-05 2020

dotnet 通过 WMI 获取系统启动的服务

本文告诉大家如何通过 WMI 获取系统启动的服务

03-05 2020

dotnet 通过 WMI 获取系统信息

本文告诉大家如何通过 WMI 获取系统信息

03-05 2020

dotnet 通过 WMI 拿到显卡信息

本文告诉大家如何通过 WMI 拿到显卡信息

03-05 2020

dotnet 通过 HttpClient 下载文件同时报告进度的方法

本文告诉大家一个简单的方法通过 HttpClient 下载文件,同时报告下载进度

03-05 2020

dotnet 获取用户设备安装了哪些 .NET Framework 框架

从注册表可以拿到当前用户安装的 .NET Framework 版本,本文告诉大家如何解析这些信息

03-05 2020

dotnet 线程静态字段

在 dotnet 程序提供了一个好用的特性,可以让字段作为线程的静态字段,也就是在相同线程的所有代码访问的静态字段是相同对象,但不同线程访问的时候是不同的

03-05 2020

dotnet 特性 DynamicallyInvokable 是用来做什么的

我在 Linq 很多函数都看到 __DynamicallyInvokable 这个特性,这是一个没有官方文档的特性,也许是用来优化反射

03-05 2020

dotnet 方法名 To 和 As 有什么不同

在看到 dotnet 框架里面有很多方法里面用了 ToXx 和 AsXx 好像都是从某个类转换为另一个类,那么这两个方法命名有什么不同

03-05 2020

dotnet 数组自动转基类数组提示 Co-variant array conversion 是什么问题

在 C# 的语法,可以提供自动将某个类的数组自动转这个类的基类数组的方法,但是这样的转换在 Resharper 会提示 Co-variant array conversion 这是什么问题?

03-05 2020

dotnet 控制台读写 Sqlite 提示 no such table 找不到文件

在使用 dotnet 读写 Sqlite 可以通过 EF Core 的方法,但是在 EF Core 创建的数据库可能和读写的数据库不是相同的文件

03-05 2020

dotnet 控制台 Hangfire 后台定时任务

本文告诉大家如何在 dotnet core 的控制台通过 Hangfire 开启后台定时任务

03-05 2020

dotnet 手动解决 json 解析中不合法字符串

如果使用 Newtonsoft Json 解析字符串,字符串里面有不清真的格式,那么默认的解析将会炸掉。如果想要自己解决字符串中的不清真格式,可以使用传入 JsonSerializerSettings 的方法

03-05 2020

dotnet 将文件删除到回收站

默认删除文件的时候 File.Delete 是将文件永久删除,如果是一些文档,建议删除到回收站,这样用户可以自己还原 通过 SHFileOperation 可以将文件放在回收站

03-05 2020

dotnet 如何调试某个文件是哪个代码创建

我发现了自己的软件,会在桌面创建一个 1.txt 文件,但是我不知道是哪个代码创建的,那么如何进行快速的调试找到是哪个代码创建的

03-05 2020

dotnet 如何在 Mock 模拟 Func 判断调用次数

在 dotnet 程序有很好用的 Mock 框架,可以用来模拟各种接口和抽象类,可以用来测试某个注入接口的被调用次数和被调用时传入参数。本文告诉大家如何在 Mock 里面模拟一个 Func 同时模拟返回对象,获取调用次数

03-05 2020

dotnet 启动 JIT 多核心编译提升启动性能

用2分钟提升十分之一的启动性能,通过在桌面程序启动 JIT 多核心编译提升启动性能 在 dotnet 可以通过让 JIT 进行多核心编译提升软件的启动性能,在默认托管的 ASP.NET 程序是开启的,对 WPF 等桌面程序需要手动开启

03-05 2020

dotnet 删除只读文件

如果直接通过 File.Delete 删除只读文件会出现没有权限提示,可以先设置文件不是只读然后删除文件

03-05 2020

dotnet 使用 lz4net 压缩 Stream 或文件

在 dotnet 可以使用 LZ4 这个无损的压缩算法,这个压缩算法的压缩率不高但是速度很快。这个库支持在 .NET Standard 1.6 .NET Core .NET Framework Mono Xamarin 和 UWP 运行

03-05 2020

dotnet 通过 WMI 获取系统安装软件

本文告诉大家如何通过 WMI 获取系统安装的软件,这个方法不能获取全部的软件

03-05 2020

dotnet core 通过 frp 发布自己的网站

很多时候写出来的网站只能自己内网访问,本文告诉大家如何通过 Frp 将自己的 asp dotnet core 网站发布到外网,让小伙伴访问自己的网站 通过 frp 的方式,可以解决自己的服务器性能太差的问题,通过 frp 的方式需要先存在一个外网的服务器

03-05 2020

asp dotnet core 实现服务器发送事件 Server-Sent Events 简单方式

在客户端开发时可以通过轮询的方式拿到服务器端的数据,同时在客户端开发时,如果是将客户端也作为服务器端,那么之间的通讯将会十分简单。有个逗比的小伙伴想要用我的客户端魔改,但是他又不想学习什么知识,此时他需要拿到我客户端的实时信息,好在他知道一点 html 的知识,于是让我通过服务器发送事件 Server-Sent Events 而他写一个简陋的 html 去拿到我客户端的数据 这是一个简陋的开发端的工具,开源的好处就是,你觉得不爽,自己改哇。自己改不动就等开发者下班协助啦,本文就使用一个简单的方式在 asp dotnet core 实现服务器发送事件。虽然标题是 asp dotnet core 而实际上我的软件是一个桌面端软件

03-05 2020

dotnet core 输出调试信息到 DebugView 软件

本文告诉大家如何在 dotnet core 输出调试信息到 DebugView 软件

03-05 2020

dotnet core 使用 CoreRT 将程序编译为 Native 程序

现在微软有一个开源项目 CoreRT 能通过将托管的 .NET Core 编译为单个无依赖的 Native 程序 这个项目现在还没发布,但是能尝试使用,可以带来很多的性能提升

03-05 2020

asp dotnet core 通过图片统计 csdn 用户访问

在 csdn 的访问统计里面,只能用 csdn 提供的访问统计,因为在 csdn 中不支持在博客加上 js 代码,也就是无法使用友盟等工具统计。 通过在 asp dotnet core 创建一个图片链接的方式,将这个链接作为图片放在 csdn 的博客,可以在链接被访问的时候统计用户访问

03-05 2020

dotnet Blazor 用 C# 控制界面行为

微软很久就在做 Blazor 但是我现在才开始创建一个测试项目,我想用 C# 去控制 HTML 界面。小伙伴也许会问现在前端不是烂大街么,为什么还需要 Blazor 来做。可能原因只有一个,就是可以使用 C# 写脚本,代码比较清真

03-05 2020

dotnet C# 获取本机外网 IP 地址

本文通过 SOHU 提供的服务获取本机的外网 IP 地址

03-05 2020

asp dotnet core 简单开发P2P中央服务器

在做P2P的时候,如何让设备发现是整个开发里面最重要的部分。可以采用的方式有组播、扫描局域网、追踪服务器发现等方法。其中效率最高,发现效果最好的也就是使用中央服务器了。本文告诉大家如何使用 ASP.NET Core 写一个简单的 P2P 追踪服务器

03-05 2020

dotnet core 获取 MacAddress 地址方法

本文告诉大家如何在 dotnet core 获取 Mac 地址

03-05 2020

dotnet 使用 MessagePack 序列化对象

和很多序列化库一样,可以通过 MessagePack 序列化和反序列化,和 json 相比这个库提供了二进制的序列化,序列化之后的内容长度比 json 小很多

03-05 2020

dotnet 使用 Environment.FailFast 结束程序

在运行到一些诡异的代码,这时的程序已经无法继续运行,需要退出,那么如何在记完日志之后在退出程序记录更多信息?可以通过 Environment.FailFast 里面添加字符串告诉用户当前的进程无法继续运行

03-05 2020

dotnet core 黑科技·String.IndexOf 性能

本文来告诉大家 dotnet core 里面使用的黑科技,如何提高String.IndexOf(char)的性能

03-05 2020

dotnet core 集成到 Mattermost 聊天工具

在找了很久的团队交流工具,发现了 Mattermost 最好用,但是还需要做一些定制化的功能,于是就找到了 Mattermost 插件开发,还找到了如何自己写服务集成到 Mattermost 里面 本文告诉大家,通过写一个 ASP .NET Core 程序集成到 Mattermost 里面,在里面添加很多有趣的功能

03-05 2020

dotnet core 隐藏控制台

如果写一个控制台程序,需要隐藏这个控制台程序,可以使用本文的方法

03-05 2020

dotnet core 用值初始化整个数组

如果想要创建一个数组,在这个数组初始化一个值,有多少不同的方法? 本文告诉大家三个不同的方法初始化

03-05 2020

dotnet core 发布只带必要的依赖文件

在使用 dotnet core 发布独立项目的时候,会带上大量依赖的库,但是通过微软提供的工具可以去掉一些在代码没有用到的库。

03-05 2020

dotnet core 使用 PowerShell 脚本

本文告诉大家如何在 dotnet core 通过 Host PowerShell 的方法使用 PowerShell 脚本

03-05 2020

dotnet Framework 源代码 类库的意思

本文告诉大家 dotnet framework 的源代码类库的意思

03-05 2020

asp dotnet core 支持客户端上传文件

本文告诉大家如何在 asp dotnet core 支持客户端上传文件

03-05 2020

dotnet 使用 GC.GetAllocatedBytesForCurrentThread 获取当前线程分配过的内存大小

在 dotnet framework 4.8 的时候支持调用 GC.GetAllocatedBytesForCurrentThread 获取当前线程分配过的内存大小

03-05 2020

dot net double 数组转 float 数组

本文告诉大家如果遇到 double 数组转 float 数组千万不要使用 Cast ,一般都使用 select 强转。

03-05 2020

Windows Community Toolkit 3.0 新功能

本文告诉大家一个令人震惊的消息,Windows Community Toolkit 有一个大更新,现在的版本是 3.0 。最大的提升就是 WinForm 和 WPF 程序可以使用部分 UWP 控件。 而且可以在 WPF 和 WinForms 使用微软最新 Edge 浏览器

03-05 2020

dot net core 使用 usb

本文告诉大家如何在 dot net core 使用 usb

03-05 2020

dotnet 从入门到放弃的 500 篇文章合集

本文是记录我从入门到放弃写的博客

03-05 2020

dotnet core 编程规范

本文实际只是翻译 .NET Core foundational libraries 官方文档的编码风格。

03-05 2020

How to output the target message in dotnet build command line

How can I output my target message when I using dotnet build in command line.

03-05 2020

C# await 高级用法

本文告诉大家 await 的高级用法,包括底层原理。

03-05 2020

Nuget 通过 dotnet 命令行发布

在开发完成一个好用的轮子就想将这个轮子发布到 nuget 让其他小伙伴可以来使用,但是 nuget.org 的登陆速度太慢,本文介绍一个命令行发布的方法,通过命令行发布的方法可以配合 Jenkins 自动打包

03-05 2020

C# 使用Emit深克隆

有人问,复制一个类所有属性到另一个类有多少方法?这也就是问深克隆有多少个方法,容易想的有三个。直接复制,反射复制,序列化复制。但是性能比较快的有表达式树复制 IL复制两个,本文主要讲最后一个

03-05 2020

C# dotnet core 局域网组播方法

我在微软官网找到了用 C# 做 UDP 组播的方法,我优化一些逻辑,保留核心代码,然后加了一点封装

03-05 2020

C# dotnet 线程不安全的弱引用缓存

很多逻辑都会使用内存做缓存,这样可以提高运行效率。但是有一些逻辑很少会执行,但是如果有执行就是频繁调用。如我写了文本编辑器,在我打开文件的逻辑,将会不断调用正则判断逻辑,而平时编辑很少会调用。如果将这部分的正则逻辑缓存了,那么可以提升打开文件速度,但是在打开文件之后这部分就成为内存垃圾了。本文给大家一个弱引用缓存,也就是在频繁使用时从内存获取,在不使用时会被回收,这样可以提升性能也能减少内存使用

03-05 2020

C# dotnet 获取整个局域网的 ip 地址

局域网可以使用的 IP 地址有很多,我写了一段代码用来枚举所有可以用的 ip 地址

03-05 2020

C# 极限压缩 dotnet core 控制台发布文件

每次发布 dotnet core 应用都会觉得发布文件太大,而如果使用极限压缩,用 CoreRT 能让发布的控制台文件到 5KB 左右,不需要带框架就能在其他设备运行

03-05 2020

C# 获取 PC 序列号

在 C++ 需要使用 GetSystemFirmwareTable 的方法来获得 PC 的序列号,需要写的代码很多,但是在 C# 可以使用 WMI 来拿到序列号

01-10 2020

.NET 将多个程序集合并成单一程序集的 4+3 种方法

编写 .NET 程序的时候,我们经常会在项目的输出目录下发现一大堆的文件。除了我们项目自己生成的程序集之外,还能找到这个项目所依赖的一大堆依赖程序集。有没有什么方法可以把这些依赖和我们的程序集合并到一起呢?

本文介绍四种将程序集和依赖打包合并到一起的方法,每一种方法都有其不同的原理和优缺点。我将介绍这些方法的原理并帮助你决定哪种方法最适合你想要使用的场景。


四种方法

目前我已知的将 .NET 程序集与依赖合并到一起的方法有下面四种:

  1. 使用 .NET Core 3.0 自带的 PublishSingleFile 属性合并依赖
  2. 使用 Fody
  3. 使用 SourceYard 源代码包
  4. 使用 ILMerge(微软所写)或者 ILRepack(基于 Mono.Ceil)
  5. 其他方法

如果你还知道有其他的方法,欢迎评论指出,非常感谢!

上面的第五种方法我也会做一些介绍,要么是因为无法真正完成任务或者适用场景非常有限,要么是其原理我还不理解,因此只进行简单介绍。

使用 .NET Core 3.0 自带的 PublishSingleFile 属性合并依赖

.NET Core 3.0 自 Preview 5 开始,增加了发布成单一 exe 文件的功能。

在你的项目文件中增加下面的两行可以开启此功能:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
++      <PublishSingleFile>true</PublishSingleFile>
      </PropertyGroup>
    
    </Project>

第一行 RuntimeIdentifier 一定需要指定,因为发布的单一文件是特定于架构的。这里,我们指定了 win10-x64,你也可以指定为其他的值。可以使用的值你可以在这篇文章中查询到:

第二行 PublishSingleFile 即开启发布时单一文件的功能。这样,你在发布你的程序的时候可以得到一个单一的可执行程序。发布一个 .NET Core 项目的方法是在命令行中输入:

dotnet publish

当然,如果你没有更改任何你的项目文件(没有增加上面的那两行),那么你在使用发布命令的时候就需要把这两个属性再增加上。因此完整的发布命令是下面这样的:

dotnet publish -r win10-x64 /p:PublishSingleFile=true

这里的 -r 就等同于在项目中指定 RuntimeIdentifier 持续。这里的 /p 是在项目中增加一个属性,而增加的属性名是 PublishSingleFile,增加的属性值是 true

使用 .NET Core 3.0 这种自带的发布单一 exe 的方法会将你的程序的全部文件(包括所有依赖文件,包括非托管程序集,包括各种资源文件)全部打包到一个 exe 中。当运行这个 exe 的时候,会首先将所有这些文件生成到本地计算机中一个临时目录下。只有第一次运行这个 exe 的时候才会生成这个目录和其中的文件,之后的运行是不会再次生成的。

下面说一些 .NET Core 3.0 发布程序集的一点扩展——.NET Core 3.0 中对于发布程序集的三种处理方式可以放在一起使用:

  • 裁剪程序集(Assembly Trimmer)
  • 提前编译(Ahead-of-Time compilation,通过 crossgen)后面马上会说到 Microsoft.DotNet.ILCompiler
  • 单一文件打包(Single File Bundling)本小节

关于 .NET Core 3.0 中发布仅一个 exe 的方法、原理和实践,可以参见林德熙的博客:

.NET Core 在 GitHub 上开源:

使用 Fody

在你的项目中安装一个 NuGet 包 Costura.Fody。一般来说,安装完之后,你编译的时候就会生成仅有一个 exe 的程序集了。

如果你继续留意,可以发现项目中多了一个 Fody 的专属配置文件 FodyWeavers.xml,内容如下:

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
    <Costura/>
</Weavers>

仅仅到此为止你已经足够利用 Fody 完成程序集的合并了。

但是,如果希望对 Fody 进行更精细化的配置,可以阅读叶洪的博客:

Fody 在 GitHub 上开源:

使用 SourceYard 源代码包

SourceYard 源代码包在程序集合并上是另辟蹊径的一种合并方式。它不能帮助你将所有的依赖全部合并,但足以让你在发布一些简单应用的时候不至于引入大量的依赖。

例如,你可以考虑新建一个项目,然后安装下面的 NuGet 包:

安装完成之后,你就可以在你的项目中使用到此 NuGet 包为你带来的获取 MAC 地址的工具类了。

using System;
using lindexi.src;

namespace Walterlv.Demo
{
    internal static class Program
    {
        static void Main()
        {
            var macList = MacAddress.GetActiveMacAddress();
            foreach (var mac in macList)
            {
                Console.WriteLine(mac);
            }
        }
    }
}

编译完你的项目,你会发现你的项目没有携带任何依赖。你安装的 NuGet 包并没有成为你的依赖,反而成为你正在编译的程序集的一部分。

如果你要制作一个像上面那样的源代码包,只需要在你要制作 NuGet 包的项目安装上 dotnetCampus.SourceYard,在你打包成 NuGet 包的时候,就会生成一个普通的 NuGet 包以及一个 *.Source.nupkg 的源代码包。将源代码包上传到 nuget.org 上,其他人便可以安装你制作的源代码包了。

关于如何使用 SourceYard 制作一个源代码包的方法可以阅读林德熙的博客:

关于能够做出源代码包的原理,可以阅读我的博客:

SourceYard 在 GitHub 上开源:

使用 ILMerge 或者 ILRepack 等工具

ILMerge 和 ILRepack 的合并就更加富有技术含量——当然坑也更多。

这两个都是工具,因此,你需要将工具下载下来使用。你有很多种方法下载到工具使用,因此我会推荐不同的人群使用不同的工具。

ILMerge

ILMerge 命令行工具是微软官方出品,下载地址:

其使用方法请参见我的博客:

ILRepack

ILRepack 基于 Mono.Ceil 来进行 IL 合并,其使用方法可以参见我的博客:

ILMerge-GUI 工具(已过时,但适合新手随便玩玩)

你可以在以下网址中找到 ILMerge-GUI 的下载链接:

ILMerge-GUI 工具在 Bitbucket 上开源:

其他方法

使用 Microsoft.DotNet.ILCompiler

可以将 .NET Core 编译为单个无依赖的 Native 程序。

你需要先安装一个预览版的 NuGet 包 Microsoft.DotNet.ILCompiler

关于 Microsoft.DotNet.ILCompiler 的使用,你可以阅读林德熙的博客:

使用 dnSpy

dnSpy 支持添加一个模块到程序集,也可以创建模块,还可以将程序集转换为模块。因此,一个程序集可以包含多个模块的功能就可以被充分利用起来。

添加模块到程序集

使用 Warp

Warp 在 GitHub 上开源:

其使用可以参见林德熙的博客:

各种方法的原理和使用场景比较

原理

使用 .NET Core 3.0 自带的 PublishSingleFile 属性合并依赖,其原理是生成一个启动器容器程序。最终没有对程序进行任何修改,只是单纯的打包而已。

使用 Fody,是将程序集依赖放到了资源里面。当要加载程序集的时候,会直接将资源中的程序集流加载到内存中。

使用 SourceYard 源代码包,是直接将源代码合并到了目标项目里面。

使用 ILMerge / ILRepack,是在 IL 级别对程序集进行了合并。

我们可以通过下面一张图来感受一下后三种原理上的不同。

这是一个分别通过 Fody、SourceYard 和 ILMerge / ILRepack 生成的程序集的反编译图。可以看到,对于 ILRepack / ILMerge 和 SourceYard,反编译后看到的源代码都在目标程序集中,而对于 Fody,依赖仅仅出现在资源中。

原理差别

适用范围

由于其原理不同,所以其适用范围和造成的副作用也不同。

如果你基于 .NET Core 3.0 开发,并且也不在意在目标计算机上生成的临时文件夹,那么可以考虑使用 PublishSingleFile 属性合并依赖。

如果你不在乎启动性能以及内存消耗,那么可以考虑 Fody(这意味着小型程序比较适合采用)。

如果你的程序非常在乎启动性能,那么就需要考虑 SourceYard、ILMerge / ILRepack 了。

对于 ILMerge / ILRepack 和 SourceYard 的比较,可以看下面这张表格:

方案 ILRepack / ILMerge SourceYard
适用于 任意 .NET 程序集 通过 SourceYard 发布的 NuGet 包
WPF ILRepack 支持,ILMerge 不支持 支持
调试(支持) 仅支持一般方法的调试 支持一般程序集支持的所有调试方法
调试(不支持) 不支持异步方法调试,不支持显示局部变量 没有不支持的
隐藏 API internal 的类型和成员可以隐藏 必须是 private 类型和成员才可隐藏

可以发现,如果我们能够充分将我们需要的包通过 SourceYard 发布成 NuGet,那么我们将可以获得比 ILRepack / ILMerge 更好的编写和调试体验。

表格之外还有一些特别需要说明的:

  1. ILRepack 额外支持修改 WPF 编译生成的 Baml 文件,将资源的引用路径修改成新程序集的路径。
  2. SourceYard 的类型需要写成 private 才可以隐藏,但是只有内部类才可以写 private,因此如果特别需要隐藏,请首先写一个内部类。(因此,你可能会发现有一个类型有很多个分部类,每一个分部类中都是一个私有的内部类)

开源社区

最后说一下,以上所说的所有方法全部是开源的,有问题欢迎在社区讨论一起解决:

2019
12-27 2019

如何在 MSBuild 中正确使用 % 来引用每一个项(Item)中的元数据

MSBuild 中写在 <ItemGroup /> 中的每一项是一个 ItemItem 除了可以使用 Include/Update/Remove 来增删之外,还可以定义其他的元数据(Metadata)。

使用 % 可以引用 Item 的元数据,本文将介绍如何正确使用 % 来引用每一个项中的元数据。


定义 Item 的元数据

就像下面这样,当引用一个 NuGet 包时,可以额外使用 Version 来指定应该使用哪个特定版本的 NuGet 包。这里的 VersionPrivateAssets 就是 PackageReference 的元数据。

<ItemGroup>
    <PackageReference Include="dotnetCampus.Configurations.Source" Version="1.0.0" PrivateAssets="All" />
    <PackageReference Include="dotnetCampus.CommandLine.Source" Version="1.2.1" PrivateAssets="All" />
    <PackageReference Include="Walterlv.Win32.Source" Version="0.12.2-alpha" PrivateAssets="All" />
    <PackageReference Include="Walterlv.IO.PackageManagement.Source" Version="0.13.2-alpha" PrivateAssets="All" />
</ItemGroup>

我们随便创建一个新的 Item,也可以定义自己的元数据。

<ItemGroup>
    <_WalterlvItem Include="欢迎访问" Url="https://" />
    <_WalterlvItem Include="吕毅的博客" Url="blog.walterlv.com" />
</ItemGroup>

引用元数据

引用元数据使用的是 % 符号。

<Target Name="_WalterlvDemo" AfterTargets="AfterBuild">
    <ItemGroup>
        <_WalterlvItem Include="欢迎访问" Url="https://" />
        <_WalterlvItem Include="吕毅的博客" Url="blog.walterlv.com" />
    </ItemGroup>
    <Message Text="@(_WalterlvItem):%(Url)" />
</Target>

虽然这里我们只写了一个 Message Task,但是最终我们会输出两次,每一个 _WalterlvItem 项都会输出一次。下面是这段代码的输出:

_WalterlvDemo:
  欢迎访问:https://
  吕毅的博客:blog.walterlv.com

当你使用 % 的时候,会为每一个项执行一次这行代码。当然,如果某个 Task 支持传入集合,那么则可以直接收到集合。

如果你不是用的 Message,而是定义一个其他的属性,使用 @(_WalterlvItem):%(Url) 作为属性的值,那么这个属性也会为每一个项都计算一次值。当然最终这个属性的值就是最后一项计算所得的值。

也许可以帮你回忆一下,如果我们不写 %(Url) 会输出什么。当只输出 @(WalterlvItem) 的时候,会以普通的分号分隔的文字。

<Target Name="_WalterlvDemo" AfterTargets="AfterBuild">
    <ItemGroup>
        <_WalterlvItem Include="欢迎访问" Url="https://" />
        <_WalterlvItem Include="吕毅的博客" Url="blog.walterlv.com" />
    </ItemGroup>
    <Message Text="@(_WalterlvItem)" />
</Target>

会输出:

_WalterlvDemo:
  欢迎访问;吕毅的博客

使用元数据

如果你希望自己处理编译过程,那么可能会对元数据做更多的处理。

为了简单说明 % 的用法,我将已收集到的所有的元数据和它的本体一起输出到一个文件中。这样,后续的编译过程可以直接使用这个文件来获得所有的项和你希望关心它的所有元数据。

<PropertyGroup>
    <_WalterlvContentArgsFilePath>$(IntermediateOutputPath)Args\Content.txt</_WalterlvContentArgsFilePath>
    <_WalterlvToolFile>$(MSBuildThisFileDirectory)..\bin\compile.exe</_WalterlvContentArgsFilePath>
</PropertyGroup>

<Target Name="_WalterlvDemo" AfterTargets="AfterBuild">
    <ItemGroup>
        <_WalterlvContentFileLine Include="@(Content)" Line="@(Content)|%(Content.PublishState)|%(Content.CopyToOutputDirectory)" />
    </ItemGroup>
    <WriteLinesToFile File="$(_WalterlvContentArgsFilePath)" Lines="%(_WalterlvContentFileLine.Line)" Overwrite="True" />
    <Exec ConsoleToMSBuild="True"
          Command="&quot;$(_WalterlvToolFile)&quot; PackContent --content-file &quot; $(_WalterlvContentArgsFilePath) &quot;" />
</Target>

这段代码的含义是:

  1. 定义一个文件路径,这个路径即将用来存放所有 Content 项和它的元数据;
  2. 定义一个工具路径,我们即将运行这个路径下的命令行程序来执行自定义的编译;
  3. 收集所有的 Content 项,然后把所有项中的 PublishStateCopyToOutputDirectory 一起拼接成这个样子:
    • Content|PublishState|CopyToOutputDirectory
  4. 写文件,将以上拼接出来的每一项写入到文件中的每一行;
  5. 执行工具程序,这个程序将使用这个文件来执行自定义的编译。

关于使用 exe 进行自定义编译的部分可以参考我的另一篇博客:

关于写文件的部分可以参考我的另一篇博客:

关于项元数据的其他信息

一些已知的元数据:


参考资料

12-27 2019

在 MSBuild 编译过程中操作文件和文件夹(检查存在/创建文件夹/读写文件/移动文件/复制文件/删除文件夹)

本文整理 MSBuild 在编译过程中对文件和文件夹处理的各种自带的编译任务(Task)。


Exists 检查文件存在

使用 Exists 可以判断一个文件或者文件夹是否存在。注意无论是文件还是文件夹,只要给定的路径存在就返回 true。可以作为 MSBuild 属性、项和编译任务的执行条件。

<PropertyGroup Condition=" Exists( '$(MSBuildThisFileDirectory)..\build\build.xml' ) ">
    <_WalterlvPackingDirectory>$(MSBuildThisFileDirectory)..\bin\$(Configuration)\</_WalterlvPackingDirectory>
</PropertyGroup>

MakeDir 创建文件夹

下面的例子演示创建一个文件夹:

<Target Name="_WalterlvCreateDirectoryForPacking">
    <MakeDir Directories="$(MSBuildThisFileDirectory)..\bin\$(Configuration)\" />
</Target>

下面是使用到 MakeDir 全部属性的例子,将已经成功创建的文件夹提取出来。

<Target Name="_WalterlvCreateDirectoryForPacking">
    <MakeDir Directories="$(MSBuildThisFileDirectory)..\bin\$(Configuration)\">
        <Output TaskParameter="DirectoriesCreated" PropertyName="CreatedPackingDirectory" />
    </MakeDir>
</Target>

Move 移动文件

下面的例子是将输出文件移动到一个专门的目录中,移动后,所有的文件将平级地在输出文件夹中(即所有的子文件夹中的文件也都被移动到同一层目录中了)。

<PropertyGroup>
    <_WalterlvPackingDirectory>$(MSBuildThisFileDirectory)..\bin\$(Configuration)\</_WalterlvPackingDirectory>
</PropertyGroup>

<Target Name="_WalterlvMoveFilesForPacking">
    <ItemGroup>
        <_WalterlvToMoveFile Include="$(OutputPath)**" />
    </ItemGroup>
    <Move SourceFiles="@(_WalterlvToMoveFile)"
          DestinationFolder="$(_WalterlvPackingDirectory)"
          SkipUnchangedFiles="True" />
</Target>

你可以通过下面的例子了解到 Move 的其他大多数属性及其用法:

<PropertyGroup>
    <_WalterlvPackingDirectory>$(MSBuildThisFileDirectory)..\bin\$(Configuration)\</_WalterlvPackingDirectory>
</PropertyGroup>

<Target Name="_WalterlvMoveFilesForPacking">
    <ItemGroup>
        <_WalterlvToMoveFile Include="$(OutputPath)**" />
        <_WalterlvTargetFile Include="$(_WalterlvPackingDirectory)\%(_WalterlvToMoveFile.RecursiveDir)" />
    </ItemGroup>
    <Move SourceFiles="@(_WalterlvToMoveFile)"
          DestinationFiles="$(_WalterlvTargetFile)"
          OverwriteReadOnlyFiles="True">
        <Output TaskParameter="MovedFiles" PropertyName="MovedOutputFiles" />
    </Copy>
</Target>

这段代码除了没有使用 DestinationFolder 之外,使用到了所有 Move 能用的属性:

  • 将所有的 _WalterlvToCopyFile 一对一地复制到 _WalterlvTargetFile 指定的路径上。
  • 即便目标文件是只读的,也会覆盖。

Copy 复制文件

下面的例子是将输出文件拷贝到一个专门的目录中,保留原来所有文件之间的目录结构,并且如果文件没有改变则跳过。

<PropertyGroup>
    <_WalterlvPackingDirectory>$(MSBuildThisFileDirectory)..\bin\$(Configuration)\</_WalterlvPackingDirectory>
</PropertyGroup>

<Target Name="_WalterlvCopyFilesForPacking">
    <ItemGroup>
        <_WalterlvToCopyFile Include="$(OutputPath)**" />
    </ItemGroup>
    <Copy SourceFiles="@(_WalterlvToCopyFile)"
          DestinationFolder="$(_WalterlvPackingDirectory)\%(RecursiveDir)"
          SkipUnchangedFiles="True" />
</Target>

如果你希望复制后所有的文件都在同一级文件夹中,不再有子文件夹,那么去掉 \%(RecursiveDir)

你可以通过下面的例子了解到 Copy 的其他大多数属性及其用法:

<PropertyGroup>
    <_WalterlvPackingDirectory>$(MSBuildThisFileDirectory)..\bin\$(Configuration)\</_WalterlvPackingDirectory>
</PropertyGroup>

<Target Name="_WalterlvCopyFilesForPacking">
    <ItemGroup>
        <_WalterlvToCopyFile Include="$(OutputPath)**" />
        <_WalterlvTargetFile Include="$(_WalterlvPackingDirectory)\%(_WalterlvToCopyFile.RecursiveDir)" />
    </ItemGroup>
    <Copy SourceFiles="@(_WalterlvToCopyFile)"
          DestinationFiles="@(_WalterlvTargetFile)"
          OverwriteReadOnlyFiles="True"
          Retries="10"
          RetryDelayMilliseconds="10"
          SkipUnchangedFiles="True"
          UseHardlinksIfPossible="True">
        <Output TaskParameter="CopiedFiles" PropertyName="CopiedOutputFiles" />
    </Copy>
</Target>

这段代码除了没有使用 DestinationFolder 之外,使用到了所有 Copy 能用的属性:

  • 将所有的 _WalterlvToCopyFile 一对一地复制到 _WalterlvTargetFile 指定的路径上。
  • 即便目标文件是只读的,也会覆盖。
  • 如果复制失败,则重试 10 次,每次等待 10 毫秒
  • 如果文件没有改变,则跳过复制
  • 如果目标文件系统支持硬连接,则使用硬连接来提升性能

Delete 删除文件

下面这个例子是删除输出目录下的所有的 pdb 文件(适合 release 下发布软件)。

<Target Name="_WalterlvDeleteFiles">
    <Delete Files="$(OutputPath)*.pdb" />
</Target>

也可以把此操作已经删除的文件列表拿出来。使用全部属性的 Delete 的例子:


<Target Name="_WalterlvDeleteFiles">
    <Delete Files="$(OutputPath)*.pdb" TreatErrorsAsWarnings="True">
        <Output TaskParameter="DeletedFiles" PropertyName="DeletedPdbFiles" />
    </Delete>
</Target>

ReadLinesFromFile 读取文件

在编译期间,可以从文件中读出文件的每一行:

<PropertyGroup>
    <_WalterlvToWriteFile>$(OutputPath)walterlv.md</_WalterlvToWriteFile>
</PropertyGroup>

<Target Name="_WalterlvReadFilesToLines">
    <ReadLinesFromFile File="$(_WalterlvToWriteFile)">
        <Output TaskParameter="Lines" PropertyName="TheLinesThatRead" />
    </ReadLinesFromFile>
</Target>

WriteLinesToFile 写入文件

可以在编译期间,将一些信息写到文件中以便后续编译的时候使用,甚至将代码写到文件中以便动态生成代码。

<PropertyGroup>
    <_WalterlvBlogSite>https://blog.walterlv.com</_WalterlvBlogSite>
    <_WalterlvToWriteFile>$(OutputPath)walterlv.md</_WalterlvToWriteFile>
</PropertyGroup>

<ItemGroup>
    <_WalterlvToWriteLine Include="This is the first line" />
    <_WalterlvToWriteLine Include="This is the second line" />
    <_WalterlvToWriteLine Include="My blog site is: $(_WalterlvBlogSite)" />
</ItemGroup>

<Target Name="_WalterlvWriteFilesForPacking">
    <WriteLinesToFile File="$(_WalterlvToWriteFile)"
                      Lines="@(_WalterlvToWriteLine)" />
</Target>

▲ 注意,默认写入文件是不会覆盖的,会将内容补充到原来文件的后面。

<Target Name="_WalterlvWriteFilesForPacking">
    <WriteLinesToFile File="$(_WalterlvToWriteFile)"
                      Lines="@(_WalterlvToWriteLine)"
                      Overwrite="True"
                      Encoding="Unicode"
                      WriteOnlyWhenDifferent="True" />
</Target>

RemoveDir 删除文件夹

在编写编译命令的时候,可能会涉及到清理资源。或者为了避免无关文件的影响,在编译之前删除我们的工作目录。

<Target Name="_WalterlvRemoveDirectoryForPacking">
    <RemoveDir Directories="$(MSBuildThisFileDirectory)..\bin\$(Configuration)\" />
</Target>

下面是使用到 MakeDir 全部属性的例子,将已经成功创建的文件夹提取出来。

<Target Name="_WalterlvRemoveDirectoryForPacking">
    <RemoveDir Directories="$(MSBuildThisFileDirectory)..\bin\$(Configuration)\">
        <Output TaskParameter="RemovedDirectories" PropertyName="RemovedPackingDirectory" />
    </RemoveDir>
</Target>

12-26 2019

如何在 .NET/C# 代码中安全地结束掉一个控制台应用程序?通过发送 Ctrl+C 信号来结束

我的电脑上每天会跑一大堆控制台程序,于是管理这些程序的运行就成了一个问题。或者说你可能也在考虑启动一个控制台程序来完成某些特定的任务。

如果我们需要结束掉这个控制台程序怎么做呢?直接杀进程吗?这样很容易出问题。我正在使用的一个控制台程序会写文件,如果直接杀进程可能导致数据没能写入到文件。所以本文介绍如何使用 .NET/C# 代码向控制台程序发送 Ctrl+C 来安全地结束掉程序。


用 Ctrl+C 结束控制台程序

如果直接用 Process.Kill 杀掉进程,进程可能来不及保存数据。所以无论是窗口程序还是控制台程序,最好都让控制台程序自己去关闭。

Process.Kill 结束控制台程序

▲ 使用 Process.Kill 结束程序,程序退出代码是 -1

Ctrl+C 结束控制台程序

▲ 使用 Ctrl+C 结束程序,程序退出代码是 0

Ctrl+C 信号

Windows API 提供了方法可以将当前进程与目标控制台进程关联起来,这样我们便可以向自己发送 Ctrl+C 信号来结束掉关联的另一个控制台进程。

关联和取消关联的方法是下面这两个,AttachConsoleFreeConsole

[DllImport("kernel32.dll")]
private static extern bool AttachConsole(uint dwProcessId);

[DllImport("kernel32.dll")]
private static extern bool FreeConsole();

不过,当发送 Ctrl+C 信号的时候,不止我们希望关闭的控制台程序退出了,我们自己程序也是会退出的(即便我们自己是一个 GUI 程序)。所以我们必须先组织自己响应 Ctrl+C 信号。

需要用到另外一个 API:

[DllImport("kernel32.dll")]
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? HandlerRoutine, bool Add);

enum CtrlTypes : uint
{
    CTRL_C_EVENT = 0,
    CTRL_BREAK_EVENT,
    CTRL_CLOSE_EVENT,
    CTRL_LOGOFF_EVENT = 5,
    CTRL_SHUTDOWN_EVENT
}

private delegate bool ConsoleCtrlDelegate(CtrlTypes CtrlType);

不过,因为我们实际上并不需要真的对 Ctrl+C 进行响应,只是单纯临时禁用以下,所以我们归这个委托传入 null 就好了。

最后,也是最关键的,就是发送 Ctrl+C 信号了:

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);

下面,我将完整的代码贴出来。

全部源代码

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Walterlv.Fracture.Utils
{
    /// <summary>
    /// 提供与控制台程序的交互。
    /// </summary>
    public class ConsoleInterop
    {
        /// <summary>
        /// 关闭控制台程序。
        /// </summary>
        /// <param name="process">要关闭的控制台程序的进程实例。</param>
        /// <param name="timeoutInMilliseconds">如果不希望一直等待进程自己退出,则可以在此参数中设置超时。你可以在超时未推出候采取强制杀掉进程的策略。</param>
        /// <returns>如果进程成功退出,则返回 true;否则返回 false。</returns>
        public static bool StopConsoleProgram(Process process, int? timeoutInMilliseconds = null)
        {
            if (process is null)
            {
                throw new ArgumentNullException(nameof(process));
            }

            if (process.HasExited)
            {
                return true;
            }

            // 尝试将我们自己的进程附加到指定进程的控制台(如果有的话)。
            if (AttachConsole((uint)process.Id))
            {
                // 我们自己的进程需要忽略掉 Ctrl+C 信号,否则自己也会退出。
                SetConsoleCtrlHandler(null, true);

                // 将 Ctrl+C 信号发送到前面已关联(附加)的控制台进程中。
                GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0);

                // 拾前面已经附加的控制台。
                FreeConsole();

                bool hasExited;
                // 由于 Ctrl+C 信号只是通知程序关闭,并不一定真的关闭。所以我们等待一定时间,如果仍未关闭,则超时不处理。
                // 业务可以通过判断返回值来角是否进行后续处理(例如强制杀掉)。
                if (timeoutInMilliseconds == null)
                {
                    // 如果没有超时处理,则一直等待,直到最终进程停止。
                    process.WaitForExit();
                    hasExited = true;
                }
                else
                {
                    // 如果有超时处理,则超时候返回。
                    hasExited = process.WaitForExit(timeoutInMilliseconds.Value);
                }

                // 重新恢复我们自己的进程对 Ctrl+C 信号的响应。
                SetConsoleCtrlHandler(null, false);

                return hasExited;
            }
            else
            {
                return false;
            }
        }

        [DllImport("kernel32.dll")]
        private static extern bool AttachConsole(uint dwProcessId);

        [DllImport("kernel32.dll")]
        private static extern bool FreeConsole();

        [DllImport("kernel32.dll")]
        private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? HandlerRoutine, bool Add);

        [DllImport("kernel32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);

        enum CtrlTypes : uint
        {
            CTRL_C_EVENT = 0,
            CTRL_BREAK_EVENT,
            CTRL_CLOSE_EVENT,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT
        }

        private delegate bool ConsoleCtrlDelegate(CtrlTypes CtrlType);
    }
}

如何使用

现在,我们可以通过调用 ConsoleInterop.StopConsoleProgram(process) 来安全地结束掉一个控制台程序。

当然,为了处理一些意外的情况,我把超时也加上了。下面的用法演示超时 2 秒候程序还没有退出,则强杀。

if (!ConsoleInterop.StopConsoleProgram(process, 2000))
{
    try
    {
        process.Kill();
    }
    catch (InvalidOperationException e)
    {
    }
}

Ctrl+C 结束控制台程序


参考资料

12-23 2019

如何将一个 .NET 对象序列化为 HTTP GET 的请求字符串

HTTP GET 请求时携带的参数直接在 URL 中,形式如 ?key1=value&key2=value&key3=value。如果是 POST 请求时,我们可以使用一些库序列化为 json 格式作为 BODY 发送,那么 GET 请求呢?有可以直接将其序列化为 HTTP GET 请求的 query 字符串的吗?


HTTP GET 请求

一个典型的 HTTP GET 请求带参数的话大概是这样的:

https://s.blog.walterlv.com/api/example?key1=value&key2=value&key3=value

于是我们将一个类型序列化为后面的参数:

[DataContract]
public class Foo
{
    [DataMember(Name = "key1")]
    public string? Key1 { get; set; }

    [DataMember(Name = "key2")]
    public string? Key2 { get; set; }

    [DataMember(Name = "key3")]
    public string? Key3 { get; set; }
}

库?

可能是这个需求太简单了,所以并没有找到单独的库。所以我就写了一个源代码包放到了 nuget.org 上。

在这里下载源代码包:

你不需要担心引入额外的依赖,因为这是一个源代码包。关于源代码包不引入额外依赖 dll 的原理,可以参见:

方法

我们需要做的是,将一个对象序列化为 query 字符串。假设这个对象的局部变量名称是 query,于是我们需要:

  1. 取得此对象所有可获取值的属性
    • query.GetType().GetProperties()
  2. 获取此属性值的方法
    • property.GetValue(query, null)
  3. 将属性和值拼接起来
    • string.Join("&", properties)

然而真实场景可能比这个稍微复杂一点:

  1. 我们需要像 Newtonsoft.Json 一样,对于标记了 DataContract 的类,按照 DataMember 来序列化
  2. URL 中的值需要进行转义

所以,我写出了下面的方法:

var isContractedType = query.GetType().IsDefined(typeof(DataContractAttribute));
var properties = from property in query.GetType().GetProperties()
                    where property.CanRead && (isContractedType ? property.IsDefined(typeof(DataMemberAttribute)) : true)
                    let memberName = isContractedType ? property.GetCustomAttribute<DataMemberAttribute>().Name : property.Name
                    let value = property.GetValue(query, null)
                    where value != null && !string.IsNullOrWhiteSpace(value.ToString())
                    select memberName + "=" + HttpUtility.UrlEncode(value.ToString());
var queryString = string.Join("&", properties);
return string.IsNullOrWhiteSpace(queryString) ? "" : prefix + queryString;

完整的代码如下:

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Web;

namespace Walterlv.Web.Core
{
    internal class QueryString
    {
        [return: NotNullIfNotNull("query")]
        public static string? Serialize(object? query, string? prefix = "?")
        {
            if (query is null)
            {
                return null;
            }

            var isContractedType = query.GetType().IsDefined(typeof(DataContractAttribute));
            var properties = from property in query.GetType().GetProperties()
                             where property.CanRead && (isContractedType ? property.IsDefined(typeof(DataMemberAttribute)) : true)
                             let memberName = isContractedType ? property.GetCustomAttribute<DataMemberAttribute>().Name : property.Name
                             let value = property.GetValue(query, null)
                             where value != null && !string.IsNullOrWhiteSpace(value.ToString())
                             select memberName + "=" + HttpUtility.UrlEncode(value.ToString());
            var queryString = string.Join("&", properties);
            return string.IsNullOrWhiteSpace(queryString) ? "" : prefix + queryString;
        }
    }
}

你可能会遇到 [return: NotNullIfNotNull("query")] 这一行编译不通过的情况,这个是 C# 8.0 带的可空引用类型所需要的契约类。

你可以将它删除,或者安装我的另一个 NuGet 包来获得更多可空引用类型契约的支持,详见:

12-23 2019

屏幕边缘上有趣的 1 个像素,看不见、摸不到

如果你的屏幕分辨率是 1920×1080,那么一个全屏的窗口程序尺寸是多少呢?想都不用想,是 1920×1080。

那么输入设备输入的坐标是多少呢?是 X∈[0, 1919] ?还是 X∈[1, 1920] ?还是 X∈[0, 1920]


鼠标输入与触摸输入

一个有趣的问题,因为 1920×1080 分辨率的屏幕,其横向只有 1920 个像素,也就是说如果需要区分一个像素,那么只需要 1920 个数值就够了。这意味着 X∈[0, 1919] 或者 X∈[1, 1920] 的取值范围就能表示横向的所有像素了。

那么实际上最左侧的点的输入数值是多少呢?最右侧的点的输入数值是多少呢?

我写了一个最大化全屏的程序专门用来测试鼠标和触摸输入的数值是多少。

鼠标输入

▲ 在鼠标输入的情况下,最右侧其实是 1919(我的屏幕是 2560×1080,所以最右侧是 2559)

测量的时候,鼠标是直接往右移动到底,移到不能动为止。

那么在触摸输入的时候又如何?

触摸输入

▲ 在触摸输入的情况下,最右侧是 1920(我的屏幕是 2560×1080,所以最右侧是 2560)

测量的时候,是让手指近乎在屏幕外触摸,不断触摸到能够在屏幕上看到的最小或最大值为止。

有趣的 1 像素

发现上面实验中有趣的现象了吗?明明只有 1920×1080 的屏幕分辨率,窗口明明只有 1920×1080 那么大,鼠标下收到正常范围内的输入坐标,而触摸下我们能收到超出我们窗口大小 1 像素的触摸事件!

问题并没有完——

如果说,触摸给了你超出窗口大小的坐标,那么你能如何使用这个坐标呢?虽然程序里收到什么坐标都无所谓(至少不崩),但如果你真拿它来渲染,就会在屏幕之外。

更有趣的是,虽然你能收到这个“在屏幕边缘之外”的坐标,但这个消息并不总会发送到你的程序里。更多的时候,你的程序根本就不会收到这个触摸事件,于是我们也就不能在程序里面更新窗口上显示的坐标到 1920 了,就像鼠标一样。

于是,你可能遇到的问题是:

  1. 如果你在屏幕的左侧边缘触摸,你的程序可以一直收到触摸事件,你能够得到正确的响应;
  2. 如果你在屏幕的右侧边缘触摸,你将仅能偶尔收到零星的刚好超出窗口大小的触摸坐标,大多数时候收不到触摸事件,于是你可能无法获知用户在屏幕右侧边缘进行触摸。

防踩坑秘籍

林德熙小伙伴告诉我说可以特意把窗口的尺寸做大一个像素。我试过了,确实能够让触摸在整个屏幕上生效,但对于双屏用户来说,就能在另外一个屏幕上看到“露馅儿”了的窗口,对于我这种强迫症患者来说,显然是不能接受的。

我的建议是,并不需要对这种情况进行什么特殊的处理。

12-17 2019

使用 MSBuild Target 复制文件的时候如何保持文件夹结构不变

使用 MSBuild 中的 Copy 这个编译目标可以在 .NET 项目编译期间复制一些文件。不过使用默认的参数复制的时候文件夹结构会丢失,所有的文件会保留在同一级文件夹下。

那么如何在复制文件的时候保持文件夹结构与原文件夹结构一样呢?


Copy

下面是一个典型的使用 MSBuild 在编译期间复制文件的一个编译目标。

<Target Name="_WalterlvCopyDemo" AfterTargets="AfterBuild">
  <ItemGroup>
    <_WalterlvToCopyFile Include="$(OutputPath)**" />
  </ItemGroup>
  <Copy SourceFiles="@(_WalterlvToCopyFile)" DestinationFolder="bin\Debug\Test" SkipUnchangedFiles="True" />
</Target>

这样复制的文件是不会保留文件夹结构的。

在同一层级

复制之后,所有的文件夹将不存在,所有文件覆盖地到同一层级。

RecursiveDir

如果希望保留文件夹层级,可以在 DestinationFolder 中使用文件路径来替代文件夹路径。

  <Target Name="_WalterlvCopyDemo" AfterTargets="AfterBuild">
    <ItemGroup>
      <_WalterlvToCopyFile Include="$(OutputPath)**" />
    </ItemGroup>
-   <Copy SourceFiles="@(_WalterlvToCopyFile)" DestinationFolder="bin\Debug\Test" SkipUnchangedFiles="True" />
+   <Copy SourceFiles="@(_WalterlvToCopyFile)" DestinationFolder="bin\Debug\Test\%(RecursiveDir)" SkipUnchangedFiles="True" />
  </Target>

保留了文件夹层次结构

12-09 2019

使用正则表达式尽可能准确匹配域名/网址

你可能需要准确地知道一段字符串是否是域名/网址/URL。虽然可以使用 ./ 这些来模糊匹配,但会造成误判。

实际上单纯使用正则表达式来精确匹配也是非常复杂的,通过代码来判断会简单很多。不过本文依然从域名的定义出发来尽可能匹配一段字符串是否是域名或者网址,在要求不怎么高的场合,使用本文的正则表达式写的代码会比较简单。


网址

网址实际上是 URL(统一资源定位符),它是由协议、主机名和路径组成。不过我们通常所说的网址中的主机名通常是域名,因此我们在匹配的时候主要考虑域名。

域名

维基百科 中关于域名的描述:

  1. 域名由一或多个部分组成,这些部分通常连接在一起,并由点分隔。最右边的一个标签是顶级域名,例如zh.wikipedia.org的顶级域名是org。一个域名的层次结构,从右侧到左侧隔一个点依次下降一层。每个标签可以包含1到63个八字节。域名的结尾有时候还有一点,这是保留给根节点的,书写时通常省略,在查询时由软件内部补上。
  2. 域名里的英文字母不区分大小写。
  3. 完整域名的所有字符加起来不得超过253个ASCII字符的总长度。因此,当每一级都使用单个字符时,限制为127个级别:127个字符加上126个点的总长度为253。但实际上,某些域名可能具有其他限制;也没有只有一个字符的域名后缀。

后面关于非 ASCII 字符的描述我没有贴出来。这种域名例如“.中国”。

中国电信网站备案自助管理系统 中,我们可以找到关于域名的描述:

域名中的标号都由英文字母和数字组成,每一个标号不超过63个字符,也不区分大小写字母。标号中除连字符(-)外不能使用其他的标点符号。级别最低的域名写在最左边,而级别最高的域名写在最右边。由多个标号组成的完整域名总共不超过255个字符。

路径

路径是使用 / 分隔的一段一段字符串。

正则表达式匹配

在确认了完整的网址 URL 的规范之后,使用正则表达式来匹配就会比较精确了。

域名

现在,我们来尝试匹配一下域名

  1. 每个标签可组成的字符是 - a-z A-Z 0-9,但是 - 不可作为开头,标签总长度 1-63 个字符,于是
    • [a-zA-Z0-9][-a-zA-Z0-9]{0,62}
    • 即首字不含 -,后面的字可以包含 -
  2. 允许多个标签,于是
    • (\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+
    • 除了标签内容和前面一样,但我们加了个 .

别忘了,我们还有总长度限制,于是考虑加上零宽断言 ^.{3,255}$,匹配开头和结尾,中间任意字符但长度在 3-255 之间。通过零宽断言,我们可以在不捕获匹配字符串的情况下对后面的字符串增加限制条件。

现在,把整个正则表达式拼出来:

^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$

URL

对于不同的业务需求,可能有严格匹配或者宽松的匹配方式。

比如你要做一些比较精准的检查时需要进行严格的检查,那么选择严格匹配;这时,稍微出现一些不符合要求的字符都将认定为不是 URL。

如果你只是打算做一些简单的检查(例如只是语法高亮),那么简单匹配即可;因为当你使用 Chrome 浏览器访问这些 URL 的时候,依然可以正常访问,Chrome 会帮你格式化一下这个 URL。

URL(严格)

匹配 URL 跟匹配域名不一样,URL 复杂得多。严格匹配的要求是准确反应出 URL 的标准,但实际上如实反应标准编写的正则表达式会非常复杂,因此相比于 100% 准确匹配,我们还是从简了。

所以如果不是有特别要求,建议还是跳到后面的“宽松”部分来阅读吧!

我们以下面这个网址为例说明。

https://blog.walterlv.com/post/read-32bit-registry-from-x64-process.html

  1. 前面是可选的协议名,于是
    • (http(s)?:\/\/)
    • 然而既然可选,而且是行首,那么加一个 ? 和什么都不加的效果是一样的
  2. 随后是域名,于是
    • [a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+
    • 这里我们没有把总长度限制算上去
  3. 别忘了有个可选的端口号
    • (:[0-9]{1,5})?
    • 端口号的范围是 0-65535,但 0 是保留端口,49152 到 65535 也是保留端口,因此可以作为网址访问的范围也就是 1-49151,因此我们限制 1-5 位长度。
  4. 接下来是资源路径
    • 资源路径可以使用的字符也是有限制的,我们接下来详细说明。

组合整个正则表达式:

^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:[0-9]{1,5})?[-a-zA-Z0-9()@:%_\\\+\.~#?&//=]*$

顺便一提,不同于域名,我们这里去掉了长度限制,因为 URL 真的可以“很长”。另外,这里的

现在,我们补充说明一下资源路径可以使用的字符问题。

; / ? : @ & = + $ , 这些字符应该被转义。转义使用的字符是 &xxx;,因此在转义之后,依然还可能在网址中看到 &;,不过没有其他字符了。

- _ . ! ~ * ' ( ) 这些字符可以不进行转义,但也不建议在 URL 中使用。对于这部分,我们考虑将其匹配。

{ } | \ ^ [ ] ` 这部分字符可能被网关当作分隔符使用,因此不建议出现在 URL 中。对于这部分,我们考虑将其匹配。

< > # % " 控制字符。使用 % 可以组成其他 Unicode 字符,使用 # 用来指代网址中的某个部分。

因此,我们最终总结应该匹配的特殊字符有 @ : % _ \ + . ~ # ? & / =

URL(宽松)

宽松一点的话,正则表达式就好写多了。

这个正则表达式可以不写 https 协议前缀:

^\w+[^\s]+(\.[^\s]+){1,}$

如果上下文中要求必须匹配 https,则可以写:

^(http(s)?:\/\/)\w+[^\s]+(\.[^\s]+){1,}$
  • https://blog.walterlv.com/post/read-32bit-registry-from-x64-process.html#content)
    • 期望不匹配(主要是不能匹配末尾的括号),实际匹配
    • 在 URL 中,如果括号是成对的,则此 URL 允许以 ) 结尾,如果 URL 中括号不成对,则此 URL 不能以 ) 结尾;> 同理
  • https://blog.walterlv.com/post/read-32bit -registry-from-x64-process.html
    • 期望不匹配,实际不匹配
  • https://blog.lindexi.com/post/dotnet-配置-github-自动打包上传-nuget-文件.html
    • 期望匹配,实际匹配
  • https://域名.中国
    • 期望匹配,实际匹配
  • blog.walterlv.com/post/read-32bit-registry-from-x64-process.html
    • 期望匹配,实际匹配
  • x<blog.walterlv.com/post/read-32bit-registry-from-x64-process.html
    • 期望不匹配,实际匹配

这里的宽松正则表达式请小心!此正则表达式会将一段话中 URL 后面非空格的部分都算作 URL 的一部分。

更多大牛匹配 URL 的正则表达式

在 GitHub 上还有很多大牛们在写各种匹配 URL 的正则表达式:

最长的一个写了 1347 个字符,最短的有 38 个字符。

有人将其整理成一张表格,一图说明各种正则表达式能匹配到什么程度:


参考资料

12-08 2019

C# 8.0 的可空引用类型,不止是加个问号哦!你还有很多种不同的可空玩法

C# 8.0 引入了可空引用类型,你可以通过 ? 为字段、属性、方法参数、返回值等添加是否可为 null 的特性。

但是如果你真的在把你原有的旧项目迁移到可空类型的时候,你就会发现情况远比你想象当中复杂,因为你写的代码可能只在部分情况下可空,部分情况下不可空;或者传入空时才可为空,传入非空时则不可为空。


C# 8.0 可空特性

在开始迁移你的项目之前,你可能需要了解如何开启项目的可空类型支持:

可空引用类型是 C# 8.0 带来的新特性。

你可能会好奇,C# 语言的可空特性为什么在编译成类库之后,依然可以被引用它的程序集识别。也许你可以理解为有什么特性 Attribute 标记了字段、属性、方法参数、返回值的可空特性,于是可空特性就被编译到程序集中了。

确实,可空特性是通过 NullableAttributeNullableContextAttribute 这两个特性标记的。

但你是否好奇,即使在古老的 .NET Framework 4.5 或者 .NET Standard 2.0 中开发的时候,你也可以编译出支持可空信息的程序集出来。这些古老的框架中没有这些新出来的类型,为什么也可以携带类型的可空特性呢?

实际上反编译一下编译出来的程序集就能立刻看到结果了。

看下图,在早期版本的 .NET 框架中,可空特性实际上是被编译到程序集里面,作为 internalAttribute 类型了。

反编译

所以,放心使用可空类型吧!旧版本的框架也是可以用的。

更灵活控制的可空特性

阻碍你将老项目迁移到可空类型的原因,可能还有你原来代码逻辑的问题。因为有些情况下你无法完完全全将类型迁移到可空。

例如:

  1. 有些时候你不得不为非空的类型赋值为 null 或者获取可空类型时你能确保此时一定不为 null(待会儿我会解释到底是什么情况);
  2. 一个方法,可能这种情况下返回的是 null 那种情况下返回的是非 null
  3. 可能调用者传入 null 的时候才返回 null,传入非 null 的时候返回非 null

为了解决这些情况,C# 8.0 还同时引入了下面这些 Attribute

  • AllowNull: 标记一个不可空的输入实际上是可以传入 null 的。
  • DisallowNull: 标记一个可空的输入实际上不应该传入 null。
  • MaybeNull: 标记一个非空的返回值实际上可能会返回 null,返回值包括输出参数。
  • NotNull: 标记一个可空的返回值实际上是不可能为 null 的。
  • MaybeNullWhen: 当返回指定的 true/false 时某个输出参数才可能为 null,而返回相反的值时那个输出参数则不可为 null。
  • NotNullWhen: 当返回指定的 true/false 时,某个输出参数不可为 null,而返回相反的值时那个输出参数则可能为 null。
  • NotNullIfNotNull: 指定的参数传入 null 时才可能返回 null,指定的参数传入非 null 时就不可能返回 null。
  • DoesNotReturn: 指定一个方法是不可能返回的。
  • DoesNotReturnIf: 在方法的输入参数上指定一个条件,当这个参数传入了指定的 true/false 时方法不可能返回。

想必有了这些描述后,你在具体遇到问题的时候应该能知道选用那个特性。但单单看到这些特性的时候你可能不一定知道什么情况下会用得着,于是我可以为你举一些典型的例子。

输入:AllowNull

设想一下你需要写一个属性:

public string Text
{
    get => GetValue() ?? "";
    set => SetValue(value ?? "");
}

当你获取这个属性的值的时候,你一定不会获取到 null,因为我们在 get 里面指定了非 null 的默认值。然而我是允许你设置 null 到这个属性的,因为我处理好了 null 的情况。

于是,请为这个属性加上 AllowNull。这样,获取此属性的时候会得到非 null 的值,而设置的时候却可以设置成 null

++  [AllowNull]
    public string Text
    {
        get => GetValue() ?? "";
        set => SetValue(value ?? "");
    }

输入:DisallowNull

与以上场景相反的一个场景:

private string? _text;

public string? Text
{
    get => _text;
    set => _text = value ?? throw new ArgumentNullException(nameof(value), "不允许将这个值设置为 null");
}

当你获取这个属性的时候,这个属性可能还没有初始化,于是我们获取到 null。然而我却并不允许你将这个属性赋值为 null,因为这是个不合理的值。

于是,请为这个属性加上 DisallowNull。这样,获取此属性的时候会得到可能为 null 的值,而设置的时候却不允许为 null

输出:MaybeNull

如果你有尝试过迁移代码到可空类型,基本上一定会遇到泛型方法的迁移问题:

public T Find<T>(int index)
{
}

比如以上这个方法,找到了就返回找到的值,找不到就返回 T 的默认值。那么问题来了,T 没有指定这是值类型还是引用类型。

如果 T 是引用类型,那么默认值 default(T) 就会引入 null。但是泛型 T 并没有写成 T?,因此它是不可为 null 的。然而值类型和引用类型的 T? 代表的是不同的含义。这种矛盾应该怎么办?

这个时候,请给返回值标记 MaybeNull

++  [return: MaybeNull]
    public T Find<T>(int index)
    {
    }

这表示此方法应该返回一个不可为 null 的类型,但在某些情况下可能会返回 null

实际上这样的写法并没有从本质上解决掉泛型 T 的问题,不过可以用来给旧项目迁移时用来兼容 API 使用。

如果你可以不用考虑 API 的兼容性,那么可以使用新的泛型契约 where T : notnull

public T Find<T>(int index) where T : notnull
{
}

输出:NotNull

设想你有一个方法,方法参数是可以传入 null 的:

public void EnsureInitialized(ref string? text)
{
}

然而这个方法的语义是确保此字段初始化。于是可以传入 null 但不会返回 null 的。这个时候请标记 NotNull

--  public void EnsureInitialized(ref string? text)
++  public void EnsureInitialized([NotNull] ref string? text)
    {
    }

NotNullWhen, MaybeNullWhen

string.IsNullOrEmpty 的实现就使用到了 NotNullWhen

bool IsNullOrEmpty([NotNullWhen(false)] string? value);

它表示当返回 false 的时候,value 参数是不可为 null 的。

这样,你在这个方法返回的 false 判断分支里面,是不需要对变量进行判空的。

当然,更典型的还有 TryDo 模式。比如下面是 Version 类的 TryParse

bool TryParse(string? input, [NotNullWhen(true)] out Version? result)

当返回 true 的时候,result 一定不为 null

NotNullIfNotNull

典型的情况比如指定默认值:

[return: NotNullIfNotNull("defaultValue")]
public string? GetValue(string key, string? defaultValue)
{
}

这段代码里面,如果指定的默认值(defaultValue)是 null 那么返回值也就是 null;而如果指定的默认值是非 null,那么返回值也就不可为 null 了。

在早期 .NET Framework 或者早期版本的 .NET Core 中使用

在本文第一小节里面,我们说 Nullable 是编译到目标程序集中的,所以不需要引用什么特别的程序集就能够使用到可空引用的特性。

那么上面这些特性呢?它们并没有编译到目标程序集中怎么办?

实际上,你只需要有一个命名空间、名字和实现都相同的类型就够了。你可以写一个放到你自己的程序集中,也可以把这些类型写到一个自己公共的库中,然后引用它。当然,你也可以用我已经写好的 NuGet 包 Walterlv.NullableAttributes。

Walterlv.NullableAttributes

微软 .NET 官方的可空特性在这里:

我将其注释翻译成中文之后,也写了一份在这里:

如果你想简单一点,可以直接引用我的 NuGet 包:

源代码包可以在不用引入其他 dll 依赖的情况下完成引用。最终你输出的程序集是不带对此包的依赖的,详见:


参考资料

12-08 2019

一个简单的方法:截取子类名称中不包含基类后缀的部分

基类是 MenuItem,子类是 WalterlvMenuItemFooMenuItem。基类是 Configuration,子类是 WalterlvConfigurationExtensionConfiguration。在代码中,我们可能会为了能够一眼看清类之间的继承(从属)关系而在子类名称后缀中带上基类的名称。但是由于这种情况下的基类不参与实际的业务,所以对外(文件/网络)的名称通常不需要带上这个后缀。

本文提供一个简单的方法,让子类中基类的后缀删掉,只取得前面的那部分。


在这段代码中,我们至少需要获得两个传入的参数,一个是基类的名称,一个是子类的名称。但是考虑到让开发者就这样传入两者名称的话会比较容易出问题,因为开发者可能根本就不会按照要求去获取类型的名称。所以我们需要自己通过类型对象来获取名称。

另外,我们还需要有一些约束,必须有一个类型是另外一个类型的子类。于是我们可能必须来使用泛型做这样的约束。

于是,我们可以写出下面的方法:

using System;

namespace Walterlv.Utils
{
    /// <summary>
    /// 包含类名相关的处理方法。
    /// </summary>
    internal static class ClassNameUtils
    {
        /// <summary>
        /// 当某个类型的派生类都以基类(<typeparamref name="T"/>)名称作为后缀时,去掉后缀取派生类名称的前面部分。
        /// </summary>
        /// <typeparam name="T">名称统一的基类名称。</typeparam>
        /// <param name="this">派生类的实例。</param>
        /// <returns>去掉后缀的派生类名称。</returns>
        internal static string GetClassNameWithoutSuffix<T>(this T @this)
        {
            if (@this is null)
            {
                throw new ArgumentNullException(nameof(@this));
            }

            var derivedTypeName = @this.GetType().Name;
            var baseTypeName = typeof(T).Name;
            // 截取子类名称中去掉基类后缀的部分。
            var name = derivedTypeName.EndsWith(baseTypeName, StringComparison.Ordinal)
                ? derivedTypeName.Substring(0, derivedTypeName.Length - baseTypeName.Length)
                : derivedTypeName;
            // 如果子类名称和基类完全一样,则直接返回子类名称。
            return string.IsNullOrWhiteSpace(name) ? derivedTypeName : name;
        }
    }
}

我们通过判断子类是否以基类名称作为后缀来决定是否截取子字符串。

在截取完子串之后,我们还需要验证截取的字符串是否已经是空串了,因为父子类的名称可能是完全一样的(虽然这样的做法真的很逗比)。

于是使用起来只需要简单调用一下:

class Program
{
    static void Main(string[] args)
    {
        var name = ClassNameUtils.GetClassNameWithoutSuffix<Foo>(new XFoo());
    }
}

internal class Foo
{

}

internal class XFoo : Foo
{

}

于是我们可以得到 name 局部变量的值为 X。如果这个时候我们对 XFoo 类型改名,例如改成 XFoo1,那么就不会截取,而是直接得到名称 XFoo1

12-08 2019

可集成到文件管理器,一句 PowerShell 脚本发布某个版本的所有 NuGet 包

要发布 NuGet 包,只需要执行命令 nuget push xxx.nupkg 即可,或者去 nuget.org 点鼠标上传。

不过,如果你有很多的 NuGet 包并且经常需要推送的话,也可以集成到 Directory Opus 或者 Total Commander 中。


NuGet 推送命令

NuGet 推送命令可直接在微软官方文档中阅读到:

在你已经设置了 ApiKey 的情况下:

nuget setapikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -source https://api.nuget.org/v3/index.json

之后你只需要执行一句命令即可:

nuget.exe push Walterlv.Themes.FluentDesign.Source.0.8.0-alpha.nupkg -source https://api.nuget.org/v3/index.json

或者推送此文件夹下 0.8.0-alpha 版本的所有 NuGet 包:

nuget.exe push *.0.8.0-alpha.nupkg -source https://api.nuget.org/v3/index.json

用 PowerShell 包装一下

要执行 NuGet 的推送命令,我们需要一个可以执行命令的终端,比如 PowerShell。命令的执行结果我们也可以直接在终端看到。

不过,如果命令是集成到其他工具里面,那么就不一定能够看得到命令的执行结果了。

这个时候,可以考虑用 PowerShell 间接执行这个命令:

# PowerShell 版本
powershell -NoExit -c "nuget push *.0.8.0-alpha.nupkg -Source https://api.nuget.org/v3/index.json"
# PowerShell Core 版本
pwsh -NoExit -c "nuget push *.0.8.0-alpha.nupkg -Source https://api.nuget.org/v3/index.json"

关于使用 PowerShell 间接执行命令的更多细节,可以参考我的另一篇博客:

集成到 Directory Opus

我将这个命令集成到了 Directory Opus 中,这样,一次点击或者一个快捷键就能发布某个特定版本的所有的 NuGet 包了。

集成到 Directory Opus

关于使用 Directory Opus 继承工具栏按钮的细节,可以阅读我的另一篇博客:

具体来说,就是安装上文中所述的方法添加一个按钮,在按钮当中需要执行的脚本如下:

cd "{sourcepath} "
pwsh -NoExit -c "$file=[Regex]::Match('{file}', '\.\d+\.\d+\.\d+.+.nupkg').Value; nuget push *$file -Source https://api.nuget.org/v3/index.json"

含义为:

  1. 转到 Directory Opus 当前目录
  2. 执行一段 PowerShell 脚本,但执行完之后不退出(这样,我可以观察到我实际上推送的是哪一些包,并且可以知道推送是否出现了错误)
  3. 要执行的命令为 nuget push *.xxx.nupkg -Source https://api.nuget.org/v3/index.json
    • 其中,中间的 xxx 是使用正则表达式匹配的 {file} 文件名
    • {file} 是 Directory Opus 当前选中的文件,我用正则表达式匹配出其版本号和后面的 .nupkg 后缀
    • 将正则表达式匹配出来的文本作为 nuget push 的包,最终生成的命令会非常类似于本文一开始提到的命令 nuget push *.0.8.0-alpha.nupkg -Source https://api.nuget.org/v3/index.json

Directory Opus 工具栏按钮

于是,当我选中了一个包,按下这个工具栏按钮之后,就可以推送与这个包相同版本的所有的 NuGet 包了。

毕竟我一次编译产生的 NuGet 包太多了,还是需要使用这样的方式来提高一点效率。至于为什么不用持续集成,是因为目前 SourceYard 还不支持在 GitHub 上集成。

一键推送 NuGet 包


参考资料

11-27 2019

WPF 程序如何跨窗口/跨进程设置控件焦点

WPF 程序提供了 Focus 方法和 TraversalRequest 来在 WPF 焦点范围内转移焦点。但如果 WPF 窗口中嵌入了其他框架的 UI(比如另一个子窗口),那么就需要使用其他的方法来设置焦点了。


一个粗略的设置方法是,使用 Win32 API:

SetFocus(hwnd);

传入的是要设置焦点的窗口的句柄。


参考资料

10-29 2019

C#/.NET 当我们在写事件 += 和 -= 的时候,方法是如何转换成事件处理器的

当我们在写 +=-= 事件的时候,我们会在 +=-= 的右边写上事件处理函数。我们可以写很多种不同的事件处理函数的形式,那么这些形式都是一样的吗?如果你不注意,可能出现内存泄漏问题。

本文将讲解事件处理函数的不同形式,理解了这些可以避免编写代码的时候出现内存相关的问题。


典型的事件处理函数

事件处理函数本质上是一个委托,比如 FileSystemWatcherChanged 事件是这样定义的:

// 这是简化的代码。
public event FileSystemEventHandler Changed;

这里的 FileSystemEventHandler 是一个委托类型:

public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);

一个典型的事件的 += 会像下面这样:

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

+= 的右边传入的是一个 new 出来的委托实例。

变种事件处理函数

除了上面直接创建的目标类型的委托之外,还有其他类型可以放到 += 的右边:

// 方法组。
watcher.Changed += OnChanged;
// Lambda 表达式。
watcher.Changed += (sender, e) => Console.WriteLine(e.ChangeType);
// Lambda 表达式。
watcher.Changed += (sender, e) =>
{
    // 事件引发时,代码会在这里执行。
};
// 匿名方法。
watcher.Changed += delegate (object sender, FileSystemEventArgs e)
{
    // 事件引发时,代码会在这里执行。
};
// 委托类型的局部变量(或者字段)。
FileSystemEventHandler onChanged = (sender, e) => Console.WriteLine(e.ChangeType);
watcher.Changed += onChanged;
// 局部方法(或者局部静态方法)。
watcher.Changed += OnChanged;
void OnChanged(object sender, FileSystemEventArgs e)
{
}

因为我们可以通过编写事件的 addremove 方法来观察事件 += -= 传入的 value 是什么类型的什么实例,所以可以很容易验证以上每一种实例最终被加入到事件中的真实实例。

实际上我们发现,无论哪一个,最终传入的都是 FileSystemEventHandler 类型的实例。

都是同一类型的实例

然而我们知道,只有直接 new 出来的那个和局部变量那个真正是 FileSystemEventHandler 类型的实例,其他都不是。

那么中间发生了什么样的转换使得我们所有种类的写法最终都可以 += 呢?

编译器类型转换

具有相同签名的不同委托类型,彼此之前并没有继承关系,因此在运行时是不可以进行类型转换的。

比如:

FileSystemEventHandler onChanged1 = (sender, e) => Console.WriteLine(e.ChangeType);
Action<object, FileSystemEventArgs> onChanged2 = (sender, e) => Console.WriteLine(e.ChangeType);

这里,onChanged1 的实例不可以赋值给 onChanged2,反过来 onChanged2 的实例也不可以赋值给 onChanged1。于是这里只有 onChanged1 才可以作为 Changed 事件 += 的右边,而 onChanged2 放到 += 右边是会出现编译错误的。

不能转换

然而,我们可以放 Lambda 表达式,可以放匿名函数,可以放方法组,也可以放局部函数。因为这些类型可以在编译期间,由编译器帮助进行类型转换。而转换的效果就类似于我们自己编写 new FileSystemEventHandler(xxx) 一样。

不是同一个委托实例

看下面这一段代码,你认为可以 -= 成功吗?

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
    watcher.Changed -= new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

实际上这是可以 -= 成功的。

我们平时编写代码的时候,下面的情况可能会多一些,于是自然而然以为 +=-= 可以成功,因为他们“看起来”是同一个实例:

watcher.Changed += OnChanged;
watcher.Changed -= OnChanged;

在读完刚刚那一段之后,我们就可以知道,实际上这一段和上面 new 出来委托的写法在运行时是一模一样的。

如果你想测试,那么在 += 的时候为对象加上一个 Id,在 -= 的时候你就会发现这是一个新对象(因为没有 Id)。

不是同一个对象

然而,你平时众多的编码经验会告诉你,这里的 -= 是一定可以成功的。也就是说,+=-= 时传入的委托实例即便不是同一个,也是可以成功 +=-= 的。

+= -= 是怎么做的

+=-= 到底是怎么做的,可以在不同实例时也能 +=-= 成功呢?

+=-= 实际上是调用了 DelegateCombineRemove 方法,并生成一个新的委托实例赋值给 += -= 的左边。

public event FileSystemEventHandler Changed
{
    add
    {
        onChangedHandler = (FileSystemEventHandler)Delegate.Combine(onChangedHandler, value);
    }
    remove
    {
        onChangedHandler = (FileSystemEventHandler)Delegate.Remove(onChangedHandler, value);
    }
}

而最终的判断也是通过 DelegateEquals 方法来比较委托的实例是否相等的(==!= 也是调用的 Equals):

public override bool Equals(object? obj)
{
    if (obj == null || !InternalEqualTypes(this, obj))
        return false;

    Delegate d = (Delegate)obj;

    // do an optimistic check first. This is hopefully cheap enough to be worth
    if (_target == d._target && _methodPtr == d._methodPtr && _methodPtrAux == d._methodPtrAux)
        return true;

    // even though the fields were not all equals the delegates may still match
    // When target carries the delegate itself the 2 targets (delegates) may be different instances
    // but the delegates are logically the same
    // It may also happen that the method pointer was not jitted when creating one delegate and jitted in the other
    // if that's the case the delegates may still be equals but we need to make a more complicated check

    if (_methodPtrAux == IntPtr.Zero)
    {
        if (d._methodPtrAux != IntPtr.Zero)
            return false; // different delegate kind
        // they are both closed over the first arg
        if (_target != d._target)
            return false;
        // fall through method handle check
    }
    else
    {
        if (d._methodPtrAux == IntPtr.Zero)
            return false; // different delegate kind

        // Ignore the target as it will be the delegate instance, though it may be a different one
        /*
        if (_methodPtr != d._methodPtr)
            return false;
            */

        if (_methodPtrAux == d._methodPtrAux)
            return true;
        // fall through method handle check
    }

    // method ptrs don't match, go down long path
    //
    if (_methodBase == null || d._methodBase == null || !(_methodBase is MethodInfo) || !(d._methodBase is MethodInfo))
        return Delegate.InternalEqualMethodHandles(this, d);
    else
        return _methodBase.Equals(d._methodBase);
}

于是可以看出来,判断相等就是两个关键对象的判断相等:

  1. 方法所在的对象
  2. 方法信息(对应到反射里的 MethodInfo)

继续回到这段代码:

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
    watcher.Changed -= new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

这里的对象就是 this,方法信息就是 OnChanged 的信息,也就是:

// this 就是对象,OnChanged 就是方法信息。
this.OnChanged

-=

于是什么样的 -= 才可以把 += 加进去的事件处理函数减掉呢?

  • 必须是同一个对象的同一个方法

所以:

  1. 使用方法组、静态局部函数、委托字段的方式创建的委托实例,在 +=-= 的时候无视哪个委托实例,都是可以减掉的;
  2. 使用局部函数、委托变量,在同一个上下文中,是可以减掉的,如果调用是再次进入此函数,则不能减掉(因为委托方法所在的对象实例不同)
  3. 使用 Lambda 表达式、匿名函数是不能减掉的,因为每次编写的 Lambda 表达式和匿名函数都会创建新的包含此对象的实例。

10-29 2019

如何在 .NET 项目中开启不安全代码(以便启用 unsafe fixed 等关键字)

有小伙伴希望在 .NET 代码中使用指针,操作非托管资源,于是可能使用到 unsafe fixed 关键字。但使用此关键字的前提是需要在项目中开启不安全代码。

本文介绍如何在项目中开启不安全代码。


入门方法

第一步:在你需要启用不安全代码的项目上点击右键,然后选择属性:

项目 - 属性

第二步:在“生成”标签下,勾选上“允许不安全代码”:

允许不安全代码

第三步:切换到 Release 配置,再勾上一次“允许不安全代码”(确保 Debug 和 Release 都打开)

在 Release 允许不安全代码

方法结束。

如果你一开始选择了“所有配置”,那么就不需要分别在 Debug 和 Release 下打开了,一次打开即可。

高级方法

推荐

如果你使用 .NET Core / .NET Standard 项目,那么你可以修改项目文件来实现,这样项目文件会更加清真。

第一步:在你需要启用不安全代码的项目上点击右键,然后选择编辑项目文件:

编辑项目文件

第二步:在你的项目文件的属性组中添加一行 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

我已经把需要新增的行高亮出来了

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>

    </Project>

临时方法

不推荐

如果你只是临时希望加上不安全代码开关,则可以在编译的时候加入 -unsafe 命令行参数:

csc -unsafe walterlv.cs

注意,不能给 msbuild 或者 dotnet build 加上 -unsafe 参数来编译项目,只能使用 csc 加上 -unsafe 来编译文件。因此使用场景非常受限,不推荐使用。

其他说明

第一种方法(入门方法)和第二种方法(高级方法)最终的修改是有一些区别的。入门方法会使得项目文件中有针对于 Debug 和 Release 的不同配置,代码会显得冗余;而高级方法中只增加了一行,对任何配置均生效。

因此如果可能,尽量使用高级方法呗。

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>

--    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
--      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
--    </PropertyGroup>

--    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
--      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
--    </PropertyGroup>

    </Project>

即使是 .NET Framework 也是可以使用 SDK 风格的项目文件的,详情请阅读:

10-29 2019

不要在 C# 代码中写部分命名空间(要么不写,要么写全),否则会有源码兼容性问题

我只是增加库的一个 API,比如增加几个类而已,应该不会造成兼容性问题吧。对于编译好的二进制文件来说,不会造成兼容性问题;但——可能造成源码不兼容。

本文介绍可能的源码不兼容问题。


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

比如我有一个项目 P 引用 A 和 B 两个库。其中使用到了 A 库中的 Walterlv.A.Diagnostics.Foo 类型。

using Walterlv.A;
using Walterlv.B;

namespace Walterlv.Demo
{
    class Hello
    {
        Run(Diagnostics.Foo foo)
        {
        }
    }
}

现在,我们在 B 库中新增一个类型 Walterlv.B.Diagnostics.Bar 类型。

那么上面的代码将无法完成编译,因为 Diagnosis 命名空间将具有不确定的含义,其中的 Foo 类型也将无法在不确定的命名空间中找到。

因此:

  1. 强烈建议遵守 使用类型的时候,要么不写命名空间(完全留给 using),要么写全命名空间(从第一段开始写,不要省略任何部分),否则就容易与其他命名空间冲突;
  2. 可选遵守 在库中新增 API 的时候,可能需要考虑避免将部分命名空间写成过于通用的名称。

是的,即使是单纯的新增 API 也可能会导致使用库的一方在源码级不兼容。当然二进制还是兼容的。

另外,OpportunityLiu 提醒,如果命名空间是 Walterlv.B.Walterlv.A.Diagnostics.Bar,一样可以让写全了的命名空间炸掉。呃……还是不要在库里面折腾这样的命名空间好……不然代码当中到处充斥着 global:: 可是非常难受的。

10-22 2019

.NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表

我们知道,32 位程序在读取注册表的时候,会自动将注册表的路径映射到 32 位路径下,即在 Wow6432Node 子节点下。但是 64 位程序不会映射到 32 位路径下。那么 64 位程序如何读取到 32 位程序写入的注册表路径呢?


Wow6432Node

Wow6432Node

对于 32 位程序,读取注册表路径的时候,会读到 Wow6432Node 节点下的项:

32 位

这张图读取的就是前面截图中的节点。

那么怎样编译的程序是 32-bit 的程序呢?

x86

AnyCPU 32-bit preferred

对于 64 位程序,读取的时候就不会有 Wow6432Node 路径部分。由于我没有在那个路径放注册表项,所以会得到 null

null

那么怎样编译的程序是 64-bit 的程序呢?

x64

AnyCPU

如何在 64 位程序中读取 32 位注册表路径

前面我们的例子代码是这样的:

var value = RegistryHive.LocalMachine.Read(@"SOFTWARE\Walterlv");

可以看到,相同的代码,在 32 位和 64 位进程下得到的结果是不同的:

  • 32 位进程在 32 位系统上,64 位进程在 64 位系统上,读取的路径会是传入的路径;
  • 32 位进程在 64 位系统上,读取的路径会包含 Wow6432Node

那么如何在 64 位进程中读取 32 位注册表路径呢?

方法是在打开注册表项的时候,传入 RegistryView.Registry32

RegistryKey.OpenBaseKey(root, RegistryView.Registry32);

Walterlv.Win32

可以在我的 GitHub 仓库中查看完整的实现。当然,除了上面那句话,其他都不是关键代码,在哪里都可以找得到的。


参考资料

10-22 2019

解决 WPF 嵌套的子窗口在改变窗口大小的时候闪烁的问题

因为 Win32 的窗口句柄是可以跨进程传递的,所以可以用来实现跨进程 UI。不过,本文不会谈论跨进程 UI 的具体实现,只会提及其实现中的一个重要缓解,使用子窗口的方式。

你有可能在使用子窗口之后,发现拖拽改变窗口大小的时候,子窗口中的内容不断闪烁。如果你也遇到了这样的问题,那么正好可以阅读本文来解决。


问题

你可以看一下下面的这张动图,感受一下窗口的闪烁:

窗口闪烁

实际上在拖动窗口的时候,是一直都在闪的,只是每次闪烁都非常快,截取 gif 的时候截不到。

如果你希望实际跑一跑项目看看,可以使用下面的代码:

我特地提取了一个提交下的代码,如果你要尝试,不能使用 master 分支,因为 master 分支修复了闪烁的问题。

后来使用 CreateWindowEx 创建了一个纯 Win32 窗口,这种闪烁现象更容易被截图:

Win32 窗口闪烁

Win32 窗口闪烁 - 动图

解决

    public class HwndWrapper : HwndHost
    {
        protected override HandleRef BuildWindowCore(HandleRef hwndParent)
        {
            const int WS_CHILD = 0x40000000;
++          const int WS_CLIPCHILDREN = 0x02000000;
            var owner = ((HwndSource)PresentationSource.FromVisual(this)).Handle;

            var parameters = new HwndSourceParameters("demo")
            {
                ParentWindow = owner,
--              WindowStyle = (int)(WS_CHILD),
++              WindowStyle = (int)(WS_CHILD | WS_CLIPCHILDREN),
            };
            var source = new HwndSource(parameters);
            source.RootVisual = new ChildPage();
            return new HandleRef(this, source.Handle);
        }

        protected override void DestroyWindowCore(HandleRef hwnd)
        {
        }
    }

原因

正在探索……


参考资料

10-22 2019

System.ComponentModel.Win32Exception (0x80004005): 无效的窗口句柄。

在 WPF 获取鼠标当前坐标的时候,可能会得到一个异常:System.ComponentModel.Win32Exception:“无效的窗口句柄。”

本文解释此异常的原因和解决方法。


异常

获取鼠标当前相对于元素 element 的坐标的代码:

var point = Mouse.GetPosition(element);

或者,还有其他的代码:

var point1 = e.PointFromScreen(new Point());
var point2 = e.PointToScreen(new Point());

如果在按下窗口关闭按钮的时候调用以上代码,则会引发异常:

System.ComponentModel.Win32Exception (0x80004005): 无效的窗口句柄。
   at Point MS.Internal.PointUtil.ClientToScreen(Point pointClient, PresentationSource presentationSource)
   at Point System.Windows.Input.MouseDevice.GetScreenPositionFromSystem()

原因

将窗口上的点转换到控件上的点的方法是这样的:

/// <summary>
///     Convert a point from "client" coordinate space of a window into
///     the coordinate space of the screen.
/// </summary>
/// <SecurityNote>
///     SecurityCritical: This code causes eleveation to unmanaged code via call to GetWindowLong
///     SecurityTreatAsSafe: This data is ok to give out
///     validate all code paths that lead to this.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
public static Point ClientToScreen(Point pointClient, PresentationSource presentationSource)
{
    // For now we only know how to use HwndSource.
    HwndSource inputSource = presentationSource as HwndSource;
    if(inputSource == null)
    {
        return pointClient;
    }
    HandleRef handleRef = new HandleRef(inputSource, inputSource.CriticalHandle);

    NativeMethods.POINT ptClient            = FromPoint(pointClient);
    NativeMethods.POINT ptClientRTLAdjusted = AdjustForRightToLeft(ptClient, handleRef);

    UnsafeNativeMethods.ClientToScreen(handleRef, ptClientRTLAdjusted);

    return ToPoint(ptClientRTLAdjusted);
}

最关键的是 UnsafeNativeMethods.ClientToScreen,此方法要求窗口句柄依然有效,然而此时窗口已经关闭,句柄已经销毁。

解决

10-22 2019

使用 Direct3D11 的 OpenSharedResource 方法渲染来自其他进程/设备的共享资源(SharedHandle)

如果你得到了一个来自于其他进程或者其他模块的 Direct3D11 的共享资源,即 SharedHandle 句柄,那么可以使用本文提到的方法将其转换成 Direct3D11 的设备和纹理,这样你可以进行后续的其他处理。


SharpDX

本文的代码会使用到 SharpDX 库,因此,你需要在你的项目当中安装这些 NuGet 包:

<!-- 基础,必装 -->
<PackageReference Include="SharpDX" Version="4.2.0" />
<PackageReference Include="SharpDX.D3DCompiler" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />

<!-- 其他,可选 -->
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" />

SharedHandle

Direct3D 支持在不同的 Direct3D 设备之间共享资源。需要设置 ResourceOptionFlagsShared 的纹理才可以支持共享,当然这不是本文要说的重点。

本文要说的是,如果你拿到了一个来自于其他模块的共享资源句柄的时候,你可以如何使用它。

你的使用可能类似于这样:

public void OnAcceleratedPaint(IntPtr sharedHandle, Int32Rect dirtyRect)
{
    // 通过 sharedHandle 进行后续的处理。
}

OpenSharedResource

DirectX 中用来表示 Direct3D11 的设备类型是 ID3D11Device,它有一个 OpenSharedResource 方法可以用来打开来自于其他设备的共享资源。

对应到 SharpDX 中,用来表示 Direct3D11 的设备的类型是 SharpDX.Direct3D11.Device,其有一个 OpenSharedResource<T> 方法来打开来自于其他设备的共享资源。

我们必须要创建一个自己的 Direct3D11 设备,因为设备是不共享的,代码如下:

var device = new SharpDX.Direct3D11.Device(DriverType.Hardware, DeviceCreationFlags.BgraSupport);
var resource = device.OpenSharedResource<SharpDX.Direct3D11.Resource>(sharedHandle);

后续操作

在得到此共享资源之后,我们可以获得更多关于此资源的描述,以及有限地使用此资源的方法。

获取 Texture2D

可以通过 QueryInterface 获取某个资源相关的 COM 对象的引用。我们拿到的共享资源是 2D 纹理的话,我们可以使用 QueryInterface 获取 SharpDX.Direct3D11.Texture2D COM 对象的引用。

var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();

获取 Texture2DDescription

可以从 Texture2D 的实例中获取到 Texture2DDescription,这是用来描述此 2D 纹理创建时的各种信息。

// 在 DirectX 的传统代码中,通常使用 desc 来作为 Texture2DDescription 实例命名的后缀。
// 不过 C# 代码通常不这么干,这是 C++ 代码的习惯。在这里这么写是为了在得到 C++ 搜索结果的时候可以与本文所述的 C# 代码对应起来。
var desc = texture.Description;

获取 Surface

或者,我们可以获取到 2D 图面,用于做渲染、绘制等操作。当然,是否能真正进行这些操作取决于 Texture2DDescription 中是否允许相关的操作。

var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>();

在获取到 SharpDX.DXGI.Surface 的 COM 组件引用之后,可以在内存中映射位图用于调试,可以参见:


参考资料

10-22 2019

将 Direct3D11 在 GPU 中的纹理(Texture2D)导出到内存(Map)或导出成图片文件

Direct3D11 的使用通常不是应用程序唯一的部分,于是使用 Direct3D11 的代码如何与其他模块正确地组合在一起就是一个需要解决的问题。

本文介绍将 Direct3D11 在 GPU 中绘制的纹理映射到内存中,这样我们可以直接观察到此纹理是否是正确的,而不用担心是否有其他模块影响了最终的渲染过程。


SharpDX

本文的代码会使用到 SharpDX 库,因此,你需要在你的项目当中安装这些 NuGet 包:

<!-- 基础,必装 -->
<PackageReference Include="SharpDX" Version="4.2.0" />
<PackageReference Include="SharpDX.D3DCompiler" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />

<!-- 其他,可选 -->
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" />

来自于 Direct3D11 的渲染纹理

本文不会说如何创建或者获取来自 Direct3D11 的渲染纹理,不过如果你希望了解,可以:

本文接下来的内容,是在你已经获得了 SharpDX.Direct3D11.Resource 的引用,或者 SharpDX.Direct3D11.Texture2D 的前提之下。当然,如果你获得了其中任何一个实例,可以通过 COM 组件的 QueryInterface 方法获得其他实例。

var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();
var resource = texture.QueryInterface<SharpDX.Direct3D11.Resource>();

关键代码(SharpDX.DXGI.Surface.Map)

要获得 GPU 中渲染的图片,我们必须要将其映射到内存中才行。而映射到内存中的核心代码是 SharpDX.DXGI.Surface 对象的 Map 方法。

using (var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>())
{
    var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream);
    for (var y = 0; y < surface.Description.Height; y++)
    {
        for (var x = 0; x < surface.Description.Width; x++)
        {
            // 在这里使用位图的像素数据,坐标为 (x, y)。
            // 得到此坐标下的像素指针:
            //     var ptr = ((byte*)map.DataPointer) + y * map.Pitch;
            // 得到此像素的颜色值:
            //     var b = *(ptr + 4 * x);
            //     var g = *(ptr + 4 * x + 1);
            //     var r = *(ptr + 4 * x + 2);
            //     var a = *(ptr + 4 * x + 3);
        }
    }
    dataStream.Dispose();
    surface.Unmap();
}

注意以上代码使用了不安全代码(指针),你需要为你的项目开启不安全代码开关,详见:

你可能需要拷贝资源

实际上,在使用上面的代码时,你可能会遇到错误,错误出现在 Map 方法的调用上,描述为“参数错误”。实际上真正检查这里的两个参数时并不能发现究竟是哪个参数出了问题。

实际上出问题的参数是 surface 的实例。

一段 GPU 中的纹理要能够被映射到内存,必须要具有 CPU 的访问权。而是否具有 CPU 访问权在创建纹理的时候就已经确定下来了。

如果前面你得到的纹理是自己创建的,那么恭喜你,你只需要改一下创建纹理的参数就好了。给 Texture2DDescriptionCpuAccessFlags 属性加上 CpuAccessFlags.Read 标识。

desc.CpuAccessFlags = CpuAccessFlags.Read

但是,如果此纹理不是由你自己创建的,那么就需要拷贝一份新的纹理了。当然,拷贝过程发生在 GPU 中,占用的也是 GPU 专用内存(即显存,如果有的话)。

拷贝需要做到两点:

  1. 创建一个新的 Texture2DDescription(一定要是新的实例,你不能影响原来的实例),然后修改其 CPU 访问权限为 Read
  2. 使用 ImmediateContext 实例的 CopyResource 方法来拷贝资源(此实例可以通过 SharpDX.Direct3D11.Device 来找到)。
var originalDesc = originalTexture.Description;
var desc = new Texture2DDescription
{
    CpuAccessFlags = CpuAccessFlags.Read,
    BindFlags = BindFlags.None,
    Usage = ResourceUsage.Staging,
    Width = originalDesc.Width,
    Height = originalDesc.Height,
    Format = originalDesc.Format,
    MipLevels = 1,
    ArraySize = 1,
    SampleDescription =
    {
        Count = 1,
        Quality = 0
    },
};

var texture2D = new Texture2D(device, desc);
device.ImmediateContext.CopyResource(originalTexture, texture2D);

需要注意,拷贝纹理会额外占用显存,一般不建议这么做,除非你真的有需求一定要 CPU 能够访问到这段纹理。

导出成图片文件

实际上,当你组合起来以上以上方法,你应该能够将纹理导出成图片了。

不过,为了理解更方便一些,我还是将导出成图片的全部代码贴出来:

public static unsafe void MapTexture2DToFile(SharpDX.Direct3D11.Texture2D texture, string fileName)
{
    // 获取 Texture2D 的相关实例。
    var device = texture.Device;
    var originDesc = texture.Description;

    // 创建新的 Texture2D 对象。
    var desc = new Texture2DDescription
    {
        CpuAccessFlags = CpuAccessFlags.Read,
        BindFlags = BindFlags.None,
        Usage = ResourceUsage.Staging,
        Width = originDesc.Width,
        Height = originDesc.Height,
        Format = originDesc.Format,
        MipLevels = 1,
        ArraySize = 1,
        SampleDescription =
        {
            Count = 1,
            Quality = 0
        },
        OptionFlags = ResourceOptionFlags.Shared
    };
    var texture2D = new Texture2D(device, desc);

    // 拷贝资源。
    device.ImmediateContext.CopyResource(texture, texture2D);

    var bitmap = new System.Drawing.Bitmap(desc.Width, desc.Height);
    using (var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>())
    {
        var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream);
        var lines = (int)(dataStream.Length / map.Pitch);
        var actualWidth = surface.Description.Width * 4;
        for (var y = 0; y < desc.Height; y++)
        {
            var h = desc.Height - y;
            var ptr = ((byte*)map.DataPointer) + y * map.Pitch;

            for (var x = 0; x < desc.Width; x++)
            {
                var b = *(ptr + 4 * x);
                var g = *(ptr + 4 * x + 1);
                var r = *(ptr + 4 * x + 2);
                var a = *(ptr + 4 * x + 3);
                bitmap.SetPixel(x, y, System.Drawing.Color.FromArgb(a, r, g, b));
            }
        }
        dataStream.Dispose();
        surface.Unmap();
        bitmap.Save(fileName);
    }
}

如果你是希望以纯软件的方式渲染到 WPF 中(WriteableBitmap),可以参考:

记得打开不安全代码开关哦!详见:


参考资料

10-22 2019

.NET 实现 NTFS 文件系统的硬链接 mklink /J(Junction)

我们知道 Windows 系统 NTFS 文件系统提供了硬连接功能,可以通过 mklink 命令开启。如果能够通过代码实现,那么我们能够做更多有趣的事情。

本文提供使用 .NET/C# 代码创建 NTFS 文件系统的硬连接功能(目录联接)。


目录联接

以管理员权限启动 CMD(命令提示符),输入 mklink 命令可以得知 mklink 的用法。

C:\WINDOWS\system32>mklink
创建符号链接。

MKLINK [[/D] | [/H] | [/J]] Link Target

        /D      创建目录符号链接。默认为文件
                符号链接。
        /H      创建硬链接而非符号链接。
        /J      创建目录联接。
        Link    指定新的符号链接名称。
        Target  指定新链接引用的路径
                (相对或绝对)

我们本次要用 .NET/C# 代码实现的是 /J 目录联接。实现的效果像这样:

目录联接

这些文件夹带有一个“快捷方式”的角标,似乎是另一些文件夹的快捷方式一样。但这些与快捷方式的区别在于,应用程序读取路径的时候,目录联接会成为路径的一部分。

比如在 D:\Walterlv\NuGet\ 中创建 debug 目录联接,目标设为 D:\Walterlv\DemoRepo\bin\Debug,那么,你在各种应用程序中使用以下两个路径将被视为同一个:

  • D:\Walterlv\NuGet\debug\DemoRepo-1.0.0.nupkg
  • D:\Walterlv\DemoRepo\bin\Debug\DemoRepo-1.0.0.nupkg

或者这种:

  • D:\Walterlv\NuGet\debug\publish\
  • D:\Walterlv\DemoRepo\bin\Debug\publish\

使用 .NET/C# 实现

本文的代码主要参考自 jeff.brownManipulating NTFS Junction Points in .NET - CodeProject 一文中所附带的源代码。

由于随时可能更新,所以你可以前往 GitHub 仓库打开此代码:

使用 JunctionPoint

如果希望在代码中创建目录联接,则直接使用:

JunctionPoint.Create("walterlv.demo", @"D:\Developments", true);

后面的 true 指定如果目录联接存在,则会覆盖掉原来的目录联接。


参考资料

10-22 2019

WPF 高性能位图渲染 WriteableBitmap 及其高性能用法示例

WPF 渲染框架并没有对外提供多少可以完全控制渲染的部分,目前可以做的有:

  • D3DImage,用来承载使用 DirectX 各个版本渲染内容的控件
  • WriteableBitmap,通过一段内存空间来指定如何渲染一个位图的图片
  • HwndHost,通过承载一个子窗口以便能叠加任何种类渲染的控件

本文将解释如何最大程度压榨 WriteableBitmap 在 WPF 下的性能。


如何使用 WriteableBitmap

创建一个新的 WPF 项目,然后我们在 MainWindow.xaml 中编写一点可以用来显示 WriteableBitmap 的代码:

<Window x:Class="Walterlv.Demo.HighPerformanceBitmap.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Walterlv.Demo.HighPerformanceBitmap"
        Title="WriteableBitmap - walterlv" SizeToContent="WidthAndHeight">
    <Grid>
        <Image x:Name="Image" Width="1280" Height="720" />
    </Grid>
</Window>

为了评估其性能,我决定绘制和渲染 4K 品质的位图,并通过以下步骤来评估:

  1. 使用 CompositionTarget.Rendering 逐帧渲染以评估其渲染帧率
  2. 使用 Benchmark 基准测试来测试内部各种不同方法的性能差异

于是,在 MainWindow.xaml.cs 中添加一些测试用的修改 WriteableBitmap 的代码:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace Walterlv.Demo.HighPerformanceBitmap
{
    public partial class MainWindow : Window
    {
        private readonly WriteableBitmap _bitmap;

        public MainWindow()
        {
            InitializeComponent();

            _bitmap = new WriteableBitmap(3840, 2160, 96.0, 96.0, PixelFormats.Pbgra32, null);
            Image.Source = _bitmap;
            CompositionTarget.Rendering += CompositionTarget_Rendering;
        }

        private void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            var width = _bitmap.PixelWidth;
            var height = _bitmap.PixelHeight;

            _bitmap.Lock();

            // 在这里添加绘制位图的逻辑。

            _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
            _bitmap.Unlock();
        }
    }
}

注意,我留了一行注释说即将添加绘制位图的逻辑,接下来我们的主要内容将从此展开。

启用不安全代码

为了获取最佳性能,我们需要开启不安全代码。为此,你需要修改一下你的项目属性。

你可以阅读我的另一篇博客了解如何启用不安全代码:

简单点说就是在你的项目文件中添加下面这一行:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>
    </Project>

启用帧率测试

接下来,我们需要添加一点点代码来评估 WriteableBitmap 的性能:

++  private readonly byte[] _empty4KBitmapArray = new byte[3840 * 2160 * 4];

--  private void CompositionTarget_Rendering(object sender, EventArgs e)
++  private unsafe void CompositionTarget_Rendering(object sender, EventArgs e)
    {
        var width = _bitmap.PixelWidth;
        var height = _bitmap.PixelHeight;

        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          Buffer.MemoryCopy(ptr, _bitmap.BackBuffer.ToPointer(), _empty4KBitmapArray.Length, _empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }

嗯,就是将一个空的 4K 大小的数组中的内容复制到 WriteableBitmap 的位图缓存中。

4K 脏区

虽然我们看不到任何可变的修改,不过 WriteableBitmap 可不这么认为。因为我们调用了 AddDirtyRect 将整个位图空间都加入到了脏区中,这样 WPF 会重新渲染整幅位图。

Visual Studio 中看到的 CPU 占用率大约维持在 16% 左右(跟具体机器相关);并且除了一开始启动的时候之外,完全没有 GC(这点很重要),内存稳定在一个值上不再变化。

也只有本文一开始提及的三种方法才可能做到渲染任何可能的图形的时候没有 GC

CPU 占用率和内存用量

查看界面渲染帧率可以发现跑满 60 帧没有什么问题(跟具体机器相关)。

帧率

小脏区

现在,我们把脏区的区域缩小为 100*100,同样看性能数据。

--  _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++  _bitmap.AddDirtyRect(new Int32Rect(0, 0, 100, 100));

可以发现 CPU 占用降低到一半(确实是大幅降低,但是跟像素数量并不成比例);内存没有变化(废话,4K 图像是确定的);帧率没有变化(废话,只要性能够,帧率就是满的)。

小脏区 CPU 占用率和内存用量

小脏区帧率

无脏区

现在,我们将脏区清零。

--  _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++  _bitmap.AddDirtyRect(new Int32Rect(0, 0, 0, 0));

在完全没有脏区的时候,CPU 占用直接降为 0,这个性能提升还是非常恐怖的。

零脏区 CPU 占用率和内存用量

不渲染

如果我们不把 WriteableBitmap 设置为 ImageSource 属性,那么无论脏区多大,CPU 占用都是 0。

脏区大小与 CPU 占用率之间的关系

从前面的测试中我们可以发现,脏区的大小在 WriteableBitmap 的渲染里占了绝对的耗时。因此,我把脏区大小与 CPU 占用率之间的关系用图表的形式贴出来,这样可以直观地理解其性能差异。

需要注意,CPU 占用率与机器性能强相关,因此其绝对占用没有意义,但相对大小则有参考价值。

脏区大小 CPU 占用率 帧率
0*0 0.0% 60
1*1 5.1% 60
16*9 5.7% 60
160*90 6.0% 60
320*180 6.5% 60
640*360 6.9% 60
1280*720 7.5% 60
1920*1080 10.5% 60
2560*1440 12.3% 60
3840*2160 16.1% 60

根据这张表我么可以得出:

  • 脏区渲染是 CPU 占用的最大瓶颈(因为没有脏区仅剩内存拷贝的时候 CPU 占用为 0%)

但是有一个需要注意的信息是——虽然 CPU 占用率受脏区影响非常大,但主线程却几乎没有消耗 CPU 占用。此占用基本上全是渲染线程的事。

如果我们分析主线程的性能分布,可以发现内存拷贝现在是性能瓶颈:

内存拷贝是性能瓶颈

后面我们会提到 WriteableBitmap 的渲染原理,也会说到这一点。

启用基准测试(Benchmark)

不过,由于内存数据的拷贝和脏区渲染实际上可以分开到两个不同的线程,如果这两者不同步执行(可能执行次数还有差异)的情况下,内存拷贝也可能成为性能瓶颈的一部分。

于是我将不同的内存拷贝方法进行一个基准测试,便于大家评估使用哪种方法来为 WriteableBitmap 提供渲染数据。

使用 CopyMemory 拷贝内存

++  [Benchmark(Description = "CopyMemory")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void CopyMemory(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          CopyMemory(_bitmap.BackBuffer, new IntPtr(ptr), (uint)_empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }
[DllImport("kernel32.dll")]
private static extern void CopyMemory(IntPtr destination, IntPtr source, uint length);

使用 MoveMemory 移动内存

++  [Benchmark(Description = "RtlMoveMemory")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void RtlMoveMemory(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          MoveMemory(_bitmap.BackBuffer, new IntPtr(ptr), (uint)_empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }
    [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
    private static extern void MoveMemory(IntPtr dest, IntPtr src, uint count);

使用 Buffer.MemoryCopy 拷贝内存

需要注意,Buffer.MemoryCopy 是 .NET Framework 4.6 才引入的 API,在 .NET Framework 后续版本以及 .NET Core 的所有版本才可以使用,更旧版本的 .NET Framework 没有这个 API。

++  [Benchmark(Baseline = true, Description = "Buffer.MemoryCopy")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void BufferMemoryCopy(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          Buffer.MemoryCopy(ptr, _bitmap.BackBuffer.ToPointer(), _empty4KBitmapArray.Length, _empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }

自己写 for 循环

++  [Benchmark(Description = "for for")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void ForForCopy(int width, int height)
    {
        _bitmap.Lock();

++      var buffer = (byte*)_bitmap.BackBuffer.ToPointer();
++      for (var j = 0; j < height; j++)
++      {
++          for (var i = 0; i < width; i++)
++          {
++              var pixel = buffer + j * width * 4 + i * 4;
++              *pixel = 0xff;
++              *(pixel + 1) = 0x7f;
++              *(pixel + 2) = 0x00;
++              *(pixel + 3) = 0xff;
++          }
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }

基准测试数据

我们跑一次基准测试:

Method Mean Error StdDev Median Ratio RatioSD
CopyMemory 2.723 ms 0.0642 ms 0.1881 ms 2.677 ms 0.84 0.08
RtlMoveMemory 2.659 ms 0.0740 ms 0.2158 ms 2.633 ms 0.82 0.08
Buffer.MemoryCopy 3.246 ms 0.0776 ms 0.2250 ms 3.200 ms 1.00 0.00
‘for for’ 10.401 ms 0.1979 ms 0.4964 ms 10.396 ms 3.21 0.25
‘CopyMemory with 100*100 dirty region’ 2.446 ms 0.0757 ms 0.2207 ms 2.368 ms 0.76 0.09
‘RtlMoveMemory with 100*100 dirty region’ 2.415 ms 0.0733 ms 0.2161 ms 2.369 ms 0.75 0.08
‘Buffer.MemoryCopy with 100*100 dirty region’ 3.076 ms 0.0612 ms 0.1523 ms 3.072 ms 0.95 0.08
‘for for with 100*100 dirty region’ 10.014 ms 0.2398 ms 0.6995 ms 9.887 ms 3.10 0.29

可以发现:

  1. CopyMemoryRtMoveMemory 性能是最好的,其性能差不多;
  2. 自己写循环拷贝内存的性能是最差的;
  3. 如果 WriteableBitmap 不渲染,那么无论设置多大的脏区都不会对性能有任何影响。

结论和使用建议

综合前面两者的结论,我们可以发现:

  1. WriteableBitmap 的性能瓶颈源于对脏区的重新渲染
    • 脏区为 0 或者不在可视化树渲染,则不消耗性能
    • 只要有脏区,渲染过程就会开始成为性能瓶颈
      • CPU 占用基础值就很高了
      • 脏区越大,CPU 占用越高,但增幅不大
  2. 内存拷贝不是 WriteableBitmap 的性能瓶颈
    • 建议使用 Windows API 或者 .NET API 来拷贝内存(而不是自己写)

另外,如果你有一些特殊的应用场景,可以适当调整下自己写代码的策略:

  • 如果你希望有较大脏区的情况下降低 CPU 占用,可以考虑降低 WriteableBitmap 脏区的刷新率
  • 如果你希望 WriteableBitmap 有较低的渲染延迟,则考虑减小脏区

WriteableBitmap 渲染原理

在调用 WriteableBitmap 的 AddDirtyRect 方法的时候,实际上是调用 MILSwDoubleBufferedBitmap.AddDirtyRect,这是 WPF 专门为 WriteableBitmap 而提供的非托管代码的双缓冲位图的实现。

在 WriteableBitmap 内部数组修改完毕之后,需要调用 Unlock 来解锁内部缓冲区的访问,这时会提交所有的修改。接下来的渲染都交给了 MediaContext,用来完成双缓冲位图的渲染。

private void SubscribeToCommittingBatch()
{
    // Only subscribe the the CommittingBatch event if we are on-channel.
    if (!_isWaitingForCommit)
    {
        MediaContext mediaContext = MediaContext.From(Dispatcher);
        if (_duceResource.IsOnChannel(mediaContext.Channel))
        {
            mediaContext.CommittingBatch += CommittingBatchHandler;
            _isWaitingForCommit = true;
        }
    }
}

在上面的 CommittingBatchHandler 中,将渲染指令发送到了渲染线程。

channel.SendCommand((byte*)&command, sizeof(DUCE.MILCMD_DOUBLEBUFFEREDBITMAP_COPYFORWARD));

前面我们通过脏区大小可以得出内存拷贝不是 CPU 占用率的瓶颈,脏区大小才是,不过是渲染线程在占用这 CPU 而不是主线程。但是内存拷贝却成为了主线程的瓶颈(当然前面我们给出了数据,实际上非常小)。所以如果试图分析这么高 CPU 的占用,会发现并不能从主线程上调查得出符合预期的结论(因为即便你完全干掉了内存拷贝,CPU 占用依然是这么高)。

内存拷贝是性能瓶颈

10-17 2019

WPF 制作高性能的透明背景异形窗口(使用 WindowChrome 而不要使用 AllowsTransparency=True)

在 WPF 中,如果想做一个背景透明的异形窗口,基本上都要设置 WindowStyle="None"AllowsTransparency="True" 这两个属性。如果不想自定义窗口样式,还需要设置 Background="Transparent"。这样的设置会让窗口变成 Layered Window,WPF 在这种类型窗口上的渲染性能是非常糟糕的。

本文介绍如何使用 WindowChrome 而不设置 AllowsTransparency="True" 制作背景透明的异形窗口,这可以避免异形窗口导致的低渲染性能。


背景透明的异形窗口

如下是一个背景透明异形窗口的示例:

示例异形窗口

此窗口包含很大的圆角,还包含 DropShadowEffect 制作的阴影效果。对于非透明窗口来说,这是不可能实现的。

如何实现

要实现这种背景透明的异形窗口,需要为窗口设置以下三个属性:

  • WindowStyle="None"
  • ResizeMode="CanMinimize"ResizeMode="NoResize"
  • WindowChrome.GlassFrameThickness="-1" 或设置为其他较大的正数(可自行尝试设置之后的效果)

如下就是一个最简单的例子,最关键的三个属性我已经高亮标记出来了。

    <Window x:Class="Walterlv.Demo.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
++          WindowStyle="None" ResizeMode="CanMinimize"
            Title="walterlv demo" Height="450" Width="800">
++      <WindowChrome.WindowChrome>
++          <WindowChrome GlassFrameThickness="-1" />
++      </WindowChrome.WindowChrome>
        <Window.Template>
            <ControlTemplate TargetType="Window">
                <Border Padding="64" Background="Transparent">
                    <Border CornerRadius="16" Background="White">
                        <Border.Effect>
                            <DropShadowEffect BlurRadius="64" />
                        </Border.Effect>
                        <ContentPresenter ClipToBounds="True" />
                    </Border>
                </Border>
            </ControlTemplate>
        </Window.Template>
        <Grid>
            <TextBlock FontSize="20" Foreground="#0083d0"
                   TextAlignment="Center" VerticalAlignment="Center">
                <Run Text="欢迎访问吕毅的博客" />
                <LineBreak />
                <Run Text="blog.walterlv.com" FontSize="64" FontWeight="Light" />
            </TextBlock>
        </Grid>
    </Window>

网上流传的主流方法

在网上流传的主流方法中,AllowsTransparency="True" 都是一个必不可少的步骤,另外也需要 WindowStyle="None"。但是我一般都会极力反对大家这么做,因为 AllowsTransparency="True" 会造成很严重的性能问题。

如果你有留意到我的其他博客,你会发现我定制窗口样式的时候都在极力避开设置此性能极差的属性:

性能对比

既然特别说到性能,那也是口说无凭,我们要拿出数据来说话。

以下是我用来测试渲染性能所使用的例子:

测试性能所用的程序

相比于上面的例子来说,主要就是加了背景动画效果,这可以用来测试帧率。

    <Window x:Class="Walterlv.Demo.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            WindowStyle="None" ResizeMode="CanMinimize"
            Title="walterlv demo" Height="450" Width="800">
        <WindowChrome.WindowChrome>
            <WindowChrome GlassFrameThickness="-1" />
        </WindowChrome.WindowChrome>
        <Window.Template>
            <ControlTemplate TargetType="Window">
                <Border Padding="64" Background="Transparent">
                    <Border CornerRadius="16" Background="White">
                        <Border.Effect>
                            <DropShadowEffect BlurRadius="64" />
                        </Border.Effect>
                        <ContentPresenter ClipToBounds="True" />
                    </Border>
                </Border>
            </ControlTemplate>
        </Window.Template>
        <Grid>
++          <Rectangle x:Name="BackgroundRectangle" Margin="0 16" Fill="#d0d1d6">
++              <Rectangle.RenderTransform>
++                  <TranslateTransform />
++              </Rectangle.RenderTransform>
++              <Rectangle.Triggers>
++                  <EventTrigger RoutedEvent="FrameworkElement.Loaded">
++                      <BeginStoryboard>
++                          <BeginStoryboard.Storyboard>
++                              <Storyboard RepeatBehavior="Forever">
++                                  <DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
++                                                   Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"
++                                                   From="800" To="-800" />
++                              </Storyboard>
++                          </BeginStoryboard.Storyboard>
++                      </BeginStoryboard>
++                  </EventTrigger>
++              </Rectangle.Triggers>
++          </Rectangle>
            <TextBlock FontSize="20" Foreground="#0083d0"
                   TextAlignment="Center" VerticalAlignment="Center">
                <Run Text="欢迎访问吕毅的博客" />
                <LineBreak />
                <Run Text="blog.walterlv.com" FontSize="64" FontWeight="Light" />
            </TextBlock>
        </Grid>
    </Window>

那么性能数据表现如何呢?我们让这个窗口在 2560×1080 的屏幕上全屏渲染,得出以下数据:

方案 WindowChrome AllowsTransparency
帧率(fps)数值越大越好,60 为最好 59 19
脏区刷新率(rects/s)数值越大越好 117 38
显存占用(MB)数值越小越好 83.31 193.29
帧间目标渲染数(个)数值越大越好 2 1

另外,对于显存的使用,如果我在 7680×2160 的屏幕上全屏渲染,WindowChrome 方案依然保持在 80+MB,而 AllowsTransparency 已经达到惊人的 800+MB 了。

可见,对于渲染性能,使用 WindowChrome 制作的背景透明异形窗口性能完虐使用 AllowsTransparency 制作的背景透明异形窗口,实际上跟完全没有设置透明窗口的性能保持一致。

使用 WindowChrome 制作透明窗口的性能数据

使用 AllowsTransparency 制作透明窗口的性能数据

功能对比

既然 WindowChrome 方法在性能上完虐网上流传的设置 AllowsTransparency 方法,那么功能呢?

值得注意的是,由于在使用 WindowChrome 制作透明窗口的时候设置了 ResizeMode="None",所以你拖动窗口在屏幕顶部和左右两边的时候,Windows 不会再帮助你最大化窗口或者靠边停靠窗口,于是你需要自行处理。不过窗口的标题栏拖动功能依然保留了下来,标题栏上的右键菜单也是可以继续使用的。

方案 WindowChrome AllowsTransparency
拖拽标题栏移动窗口 保留 自行实现
最小化最大化关闭按钮 丢失 丢失
拖拽边缘调整窗口大小 丢失 丢失
移动窗口到顶部可最大化 丢失 自行实现
拖拽最大化窗口标题栏还原窗口 保留 自行实现
移动窗口到屏幕两边可侧边停靠 丢失 自行实现
拖拽摇动窗口以最小化其他窗口 保留 自行实现
窗口打开/关闭/最小化/最大化/还原动画 丢失 丢失

表格中:

  • 保留 表示此功能无需任何处理即可继续支持
  • 自行实现 表示此功能已消失,但仅需要一两行代码即可补回功能
  • 丢失 表示此功能已消失,如需实现需要编写大量代码

另外,以上表格仅针对鼠标操作窗口。如果算上使用触摸来操作窗口,那么所有标记为 自行实现 的都将变为 丢失。因为虽然你可以一句话补回功能,但在触摸操作下各种 Bug,你解不完……

这两种实现的窗口之间还有一些功能上的区别:

方案 WindowChrome AllowsTransparency 说明
点击穿透 在完全透明的部分点击依然点在自己的窗口上 在完全透明的部分点击会穿透到下面的其他窗口 感谢 nocanstillbb (huang bin bin) 提供的信息

10-12 2019

使用 .editorconfig 配置 .NET/C# 项目的代码分析规则的严重程度

随着 Visual Studio 2019 更新,在 Visual Studio 中编写代码的时候也带来了基于 Roslyn 的代码质量分析。有一些代码分析严重程度可能与团队约定的不一致,这时就需要配置规则的严重程度。另外如果是个人使用插件安装了分析器,也可以配置一些严重程度满足个人的喜好。

本文介绍使用 .editorconfig 文件来配置 .NET/C# 项目中,代码分析规则的严重性。可以是全局的,也可以每个项目有自己的配置。


生效范围与继承

.editorconfig 文件可以在你的项目中的任何地方,甚至是代码仓库之外。是按照文件夹结构来继承生效的。

比如我的项目结构是这样:

+ Walterlv.Demo
    + Core
        - .editorconfig
        - Foo.cs
    - .editorconfig
    - Program.cs

项目结构

那么 Foo.cs 文件的规则严重性将受 Core 文件夹中的 .editorconfig 文件管理,如果有些规则不在此文件夹的 .editorconfig 里面,就会受外层 .editorconfig 管理。

另外,你甚至可以在整个代码仓库的外部文件夹放一个 .editorconfig 文件,这样,如果项目中没有对应的规则,那么外面文件夹中的 .editorconfig 规则就会生效,这相当于间接做了一个全局生效的规则集。

.editorconfig 中的内容

.editorconfig 中的分析器严重性内容就像下面这样:

[*.cs]

# CC0097: You have missing/unexistent parameters in Xml Docs
dotnet_diagnostic.CC0097.severity = error

# CA1031: Do not catch general exception types
dotnet_diagnostic.CA1031.severity = suggestion

# IDE0051: 删除未使用的私有成员
dotnet_diagnostic.IDE0051.severity = none

对于 C# 语言的规则,在 [*.cs] 区,每个规则格式是 dotnet_diagnostic.{DiagnosticId}.severity = {Severity}

当然,我们不需要手工书写这个文件,了解它的格式只是为了出问题的时候不至于一脸懵逼。

配置严重程度

使用 Visual Studio 2019,配置规则严重性非常简单。当然,16.3 以上版本才这么简单,之前的版本步骤多一点。

配置规则严重性

在提示有问题的代码上按下重构快捷键(默认是 Ctrl + .),可以出现重构菜单,其中就有配置规则严重性的选项,直接选择即可自动添加到 .editorconfig 文件中。如果项目中没有 .editorconfig 文件,则会自动在解决方案同目录下创建一个新的。

对这部分快捷键不了解的话可以阅读:提高使用 Visual Studio 开发效率的键盘快捷键 - walterlv

10-10 2019

什么是模态窗口?本文带你了解模态窗口的本质

做 Windows 桌面应用开发的小伙伴们对“模态窗口”(Modal Dialog)一定不陌生。如果你希望在模态窗口之上做更多的事情,或者自己实现一套模态窗口类似的机制,那么你可能需要了解模态窗口的本质。

本文不会太深,只是从模态窗口一词出发,抵达大家都熟知的一些知识为止。


开发中的模态窗口

在各种系统、语言和框架中,只要有用户可以看见的界面,都存在模态窗口的概念。从交互层面来说,它的形式是在保留主界面作为环境来显示的情况下,打开一个新的界面,用户只能在新的界面上操作,完成或取消后才能返回主界面。从作用上来说,通常是要求用户必须提供某些信息后才能继续操作,或者单纯只是为了广告。

模态窗口的三个特点

如果你希望自己搞一套模态窗口出来,那么只需要满足这三点即可。你可以随便加戏但那都无关紧要。

  1. 保留主界面显示的同时,禁用主界面的用户交互;
  2. 显示子界面,主界面在子界面操作完成后返回;
  3. 当用户试图跳过子界面的交互的时候进行强提醒。

拿 Windows 系统中的模态对话框为例子,大概就像下面这两张图片这样:

有一个小的子界面盖住了主界面,要求用户必须进行选择。Windows 系统设置因为让背景变暗了,所以用户肯定会看得到需要进行的交互;而任务管理器没有让主界面变暗,所以用户在操作子界面的时候,模态窗口的边框和标题栏闪烁以提醒用户注意。

Windows 系统设置

任务管理器

实现模态窗口

对于 Windows 操作系统来说,模态窗口并不是一个单一的概念,你并不能仅通过一个 API 调用就完成显示模态窗口,你需要在不同的时机调用不同的 API 来完成一个模态窗口。如果要完整实现一个自己的模态窗口,则需要编写实现以上三个特点的代码。

当然,你可能会发现实际上你显示一个模态窗口仅仅一句话调用就够了,那是因为你所用的应用程序框架帮你完成了模态窗口的一系列机制。

关于 WPF 框架是如何实现模态窗口的,可以阅读:直击本质:WPF 框架是如何实现模态窗口的

关于如何自己实现一个跨越线程/进程边界的模态窗口,可以阅读:实现 Windows 系统上跨进程/跨线程的模态窗口

如果你希望定制以上第三个特点中强提醒的动画效果,可以阅读:WPF window 子窗口反馈效果(抖动/阴影渐变) - 唐宋元明清2188 - 博客园

API 调用

为了在 Windows 上实现模态窗口,需要一些 Win32 API 调用(当然,框架够用的话直接用框架就好)。

禁用主窗口

我们需要使用到 BOOL EnableWindow(HWND hWnd, BOOL bEnable); 来启用与禁用某个窗口。

EnableWindow(hWnd, false);
try
{
    // 模态显示一个窗口。
}
finally
{
    EnableWindow(hWnd, true);
}
[DllImport("user32")]
private static extern bool EnableWindow(IntPtr hwnd, bool bEnable);

阻塞代码等待操作完成

因为 async/await 的出现,阻塞其实可以使用 await 来实现。虽然这不是真正的阻塞,但可以真实反应出“异步”这个过程,也就是虽然这里在等待,但实际上依然能够继续在同一个线程响应用户的操作。

UWP 中的新 API 当然已经都是使用 async/await 来实现模态等待了,不过 WPF/Windows Forms 比较早,只能使用 Dispatcher 线程模型来实现模态等待。

于是我们可以考虑直接使用现成的 Dispatcher 线程模型来完成等待,方法是调用下面两个当中的任何一个:

  • Window.ShowDialog 也就是直接使用窗口原生的模态
  • Dispatcher.PushFrame 新开一个消息循环以阻塞当前代码的同时继续响应 UI 交互

上面 Window.ShowDialog 的本质也是在调用 Dispatcher.PushFrame,详见:

关于 PushFrame 新开消息循环阻塞的原理可以参考:

当然,还有其他可以新开消息循环的方法。

进行 UI 强提醒

由于我们一开始禁用了主窗口,所以如果用户试图操作主窗口是不会有效果的。然而如果用户不知道当前显示了一个模态窗口需要操作,那么给出提醒也是必要的。

简单的在 UI 上的提醒是最简单的了,比如:

  • 将主界面变暗(UWP 应用,Web 应用喜欢这么做)
  • 将主界面变模糊(iOS 应用喜欢这么做)
  • 在模态窗口上增加一个很厚重的阴影(Android 应用喜欢这么做)

然而 Windows 和 Mac OS 这些古老的系统由于兼容性负担不能随便那么改,于是需要有其他的提醒方式。

Windows 采用的方式是让标题栏闪烁,让阴影闪烁。

而这些特效的处理,来自于子窗口需要处理一些特定的消息 WM_SETCURSOR

详见:WPF window 子窗口反馈效果(抖动/阴影渐变) - 唐宋元明清2188 - 博客园

通常你不需要手工处理这些消息,但是如果你完全定制了窗口样式,则可能需要自行做一个这样的模态窗口提醒效果。

10-10 2019

直击本质:WPF 框架是如何实现模态窗口的

想知道你在 WPF 编写 Window.ShowDialog() 之后,WPF 框架是如何帮你实现模态窗口的吗?

本文就带你来了解这一些。


Window.ShowDialog

WPF 显示模态窗口的方法就是 Window.ShowDialog,因此我们直接进入这个方法查看。由于 .NET Core 版本的 WPF 已经开源,我们会使用 .NET Core 版本的 WPF 源代码。

Window.ShowDialog 的源代码可以在这里查看:

这个方法非常长,所以我只把其中与模态窗口最关键的代码和相关注释留下,其他都删除(这当然是不可编译的):

public Nullable<bool> ShowDialog()
{
    // NOTE:
    // _threadWindowHandles is created here.  This reference is nulled out in EnableThreadWindows
    // when it is called with a true parameter.  Please do not null it out anywhere else.
    // EnableThreadWindow(true) is called when dialog is going away.  Once dialog is closed and
    // thread windows have been enabled, then there no need to keep the array list around.
    // Please see BUG 929740 before making any changes to how _threadWindowHandles works.
    _threadWindowHandles = new ArrayList();
    //Get visible and enabled windows in the thread
    // If the callback function returns true for all windows in the thread, the return value is true.
    // If the callback function returns false on any enumerated window, or if there are no windows
    // found in the thread, the return value is false.
    // No need for use to actually check the return value.
    UnsafeNativeMethods.EnumThreadWindows(SafeNativeMethods.GetCurrentThreadId(),
                                            new NativeMethods.EnumThreadWindowsCallback(ThreadWindowsCallback),
                                            NativeMethods.NullHandleRef);
    //disable those windows
    EnableThreadWindows(false);

    try
    {
        _showingAsDialog = true;
        Show();
    }
    catch
    {
        // NOTE:
        // See BUG 929740.
        // _threadWindowHandles is created before calling ShowDialog and is deleted in
        // EnableThreadWindows (when it's called with true).
        //
        // Window dlg = new Window();
        // Button b = new button();
        // b.OnClick += new ClickHandler(OnClick);
        // dlg.ShowDialog();
        //
        //
        // void OnClick(...)
        // {
        //      dlg.Close();
        //      throw new Exception();
        // }
        //
        //
        // If above code is written, then we get inside this exception handler only after the dialog
        // is closed.  In that case all the windows that we disabled before showing the dialog have already
        // been enabled and _threadWindowHandles set to null in EnableThreadWindows.  Thus, we don't
        // need to do it again.
        //
        // In any other exception cases, we get in this handler before Dialog is closed and thus we do
        // need to enable all the disable windows.
        if (_threadWindowHandles != null)
        {
            // Some exception case. Re-enable the windows that were disabled
            EnableThreadWindows(true);
        }
    }
}

觉得代码还是太长?不要紧,我再简化一下:

  1. EnumThreadWindows 获取当前线程的所有窗口
  2. 把当前线程中的所有窗口都禁用掉(用的是 Win32 API 的禁用哦,这不会导致窗口内控件的样式变为禁用状态)
  3. 将窗口显示出来(如果出现异常,则还原之前禁用的窗口)

可以注意到禁用掉的窗口是“当前线程”的哦。

ShowHelper

接下来的重点方法是 Window.ShowDialog 中的那句 Show()。在 Show() 之前设置了 _showingAsDialogtrue,于是这里会调用 ShowHelper 方法并传入 true

下面的代码也是精简后的 ShowHelper 方法:

private object ShowHelper(object booleanBox)
{
    try
    {
        // tell users we're going modal
        ComponentDispatcher.PushModal();

        _dispatcherFrame = new DispatcherFrame();
        Dispatcher.PushFrame(_dispatcherFrame);
    }
    finally
    {
        // tell users we're going non-modal
        ComponentDispatcher.PopModal();
    }
}

可以看到,重点是 PushModalPopModal 以及 PushFrame

PushFrame 的效果就是让调用 ShowDialog 的代码看起来就像阻塞了一样(实际上就是阻塞了,只不过开了新的消息循环看起来 UI 不卡)。

关于 PushFrame 为什么能够“阻塞”你的代码的同时还能继续响应 UI 操作的原理,可以阅读:

那么 ComponentDispatcher.PushModalComponentDispatcher.PopModal 呢?可以在这里(ComponentDispatcherThread.cs)看它的代码,实际上是为了模态计数以及引发事件的,对模态的效果没有本质上的影响。

10-10 2019

.NET/C# 检测电脑上安装的 .NET Framework 的版本

如果你希望知道某台计算机上安装了哪些版本的 .NET Framework,那么正好本文可以帮助你解决问题。


如何找到已安装的 .NET Framework

有的电脑的 .NET Framework 是自带的,有的是操作系统自带的。这样,你就不能通过控制面板的“卸载程序”去找到到底安装了哪个版本的 .NET Framework 了。

关于各个版本 Windows 10 上自带的 .NET Framework 版本,可以阅读 各个版本 Windows 10 系统中自带的 .NET Framework 版本 - walterlv

而如果通过代码 Environment.Version 来获取 .NET 版本,实际上获取的是 CLR 的版本,详见 使用 PowerShell 获取 CLR 版本号 - walterlv

这些版本号是不同的,详见 .NET Framework 4.x 程序到底运行在哪个 CLR 版本之上 - walterlv

那么如何获取已安装的 .NET Framework 的版本呢?最靠谱的方法竟然是通过读取注册表。

注册表位置和含义

读取位置在这里:

计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\2052

注册表位置

而唯一准确能够判定 .NET Framework 版本的,只有里面的 Release 值。但可惜的是,这个值并不能直接看出来到底是 4.5 还是 4.8。我们需要有一张对应表。

我把它整理成了字典和注释,这样会比较容易理解每个编号对应的 .NET Framework 版本代号。

/// <summary>
/// 获取 .NET Framework 4.5 及以上版本的发行号与版本名称的对应关系。
/// 4.5 及以下版本没有这样的对应关系。
/// </summary>
private static readonly Dictionary<int, string> ReleaseToNameDictionary = new Dictionary<int, string>
{
    // .NET Framework 4.5
    { 378389, "4.5" },
    // .NET Framework 4.5.1(Windows 8.1 或 Windows Server 2012 R2 自带)
    { 378675, "4.5.1" },
    // .NET Framework 4.5.1(其他系统安装)
    { 378758, "4.5.1" },
    // .NET Framework 4.5.2
    { 379893, "4.5.2" },
    // .NET Framework 4.6(Windows 10 第一个版本 1507 自带)
    { 393295, "4.6" },
    // .NET Framework 4.6(其他系统安装)
    { 393297, "4.6" },
    // .NET Framework 4.6.1(Windows 10 十一月更新 1511 自带)
    { 394254, "4.6.1" },
    // .NET Framework 4.6.1(其他系统安装)
    { 394271, "4.6.1" },
    // .NET Framework 4.6.2(Windows 10 一周年更新 1607 和 Windows Server 2016 自带)
    { 394802, "4.6.2" },
    // .NET Framework 4.6.2(其他系统安装)
    { 394806, "4.6.2" },
    // .NET Framework 4.7(Windows 10 创造者更新 1703 自带)
    { 460798, "4.7" },
    // .NET Framework 4.7(其他系统安装)
    { 460805, "4.7" },
    // .NET Framework 4.7.1(Windows 10 秋季创造者更新 1709 和 Windows Server 1709 自带)
    { 461308, "4.7.1" },
    // .NET Framework 4.7.1(其他系统安装)
    { 461310, "4.7.1" },
    // .NET Framework 4.7.2(Windows 10 2018年四月更新 1803 和 Windows Server 1803 自带)
    { 461808, "4.7.2" },
    // .NET Framework 4.7.2(其他系统安装)
    { 461814, "4.7.2" },
    // .NET Framework 4.8(Windows 10 2019年五月更新 1903 自带)
    { 528040, "4.8" },
    // .NET Framework 4.8(其他系统安装)
    { 528049, "4.8" },
};

另外,还有一些值也是有意义的(只是不那么精确):

  • 主版本
    • 也就是可以共存的版本,比如 v3.5 系列和 v4 系列就是可以共存的,它们分别是就地更新的保持兼容的版本
  • 发行版本名称
    • 完整版 Full 和精简版 Client
  • 版本号
    • 比如 3.5.30729.4926 或者 4.7.02556
  • 服务包版本
    • 古时候的微软喜欢用 SP1 SP2 来命名同一个版本的多次更新,这也就是那个年代的产物

它们分别在注册表的这些位置:

  • 主版本
    • 计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP 里项的名称
  • 发行版本名称
    • 以上项里子项的名称
  • 版本号
    • 以上项里的 Version
  • 服务包版本
    • 以上项里的 SP

读取注册表

在上面已经梳理了读取注册表的位置之后,相信你可以很容易写出读取已安装 .NET Framework 版本的代码出来。

我已经将其做成了 NuGet 源代码包(使用 SourceYard 打包),你可以安装 NuGet 包来获得读取已安装 .NET Framework 版本的功能:

或者在 GitHub 查看源代码:

只有一个类型——NdpInfo

使用方法有两种。

第一种,获取当前计算机上所有已经安装的 .NET Framework 版本:

var allVersions = await NdpInfo.ReadFromRegistryAsync();

执行完成之后看看得到的字典 allVersions 如下:

已安装的全部 .NET Framework

字典里 Key 是不能共存的主版本,Value 是这个主版本里当前已经安装的具体版本信息。

如果直接使用 ToString(),是可以生成我们平时经常在各大文档或者社区使用的 .NET Framework 的名称。

第二种,获取当前已安装的最新的 .NET Framework 版本名称:

var currentVersion = NdpInfo.GetCurrentVersionName();

这可以直接获取到一个字符串,比如 .NET Framework 4.8。对于只是简单获取一下已安装名称而不用做更多处理的程序来说会比较方便。

10-10 2019

Windows 系统上用 .NET/C# 查找所有窗口,并获得窗口的标题、位置、尺寸、最小化、可见性等各种状态

在 Windows 应用开发中,如果需要操作其他的窗口,那么可以使用 EnumWindows 这个 API 来枚举这些窗口。

你可以使用本文编写的一个类型,查找到所有窗口中你关心的信息。


需要使用的 API

枚举所有窗口仅需要使用到 EnumWindows,其中需要定义一个委托 WndEnumProc 作为传入参数的类型。

剩下的我们需要其他各种方法用于获取窗口的其他属性。

private delegate bool WndEnumProc(IntPtr hWnd, int lParam);

[DllImport("user32")]
private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam);

[DllImport("user32")]
private static extern IntPtr GetParent(IntPtr hWnd);

[DllImport("user32")]
private static extern bool IsWindowVisible(IntPtr hWnd);

[DllImport("user32")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount);

[DllImport("user32")]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

[DllImport("user32")]
private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect);

[StructLayout(LayoutKind.Sequential)]
private readonly struct LPRECT
{
    public readonly int Left;
    public readonly int Top;
    public readonly int Right;
    public readonly int Bottom;
}

枚举所有窗口

我将以上 API 封装成 FindAll 函数,并提供过滤器可以给大家过滤众多的窗口使用。

比如,我写了下面一个简单的示例,可以输出当前可见的所有窗口以及其位置和尺寸:

using System;

namespace Walterlv.WindowDetector
{
    class Program
    {
        static void Main(string[] args)
        {
            var windows = WindowEnumerator.FindAll();
            for (int i = 0; i < windows.Count; i++)
            {
                var window = windows[i];
                Console.WriteLine($@"{i.ToString().PadLeft(3, ' ')}. {window.Title}
     {window.Bounds.X}, {window.Bounds.Y}, {window.Bounds.Width}, {window.Bounds.Height}");
            }
            Console.ReadLine();
        }
    }
}

获取所有窗口以及其位置和尺寸

这里的 FindAll 方法,我提供了一个默认参数,可以指定如何过滤所有枚举到的窗口。如果不指定,则会找可见的,包含标题的,没有最小化的窗口。如果你希望找一些看不见的窗口,可以自己写过滤条件。

什么都不要过滤的话,就传入 _ => true,意味着所有的窗口都会被枚举出来。

附源码

因为源代码会经常更新,所以建议在这里查看:

无法访问的话,可以看下面:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;

namespace Walterlv.WindowDetector
{
    /// <summary>
    /// 包含枚举当前用户空间下所有窗口的方法。
    /// </summary>
    public class WindowEnumerator
    {
        /// <summary>
        /// 查找当前用户空间下所有符合条件的窗口。如果不指定条件,将仅查找可见窗口。
        /// </summary>
        /// <param name="match">过滤窗口的条件。如果设置为 null,将仅查找可见窗口。</param>
        /// <returns>找到的所有窗口信息。</returns>
        public static IReadOnlyList<WindowInfo> FindAll(Predicate<WindowInfo> match = null)
        {
            var windowList = new List<WindowInfo>();
            EnumWindows(OnWindowEnum, 0);
            return windowList.FindAll(match ?? DefaultPredicate);

            bool OnWindowEnum(IntPtr hWnd, int lparam)
            {
                // 仅查找顶层窗口。
                if (GetParent(hWnd) == IntPtr.Zero)
                {
                    // 获取窗口类名。
                    var lpString = new StringBuilder(512);
                    GetClassName(hWnd, lpString, lpString.Capacity);
                    var className = lpString.ToString();

                    // 获取窗口标题。
                    var lptrString = new StringBuilder(512);
                    GetWindowText(hWnd, lptrString, lptrString.Capacity);
                    var title = lptrString.ToString().Trim();

                    // 获取窗口可见性。
                    var isVisible = IsWindowVisible(hWnd);

                    // 获取窗口位置和尺寸。
                    LPRECT rect = default;
                    GetWindowRect(hWnd, ref rect);
                    var bounds = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);

                    // 添加到已找到的窗口列表。
                    windowList.Add(new WindowInfo(hWnd, className, title, isVisible, bounds));
                }

                return true;
            }
        }

        /// <summary>
        /// 默认的查找窗口的过滤条件。可见 + 非最小化 + 包含窗口标题。
        /// </summary>
        private static readonly Predicate<WindowInfo> DefaultPredicate = x => x.IsVisible && !x.IsMinimized && x.Title.Length > 0;

        private delegate bool WndEnumProc(IntPtr hWnd, int lParam);

        [DllImport("user32")]
        private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam);

        [DllImport("user32")]
        private static extern IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32")]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        [DllImport("user32")]
        private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount);

        [DllImport("user32")]
        private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

        [DllImport("user32")]
        private static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);

        [DllImport("user32")]
        private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect);

        [StructLayout(LayoutKind.Sequential)]
        private readonly struct LPRECT
        {
            public readonly int Left;
            public readonly int Top;
            public readonly int Right;
            public readonly int Bottom;
        }
    }

    /// <summary>
    /// 获取 Win32 窗口的一些基本信息。
    /// </summary>
    public readonly struct WindowInfo
    {
        public WindowInfo(IntPtr hWnd, string className, string title, bool isVisible, Rectangle bounds) : this()
        {
            Hwnd = hWnd;
            ClassName = className;
            Title = title;
            IsVisible = isVisible;
            Bounds = bounds;
        }

        /// <summary>
        /// 获取窗口句柄。
        /// </summary>
        public IntPtr Hwnd { get; }

        /// <summary>
        /// 获取窗口类名。
        /// </summary>
        public string ClassName { get; }

        /// <summary>
        /// 获取窗口标题。
        /// </summary>
        public string Title { get; }

        /// <summary>
        /// 获取当前窗口是否可见。
        /// </summary>
        public bool IsVisible { get; }

        /// <summary>
        /// 获取窗口当前的位置和尺寸。
        /// </summary>
        public Rectangle Bounds { get; }

        /// <summary>
        /// 获取窗口当前是否是最小化的。
        /// </summary>
        public bool IsMinimized => Bounds.Left == -32000 && Bounds.Top == -32000;
    }
}

10-10 2019

在 WPF 程序中应用 Windows 10 真•亚克力效果

从 Windows 10 (1803) 开始,Win32 应用也可以有 API 来实现原生的亚克力效果了。不过相比于 UWP 来说,可定制性会差很多。

本文介绍如何在 WPF 程序中应用 Windows 10 真•亚克力效果。(而不是一些流行的项目里面自己绘制的亚克力效果。)


API

需要使用的 API 是微软的文档中并未公开的 SetWindowCompositionAttribute

我在另一篇博客中有介绍此 API 各种用法的效果,详见:

当然,使用此 API 也可以做 Windows 10 早期的模糊效果,比如:

如何使用

为了方便地让你的窗口获得亚克力效果,我做了两层不同的 API:

  1. AcrylicBrush 当然,受到 Win32 启用亚克力效果的限制,只能在窗口上设置此属性
  2. WindowAccentCompositor 用于更多地控制窗口与系统的叠加组合效果

代码请参见:

亚克力效果在 WPF 程序中

注意事项

要使得亚克力效果可以生效,需要:

  1. 设置一个混合色 GradientColor
  2. 混合色不能是全透明(如果全透明,窗口的亚克力部分就全透明穿透了),当然也不能全不透明,这样就看不到亚克力效果了。

参考资料

10-10 2019

使用 SetWindowCompositionAttribute 来控制程序的窗口边框和背景(可以做 Acrylic 亚克力效果、模糊效果、主题色效果等)

Windows 系统中有一个没什么文档的 API,SetWindowCompositionAttribute,可以允许应用的开发者将自己窗口中的内容渲染与窗口进行组合。这可以实现很多系统中预设的窗口特效,比如 Windows 7 的毛玻璃特效,Windows 8/10 的前景色特效,Windows 10 的模糊特效,以及 Windows 10 1709 的亚克力(Acrylic)特效。而且这些组合都发生在 dwm 进程中,不会额外占用应用程序的渲染性能。

本文介绍 SetWindowCompositionAttribute 可以实现的所有效果。你可以通过阅读本文了解到与系统窗口可以组合渲染到哪些程度。


试验用的源代码

本文将创建一个简单的 WPF 程序来验证 SetWindowCompositionAttribute 能达到的各种效果。你也可以不使用 WPF,得到类似的效果。

简单的项目文件结构是这样的:

  • [项目] Walterlv.WindowComposition
    • App.xaml
    • App.xaml.cs
    • MainWindow.xaml
    • MainWindow.xaml.cs
    • WindowAccentCompositor

其中,App.xaml 和 App.xaml.cs 保持默认生成的不动。

为了验证此 API 的效果,我需要将 WPF 主窗口的背景色设置为纯透明或者 null,而设置 ControlTemplate 才能彻彻底底确保所有的样式一定是受我们自己控制的,我们在 ControlTemplate 中没有指定任何可以显示的内容。MainWindow.xaml 的全部代码如下:

<Window x:Class="Walterlv.WindowComposition.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="欢迎访问吕毅的博客:blog.walterlv.com" Height="450" Width="800">
    <Window.Template>
        <ControlTemplate TargetType="Window">
            <AdornerDecorator>
                <ContentPresenter />
            </AdornerDecorator>
        </ControlTemplate>
    </Window.Template>
    <!-- 我们注释掉 WindowChrome,是因为即将验证 WindowChrome 带来的影响。 -->
    <!--<WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="-1" />
    </WindowChrome.WindowChrome>-->
    <Grid>
    </Grid>
</Window>

而 MainWindow.xaml.cs 中,我们简单调用一下我们即将写的调用 SetWindowCompositionAttribute 的类型。

using System.Windows;
using System.Windows.Media;
using Walterlv.Windows.Effects;

namespace Walterlv.WindowComposition
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var compositor = new WindowAccentCompositor(this);
            compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
        }
    }
}

还剩下一个 WindowAccentCompositor.cs 文件,因为比较长放到博客里影响阅读,所以建议前往这里查看:

而其中对我们最终渲染效果有影响的就是 AccentPolicy 类型的几个属性。其中 AccentState 属性是下面这个枚举,而 GradientColor 将决定窗口渲染时叠加的颜色。

private enum AccentState
{
    ACCENT_DISABLED = 0,
    ACCENT_ENABLE_GRADIENT = 1,
    ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
    ACCENT_ENABLE_BLURBEHIND = 3,
    ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
    ACCENT_INVALID_STATE = 5,
}

影响因素

经过试验,对最终显示效果有影响的有这些:

  • 选择的 AccentState 枚举值
  • 使用的 GradientColor 叠加色
  • 是否使用 WindowChrome 让客户区覆盖非客户区
  • 目标操作系统(Windows 7/8/8.1/10)

使用 WindowChrome,你可以用你自己的 UI 覆盖掉系统的 UI 窗口样式。关于 WindowChrome 让客户区覆盖非客户区的知识,可以阅读:

需要注意的是,WindowChromeGlassFrameThickness 属性可以设置窗口边框的粗细,设置为 0 将导致窗口没有阴影,设置为负数将使得整个窗口都是边框。

排列组合

我们依次来看看效果。

AccentState=ACCENT_DISABLED

使用 ACCENT_DISABLED 时,GradientColor 叠加色没有任何影响,唯一影响渲染的是 WindowChrome 和操作系统。


不使用 WindowChrome,在 Windows 10 上:

without WindowChrome in Windows 10


不使用 WindowChrome 在 Windows 7 上:

without WindowChrome in Windows 7


在 Windows 10 上,使用 WindowChrome

<WindowChrome.WindowChrome>
    <WindowChrome />
</WindowChrome.WindowChrome>

with WindowChrome in Windows 10


在 Windows 7 上,使用 WindowChrome

with WindowChrome in Windows 7

当然,以上边框比较细,跟系统不搭,可以设置成其他值:

bold thickness WindowChrome in Windows 7


在 Windows 10 上,使用 WindowChrome 并且 GlassFrameThickness 设置为 -1

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>

-1 glass frame in Windows 10


而在 Windows 7 上,这就是非常绚丽的全窗口的 Aero 毛玻璃特效:

-1 glass frame in Windows 7

AccentState=ACCENT_ENABLE_GRADIENT

使用 ACCENT_DISABLED 时,GradientColor 叠加色会影响到最终的渲染效果。

还记得我们前面叠加的颜色是什么吗?

叠加的颜色

接下来别忘了然后把它误以为是我系统的主题色哦!


不使用 WindowChrome,在 Windows 10 上:

without WindowChrome in Windows 10

另外,你会注意到左、下、右三个方向上边框会深一些。那是 Windows 10 的窗口阴影效果,因为实际上 Windows 10 叠加的阴影也是窗口区域的一部分,只是一般人看不出来而已。我们叠加了颜色之后,这里就露馅儿了。

另外,这个颜色并不是我们自己的进程绘制的哦,是 dwm 绘制的颜色。

如果不指定 GradientColor 也就是保持为 0,你将看到上面绿色的部分全是黑色的;嗯,包括阴影的部分……

without WindowChrome in Windows 10 - default gradient color


不使用 WindowChrome 在 Windows 7 上:

without WindowChrome in Windows 7

可以看出,在 Windows 7 上,GradientColor 被无视了。


而使用 WindowChrome 在 Windows 10 上,则可以得到整个窗口的叠加色:

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="16 48 16 16" />
</WindowChrome.WindowChrome>

with WindowChrome in Windows 10

可以注意到,窗口获得焦点的时候,整个窗口都是叠加色;而窗口失去焦点的时候,指定了边框的部分颜色会更深(换其他颜色叠加可以看出来是叠加了半透明黑色)。

如果你希望失去焦点的时候,边框部分不要变深,请将边框设置为 -1

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>

使用 WindowChrome 在 Windows 7 上,依然没有任何叠加色的效果:

with WindowChrome in Windows 7

AccentState=ACCENT_ENABLE_TRANSPARENTGRADIENT

使用 ACCENT_ENABLE_TRANSPARENTGRADIENT 时,GradientColor 叠加色没有任何影响,唯一影响渲染的是 WindowChrome 和操作系统。


不使用 WindowChrome,在 Windows 10 上:

without WindowChrome frame in Windows 10

依然左、下、右三个方向上边框会深一些,那是 Windows 10 的窗口阴影效果。


不使用 WindowChrome 在 Windows 7 上:

without WindowChrome in Windows 7

GradientColor 也是被无视的,而且效果跟之前一样。


使用 WindowChrome 在 Windows 10 上,在获得焦点的时候整个背景是系统主题色;而失去焦点的时候是灰色,但边框部分是深色。

with WindowChrome frame in Windows 10

依然可以将边框设置为 -1 使得边框不会变深:

with WindowChrome in Windows 10


使用 WindowChrome 在 Windows 7 上,依然是老样子:

with WindowChrome in Windows 7

AccentState=ACCENT_ENABLE_BLURBEHIND

ACCENT_ENABLE_BLURBEHIND 可以在 Windows 10 上做出模糊效果,就跟 Windows 10 早期版本的模糊效果是一样的。你可以看我之前的一篇博客,那时亚克力效果还没出来:

使用 ACCENT_ENABLE_BLURBEHIND 时,GradientColor 叠加色没有任何影响,唯一影响渲染的是 WindowChrome 和操作系统。


在 Windows 10 上,没有使用 WindowChrome

模糊效果

你可能需要留意一下那个“诡异”的模糊范围,你会发现窗口的阴影外侧也是有模糊的!!!你能忍吗?肯定不能忍,所以还是乖乖使用 WindowChrome 吧!


在 Windows 7 上,没有使用 WindowChrome,效果跟其他值一样,依然没有变化:

without WindowChrome in Windows 7


在 Windows 10 上,使用 WindowChrome

with WindowChrome in Windows 10


使用 WindowChrome 在 Windows 7 上,依然是老样子:

with WindowChrome in Windows 7

AccentState=ACCENT_ENABLE_ACRYLICBLURBEHIND

从 Windows 10 (1803) 开始,Win32 程序也能添加亚克力效果了,因为 SetWindowCompositionAttribute 的参数枚举新增了 ACCENT_ENABLE_ACRYLICBLURBEHIND

亚克力效果相信大家不陌生,那么在 Win32 应用程序里面使用的效果是什么呢?


不使用 WindowChrome,在 Windows 10 上:

without WindowChrome in Windows 10

咦!等等!这不是跟之前一样吗?


嗯,下面就是不同了,亚克力效果支持与半透明的 GradientColor 叠加,所以我们需要将传入的颜色修改为半透明:

    var compositor = new WindowAccentCompositor(this);
--  compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++  compositor.Composite(Color.FromArgb(0x3f, 0x18, 0xa0, 0x5e));

acrylic without WindowChrome


那么如果改为全透明会怎么样呢?

不幸的是,完全没有效果!!!

    var compositor = new WindowAccentCompositor(this);
--  compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++  compositor.Composite(Color.FromArgb(0x00, 0x18, 0xa0, 0x5e));

no acrylic without WindowChrome


接下来是使用 WindowChrome 时:

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="16 48 16 16" />
</WindowChrome.WindowChrome>

acrylic with WindowChrome frame

然而周围有一圈偏白色的渐变是什么呢?那个其实是 WindowChrome 设置的边框白,被亚克力效果模糊后得到的混合效果。


所以,如果要获得全窗口的亚克力效果,请将边框设置成比较小的值:

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="0 1 0 0" />
</WindowChrome.WindowChrome>

acrylic with thin WindowChrome frame


记得不要像前面的那些效果一样,如果设置成 -1,你将获得纯白色与设置的 Gradient 叠加色的亚克力特效,是个纯色:

acrylic with WindowChrome -1 frame


你可以将叠加色的透明度设置得小一些,这样可以看出叠加的颜色:

    var compositor = new WindowAccentCompositor(this);
--  compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++  compositor.Composite(Color.FromArgb(0xa0, 0x18, 0xa0, 0x5e));

acrylic with darker gradient color


那么可以设置为全透明吗?

    var compositor = new WindowAccentCompositor(this);
--  compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++  compositor.Composite(Color.FromArgb(0x00, 0x18, 0xa0, 0x5e));

很不幸,最终你会完全看不到亚克力效果,而变成了毫无特效的透明窗口:

acrylic with transparent gradient color

最上面那根白线,是我面前面设置边框为 0 1 0 0 导致的。


如果在这种情况下,将边框设置为 0 会怎样呢?记得前面我们说过的吗,会导致阴影消失哦!

呃……你将看到……这个……

什么都没有……

acrylic with zero WindowChrome frame thickness

是不是找到了一条新的背景透明异形窗口的方法?

还是省点心吧,亚克力效果在 Win32 应用上的性能还是比较堪忧的……

想要背景透明,请参见:


不用考虑 Windows 7,因为大家都知道不支持。实际效果会跟前面的一模一样。

AccentState=ACCENT_INVALID_STATE

这个值其实不用说了,因为 AccentState 在不同系统中可用的值不同,为了保证向后兼容性,对于新系统中设置的值,旧系统其实就视之为 ACCENT_INVALID_STATE

那么如果系统认为设置的是 ACCENT_INVALID_STATE 会显示成什么样子呢?

答案是,与 ACCENT_DISABLED 完全相同。

总结

由于 Windows 7 上所有的值都是同样的效果,所以下表仅适用于 Windows 10。

  效果
ACCENT_DISABLED 黑色(边框为纯白色)
ACCENT_ENABLE_GRADIENT GradientColor 颜色(失焦后边框为深色)
ACCENT_ENABLE_TRANSPARENTGRADIENT 主题色(失焦后边框为深色)
ACCENT_ENABLE_BLURBEHIND 模糊特效(失焦后边框为灰色)
ACCENT_ENABLE_ACRYLICBLURBEHIND 与 GradientColor 叠加颜色的亚克力特效
ACCENT_INVALID_STATE 黑色(边框为纯白色)

在以上的特效之下,WindowChrome 可以让客户区覆盖非客户区,或者让整个窗口都获得特效,而不只是标题栏。

附源代码

请参见 GitHub 地址以获得最新代码。如果不方便访问,那么就看下面的吧。

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;

namespace Walterlv.Windows.Effects
{
    /// <summary>
    /// 为窗口提供模糊特效。
    /// </summary>
    public class WindowAccentCompositor
    {
        private readonly Window _window;

        /// <summary>
        /// 创建 <see cref="WindowAccentCompositor"/> 的一个新实例。
        /// </summary>
        /// <param name="window">要创建模糊特效的窗口实例。</param>
        public WindowAccentCompositor(Window window) => _window = window ?? throw new ArgumentNullException(nameof(window));

        public void Composite(Color color)
        {
            Window window = _window;
            var handle = new WindowInteropHelper(window).EnsureHandle();

            var gradientColor =
                // 组装红色分量。
                color.R << 0 |
                // 组装绿色分量。
                color.G << 8 |
                // 组装蓝色分量。
                color.B << 16 |
                // 组装透明分量。
                color.A << 24;

            Composite(handle, gradientColor);
        }

        private void Composite(IntPtr handle, int color)
        {
            // 创建 AccentPolicy 对象。
            var accent = new AccentPolicy
            {
                AccentState = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND,
                GradientColor = 0,
            };

            // 将托管结构转换为非托管对象。
            var accentPolicySize = Marshal.SizeOf(accent);
            var accentPtr = Marshal.AllocHGlobal(accentPolicySize);
            Marshal.StructureToPtr(accent, accentPtr, false);

            // 设置窗口组合特性。
            try
            {
                // 设置模糊特效。
                var data = new WindowCompositionAttributeData
                {
                    Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
                    SizeOfData = accentPolicySize,
                    Data = accentPtr,
                };
                SetWindowCompositionAttribute(handle, ref data);
            }
            finally
            {
                // 释放非托管对象。
                Marshal.FreeHGlobal(accentPtr);
            }
        }

        [DllImport("user32.dll")]
        private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);

        private enum AccentState
        {
            ACCENT_DISABLED = 0,
            ACCENT_ENABLE_GRADIENT = 1,
            ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
            ACCENT_ENABLE_BLURBEHIND = 3,
            ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
            ACCENT_INVALID_STATE = 5,
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct AccentPolicy
        {
            public AccentState AccentState;
            public int AccentFlags;
            public int GradientColor;
            public int AnimationId;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WindowCompositionAttributeData
        {
            public WindowCompositionAttribute Attribute;
            public IntPtr Data;
            public int SizeOfData;
        }

        private enum WindowCompositionAttribute
        {
            // 省略其他未使用的字段
            WCA_ACCENT_POLICY = 19,
            // 省略其他未使用的字段
        }
    }
}

09-27 2019

推荐几款连字字体,在代码编辑器中启用连字字体(Visual Studio Code)

启用转为编程设计的连字字体,可以给你的变成带来不一样的体验。


连字字体

微软随 Windows Terminal 设计了一款新的字体 Cascadia Code,而这是一款连字字体。

你可以看到,在 Windows Terminal 的终端中,=> == != 符号显示成了更容易理解的连字符号:

Cascadia Code

在 Cascadia Code 发布之前,Fira Code 是一款特别火的连字字体,下面是 Fira Code 连字字体在 Visual Studio Code 中的显示效果:

Fira Code in Visual Studio Code

而显示的,其实是下面这一段代码:

x =>
{
    if (x >= 2 || x == 0)
    {
        Console.WriteLine(" >=> 欢迎访问吕毅的博客 ~~> blog.walterlv.com");
    }
}

连字字体推荐

作为微软的粉丝,当然首推 Cascadia Code!不过我喜欢比较细的字体风格,目前 Cascadia Code 还没有提供细体,因此我可能还需要等一些时间才正式入坑。

在这里可以关注 Cascadia Code 的状态:

灵台,你也可以在这里找到其他一些好看的用于编程的连字字体:

相关的开源项目链接:

以 Fira Code 为例安装的话,去它的 GitHub 的 release 页面:

下载最新的发布文件 FiraCode_1.207.zip

下载解压后,你会看到五个不同的文件夹,这是四种不同的字体类型:

  • otf (Open Type)
  • ttf (True Type)
  • variable_ttf (Variable True Type)
  • woff (Web Open Font Format)
  • woff2 (Web Open Font Format)

对于 Open Type 和 True Type 的选择,一般有对应的 Open Type 类型字体的时候就优先选择 Open Type 类型的,因为 True Type 格式是比较早期的,限制比较多,比如字符的数量受到限制,而 Open Type 是基于 Unicode 字符集来设计的新的跨平台的字体格式。

Variable True Type 是可以无极变换的 True Type 字体。

而 Web Open Font Format 主要为网络传输优化,其特点是字体均经过压缩,其大小会比较小。

我们点击进入 otf 文件夹,然后全选所有的字体文件,右键,安装,等待安装完成即可。

在编辑器中启用

在 Visual Studio Code 中启用

在 Visual Studio Code 中启用连字字体需要用到两个选项:

"editor.fontFamily": "Fira Code Light, Consolas, Microsoft YaHei",
"editor.fontLigatures": true,

打开 Visual Studio Code 设置

然后点击新打开的标签右上角的 {} 图标以打开 json 形式编辑的设置:

使用 json 编辑设置

然后修改把上面两个设置增加或替换进去即可。下面是我的设置的部分截图:

设置启用连字字体

在 Visual Studio 或其他 Windows 系统自带软件中启用

只需要将字体设置成 Fira Code 即可。


参考资料

09-24 2019

如何在 Visual Studio 2019 中设置使用 .NET Core SDK 的预览版(全局生效)

.NET Core 3 相比于 .NET Core 2 是一个大更新。也正因为如此,即便它长时间处于预览版尚未发布的状态,大家也一直在使用。

Visual Studio 2019 中提供了使用 .NET Core SDK 预览版的开关。但几个更新的版本其开关的位置不同,本文将介绍在各个版本中的位置,方便你找到然后设置。


Visual Studio 2019 (16.3 及以上)

.NET Core 3.0 已经发布,下载地址:

Visual Studio 16.3 与 .NET Core 3.0 正式版同步发布,因此不再需要 .NET Core 3.0 的预览版设置界面。你只需要安装正式版 .NET Core SDK 即可。

Visual Studio 2019 (16.2)

从 Visual Studio 2019 的 16.2 版本,.NET Core 预览版的设置项的位置在:

  • 工具 -> 选项
  • 环境 -> 预览功能 -> Use previews of the .NET Core SDK (需要 restart)

Visual Studio 2019 16.2 的设置位置

如果你是英文版的 Visual Studio,也可以参考英文版:

  • Tools -> Options
  • Environment -> Preview Features -> Use previews of the .NET Core SDK (requires restart)

Option location of Visual Studio 2019 16.2

Visual Studio 2019 (16.1)

从 Visual Studio 2019 的 16.1 版本,.NET Core 预览版的设置项的位置在:

  • 工具 -> 选项
  • 环境 -> 预览功能 -> 使用 .NET Core SDK 的预览

Visual Studio 2019 16.1 的设置位置

如果你是英文版的 Visual Studio,也可以参考英文版:

  • Tools -> Options
  • Environment -> Preview Features -> Use previews of the .NET Core SDK

Option location of Visual Studio 2019 16.1

Visual Studio 2019 (16.0 和早期预览版)

在 Visual Studio 2019 的早期,.NET Core 在设置中是有一个专用的选项的,在这里:

  • 工具 -> 选项
  • 项目和解决方案 -> .NET Core -> 使用 .NET Core SDK 预览版

Visual Studio 2019 16.0 的设置位置

如果你是英文版的 Visual Studio,也可以参考英文版:

  • Tools -> Options
  • Projects and solutions -> .NET Core -> Use previews of the .NET Core SDK

Option location of Visual Studio 2019 16.0

关于全局配置

Visual Studio 2019 中此对于 .NET Core SDK 的预览版的设置是全局生效的。

也就是说,你在 Visual Studio 2019 中进行了此设置,在命令行中使用 MSBuild 或者 dotnet build 命令进行编译也会使用这样的设置项。

那么这个全局的设置项在哪个地方呢?是如何全局生效的呢?可以阅读我的其他博客:

09-19 2019

WPF 程序如何移动焦点到其他控件

WPF 中可以使用 UIElement.Focus() 将焦点设置到某个特定的控件,也可以使用 TraversalRequest 仅仅移动焦点。本文介绍如何在 WPF 程序中控制控件的焦点。


UIElement.Focus

仅仅需要在任何一个控件上调用 Focus() 方法即可将焦点设置到这个控件上。

但是需要注意,要使 Focus() 能够工作,这个元素必须满足两个条件:

  • Focusable 设置为 true
  • IsVisibletrue

TraversalRequest

如果你并不是将焦点设置到某个特定的控件,而是希望将焦点转移,可以考虑使用 TraversalRequest 类。

比如,以下代码是将焦点转移到下一个控件,也就是按下 Tab 键时焦点会去的控件。

var traversalRequest = new TraversalRequest(FocusNavigationDirection.Next);
// view 是可视化树中的一个控件。
view.MoveFocus(traversalRequest);

关于逻辑焦点和键盘焦点

键盘焦点就是你实际上按键输入和快捷键会生效的焦点,也就是当前正在工作的控件的焦点。

而 WPF 有多个焦点范围(Focus Scope),按下 Tab 键切换焦点的时候只会在当前焦点范围切焦点,不会跨范围。那么一旦跨范围切焦点的时候,焦点会去哪里呢?答案是逻辑焦点。

每个焦点范围内都有一个逻辑焦点,记录如果这个焦点范围一旦获得焦点后应该在哪个控件获得键盘焦点。

比如默认情况下 WPF 每个 Window 就是一个焦点范围,那么每个 Window 中的当前焦点就是逻辑焦点。而一旦这个 Window 激活,那么这个窗口中的逻辑焦点就会成为键盘焦点,另一个窗口当中的逻辑焦点保留,而键盘焦点则丢失。

跨窗口/跨进程切换焦点

参见我的另一篇博客:


参考资料

09-19 2019

使用 SetParent 制作父子窗口的时候,如何设置子窗口的窗口样式以避免抢走父窗口的焦点

制作传统 Win32 程序以及 Windows Forms 程序的时候,一个用户看起来独立的窗口本就是通过各种父子窗口嵌套完成的,有大量窗口句柄,窗口之间形成父子关系。不过,对于 WPF 程序来说,一个独立的窗口实际上只有一个窗口句柄,窗口内的所有内容都是 WPF 绘制的。

如果你不熟悉 Win32 窗口中的父子窗口关系和窗口样式,那么很有可能遇到父子窗口之间“抢夺焦点”的问题,本文介绍如何解决这样的问题。


“抢夺焦点”

下图中的上下两个部分是两个不同的窗口,他们之间通过 SetParent 建立了父子关系。

注意看下面的窗口标题栏,当我在这些不同区域间点击的时候,窗口标题栏在黑色和灰色之间切换:

抢夺焦点

这说明当子窗口获得焦点的时候,父窗口会失去焦点并显示失去焦点的样式。

你可以在这篇博客中找到一个简单的例子:

解决办法

而原因和解决方法仅有一个,就是子窗口需要有一个子窗口的样式。

具体来说,子窗口必须要有 WS_CHILD 样式。

你可以看看 Spyxx.exe 抓出来的默认普通窗口和子窗口的样式差别:

![默认普通窗口]](/static/posts/2019-09-19-10-21-31.png)

▲ 默认普通窗口

子窗口

▲ 子窗口


参考资料

09-18 2019

.NET/C# 利用 Walterlv.WeakEvents 高性能地定义和使用弱事件

弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。

本文介绍 Walterlv.WeakEvents 库来定义和使用弱事件。


系列博客:

下载安装 Walterlv.WeakEvents

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

定义弱事件

现在,定义弱事件就不能直接写 event EventHandler Bar 了,要像下面这样写:

using System;
using Walterlv.WeakEvents;

namespace Walterlv.Demo
{
    public class Foo
    {
        private readonly WeakEvent<EventArgs> _bar = new WeakEvent<EventArgs>();

        public event EventHandler Bar
        {
            add => _bar.Add(value, value.Invoke);
            remove => _bar.Remove(value);
        }

        private void OnBar() => _bar.Invoke(this, EventArgs.Empty);
    }
}

使用弱事件

对于弱事件的使用,就跟以前任何其他正常事件一样了,直接 +=-=

这样,如果我有一个 A 类的实例 a,订阅了以上 FooBar 事件,那么当 a 脱离作用范围后,将可以被垃圾回收机制回收。而如果不这么做,Foo 将始终保留对 a 实例的引用,这将阻止垃圾回收。

09-18 2019

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

弱引用是 .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


参考资料

09-18 2019

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

弱引用是 .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 如此设计?

一句话解答:为了高性能

请参见我的另一篇博客:


参考资料

09-17 2019

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

在 .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
  • 需要打开脚本执行
  • 需要打开计算机上环境变量中路径中的程序

参考资料

09-12 2019

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

在 .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]

请参见:

09-12 2019

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

获取 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 还可能是其他一些可能延迟计算值的提供者。因此,你不能在这里获取到常规方法获取到的依赖项属性的真实类型的值。

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


参考资料

09-12 2019

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

本文介绍如何在 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);

参考资料

09-05 2019

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

在使用 .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 属性。


参考资料

09-05 2019

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

在 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 的时候,你需要避免程序对空闲时机的控制不好,导致屏幕始终不关闭。

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


参考资料

09-02 2019

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

最近总是收到一个异常 “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 设置就没有问题(而且可以做到类似的效果。

09-02 2019

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

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",导致看起来好像这个变换生效了一样。

小心异常

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

08-27 2019

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

在 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 的分析,由于比较长,请参见我的另一篇博客:


参考资料

08-27 2019

WPF 的 Application.Current.Dispatcher 中,Dispatcher 属性一定不会为 null

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

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


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

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 自构造起便拥有一个不为 nullDispatcher 属性,其所有子类在初始化之前便会得到不为 nullDispatcher 属性。

后续赋值

现在我们来看看在初始化完成之后,后面是否有可能将 _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 很好理解,让 DispatcherObjectDispatcher 无关。在整个 WPF 的代码中,使用此方法的仅有以下 6 处:

  • Freezable.Freeze 实例方法
  • BeginStoryboard.Seal 实例方法
  • Style.Seal 实例方法
  • TriggerBase.Seal 实例方法
  • StyleHelperSealTemplate 静态方法中对 FrameworkTemplate 类型的实例调用此方法
  • ResourceDictionary 在构造函数中为 DispatcherObject 类型的 DummyInheritanceContext 属性调用此方法

Application 类型不是以上任何一个类型的子类(Application 类的直接基类是 DispatcherObject),因此 Application 类中的 Dispatcher 属性不可能因为 DetachFromDispatcher 方法的调用而被赋值为 null

接下来看看 MakeSentinel 方法,此方法的作用不如上面方法那样直观,实际上它的作用仅仅为了验证某个方法调用时所在的线程是否是符合预期的(给 VerifyAccessCheckAccess 使用)。

使用此方法的仅有 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 无关。

结论

总结以上所有的分析:

  1. Application 类型的实例在初始化之前,Dispatcher 属性就已经被赋值且不为 null
  2. 所有可能改变 _dispatcher 属性的常规方法均与 Application 类型无关;

因此,所有常规手段均不会让 Application 类的 Dispatcher 属性拿到 null 值。如果你还说拿到了 null,那就检查是否有逗比程序员通过反射或其他手段将 _dispatcher 字段改为了 null 吧……

Application.Current 静态属性

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


参考资料

08-27 2019

使用 SetParent 跨进程设置父子窗口时的一些问题(小心卡死)

在微软的官方文档中,说 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 Set­Parent 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.)
正如有些人说的“如果你不知道后果,那么你也不足以正确地完成某件事情”。在开始破坏规则之前,您必须先成为规则的主人。

你必须清楚跨进程设置父子窗口带来的各种副作用,然后针对性地给出解决方案:

  1. 比如所有窗口会强制串联成一个队列,那么可以考虑将暂时不显示的窗口断开父子关系;
  2. 比如设置窗口的位置大小等操作,必须考虑此窗口不是顶层窗口的问题,需要跨越进程到顶层窗口来操作;

参考资料

08-14 2019

System.InvalidOperationException:“BuildWindowCore 无法返回寄宿的子窗口句柄。”

当试图在 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.

原因和解决办法

此异常的原因非常简单,是 HwndSourceBuildWindowCore 的返回值有问题。具体来说,就是子窗口的句柄返回了 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 才是正确的。

08-14 2019

System.InvalidOperationException:“寄宿 HWND 必须是子窗口。”

当试图在 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();
        }
    }
}

寄宿 HWND 必须是子窗口

当运行此代码的时候,会提示错误:

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,
++  };

最关键的是两点:

  1. 需要设置此窗口为子窗口,也就是设置 WindowStyleWS_CHILD
  2. 需要设置此窗口的父窗口,也就是设置 ParentWindowhwndParent.Handle(我们使用参数中传入的 hwndParent 作为父窗口)。

现在再运行,即可正常显示此嵌套窗口:

嵌套窗口

另外,WindowStyle 属性最好加上 WS_CLIPCHILDREN,详情请阅读:


参考资料

08-14 2019

System.InvalidOperationException:“寄宿的 HWND 必须是指定父级的子窗口。”

当试图在 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 设置一次子窗口。

08-04 2019

通过 mklink 收集本地文件系统的所有 NuGet 包输出目录来快速调试公共组件代码

我们做的公共库可能通过 nuget.org 发布,也可能是自己搭建 NuGet 服务器。但是,如果某个包正在开发中,需要快速验证其是否解决掉一些诡异的 bug 的话,除了单元测试这种间接的测试方法,还可以在本地安装未发布的 NuGet 包的方法来快速调试。

本文介绍如何本地打包发布 NuGet 包,然后通过 mklink 收集所有的本地包达到快速调试的目的。


将本地文件夹作为 NuGet 源

我有另一篇博客介绍如何将本地文件夹设置称为 NuGet 包源:

在 Visual Studio 中打开 工具 -> 选项 -> NuGet 包管理器 -> 包源 可以直接将一个本地文件夹设置称为 NuGet 包源。

管理包源

其他设置方法可以去那篇博客当中阅读。

如下图,是我通过 mklink 将散落在各处的 NuGet 包的调试输出目录收集了起来:

通过 mklink 收集的 NuGet 包源

比如,点开其中的 Walterlv.Packages 可以看到 Walterlv.Packages 仓库中输出的 NuGet 包:

其中的一个 NuGet 输出文件夹

由于我的每一个文件夹都是指向的 Visual Studio 编译后的输出目录,所以,只需要使用 Visual Studio 重新编译一下项目,文件夹中的 NuGet 包即会更新。

于是,这相当于我在一个文件夹中,包含了我整个计算机上所有库项目的 NuGet 包,只需要将这个文件夹设置称为 NuGet 包源,即可直接调试本地任何一个公共组件库打出来的 NuGet 包。

设置源并体验快速调试

如下图,是我将那个收集所有 NuGet 文件夹的目录设置成为了 NuGet 源:

设置的本地 NuGet 源

于是,我可以在 Visual Studio 的包管理器中看到所有还没有发布的,依然处于调试状态的各种库:

各种处于调试状态的各种库

基于此,我们可以在包还没有编写完的时候调试,验证速度非常快。

08-04 2019

使用 C# 中的 dynamic 关键字调用类型方法时可能遇到的各种问题

你可以使用 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”

对于 .NET Core 或者 .NET Standard 项目

需要为你的项目安装以下两个 NuGet 包:

引用两个 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>

对于 .NET Framework 项目

你需要引用 Microsoft.CSharp

添加引用

引用 Microsoft.CSharp

于是你的项目里面会多出一项引用:

    <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

      <PropertyGroup>
        <TargetFramework>net48</TargetFramework>
      </PropertyGroup>

      <ItemGroup>
++      <Reference Include="Microsoft.CSharp" />
      </ItemGroup>

    </Project>

异常:“{0}”未包含“{1}”的定义

{0} 是类型名称,而 {1} 是使用 dynamic 访问的属性或者方法的名称。

比如,我试图从某个 Attribute 中访问到 Key 属性的时候会抛出以下异常:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:““System.Attribute”未包含“Key”的定义”

出现此异常的原因是:

  • dynamic 所引用的对象里面,没有签名相同的 public 的属性或者方法

于是,如果你确认你的类型里面是有这个属性或者方法的话,那么就需要注意需要将此成员改成 public 才可以访问。


参考资料

08-04 2019

设计一个 .NET 可用的弱引用集合(可用来做缓存池使用)

我们有弱引用 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[]CountIsReadOnlyContainsCopyToIndexOfGetEnumerator 这些都是在获取状态,AddClearRemove 是在修改状态,而 InsertRemoveAt 会在修改状态的同时读取状态。

这么多的获取和修改状态的方法,如果提供出去,还指望使用者能够正常使用,简直是做梦!违背以上两个原则。

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();
    }

不过,AddRemove 方法可能我们会考虑留下来,但这就不能是继承自 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 + 鼠标点击的方式打开类型的源代码,而不需要进行反编译。

08-03 2019

如何为非常不确定的行为(如并发)设计安全的 API,使用这些 API 时如何确保安全

.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 用法指导

如果你正在为一个易变的状态设计 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;
            }
        }
    }
}

这段代码的完全解读:

  1. 当执行 Run 方法的时候,先判断当前是否已经在跑其他的任务:
    • isRunning0 表示当前一定没有在跑其他任务,我们使用原则操作立刻将其修改为 1
    • isRunning1 表示当前不确定是否在跑其他任务;
  2. 既然 isRunning1 的时候状态不确定,于是我们加锁来判断其是否真的有任务在跑:
    • lock 环境中确认 _isRunning 字段而非变量为 1 则说明真的有任务在跑,此时等待任务完成即可,这里就可以退出了;
    • lock 环境中发现 _isRunning 字段而非变量为 0 则说明实际上是没有任务在跑的(刚刚判断为 1 只是因为这两次判断之间,并发的任务刚刚在结束的过程中),于是需要跟一开始判断为 0 一样,进入到后面的循环中;
  3. 外层的 while 循环第一次是一定能进去的,于是我们暂且不谈;
  4. while 内循环中,我们依次检查并发队列 _queue 中是否还有任务要执行,如果有要执行的,就执行:
    • 这个过程我们完全没有做加锁,因为这可能是非常耗时的任务,如果我们加锁,将导致其他线程出现非常严重的资源浪费;
  5. 如果 queue 中的所有任务执行完毕,我们将进入一个 lock 区间:
    • 在这个 lock 区间里面我们再次确认任务是否已经完成,如果没有完成,我们靠最外层的 while 循环重新回到内层 while 循环中继续任务;
    • 如果在这个 lock 区间里面我们发现任务已经完成了,就设置 _isRunning0,表示任务真的已经完成,随后退出 while 循环;

你可以注意到我们的 lock 是用来确认一开始 isRunning1 时的那个不确定的状态的。因为我们需要多次访问这个状态,所以必须加锁来确认状态是同步的。

API 设计指导

在了解了上面的用法指导后,API 设计指导也呼之欲出了:

  1. 针对典型的应用场景,必须设计一个专门的方法,一次调用即可完全获取当时需要的状态,或者一次调用即可完全修改需要修改的状态;
  2. 不要提供大于 1 个方法组合在一起才能使用的 API,这会让调用方获取不一致的状态。

对于多线程并发导致的不确定性,使用方虽然可以通过 lock 来规避以上第二条问题,但设计方最好在设计之初就避免问题,以便让 API 更好使用。

关于通用 API 设计指导,你可以阅读我的另一篇双语博客:

08-01 2019

通过 AppSwitch 禁用 WPF 内置的触摸让 WPF 程序可以处理 Windows 触摸消息

WPF 框架自己实现了一套触摸机制,但同一窗口只能支持一套触摸机制,于是这会禁用系统的触摸消息(WM_TOUCH)。这能够很大程度提升 WPF 程序的触摸响应速度,但是很多时候又会产生一些 Bug。

如果你有需要,可以考虑禁用 WPF 的内置的实时触摸(RealTimeStylus)。本文介绍禁用方法,使用 AppSwitch,而不是网上广为流传的反射方法。


如何设置 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 的版本更新中出现问题:

此方法可以解决的问题一览

拖拽窗口或者调整窗口大小时不能实时跟随的问题

在部分设备上启动即崩溃

在透明窗口上触摸会挡住 UWP 程序


参考资料

07-29 2019

Visual Studio 2019 中使用 .NET Core 预览版 SDK 的全局配置文件在哪里?

本文介绍在使用 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

sdk.txt 的所在路径

你一定觉得奇怪,我们在 Visual Studio 2019 中设置了使用 .NET Core SDK 预览版之后,这个配置是全局生效的,即便在命令行中运行 MSBuild 或者 dotnet build 也是会因此而使用预览版或者正式版的。但是这个路径明显看起来是 Visual Studio 的私有路径。

虽然这很诡异,但确实如此,不信,可以看我是如何确认这个文件就是 .NET Core SDK 预览版的全局配置的:

另外,如果你想知道如何在 Visual Studio 2019 中指定使用 .NET Core SDK 的预览版,可以参考我的另外一篇博客:

Visual Studio 2019 的

07-27 2019

The partial same C# namespace may cause source code compatibility issue

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.

07-27 2019

使用基于 Roslyn 的 Microsoft.CodeAnalysis.PublicApiAnalyzers 来追踪项目的 API 改动,帮助保持库的 API 兼容性

做库的时候,需要一定程度上保持 API 的兼容性


第一步:安装 NuGet 包

首先打开你的库项目,或者如果你希望从零开始也可以直接新建一个项目。这里为了博客阅读的简单,我创建一个全新的项目来演示。

打开一个项目

然后,为主要的库项目安装 NuGet 包:

安装 NuGet 包

安装完成之后,你的项目文件(.csproj)可能类似于下面这样:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="2.9.3" />
  </ItemGroup>

</Project>

第二步:创建 API 记录文件

在你的项目内创建两个文件:

  • PublicAPI.Shipped.txt
  • PublicAPI.Unshipped.txt

创建 API 记录文件

这就是两个普通的文本文件。创建纯文本文件的方法是在项目上右键 -> 添加 -> 新建项...,然后在打开的模板中选择 文本文件,使用上面指定的名称即可(要创建两个)。

然后,编辑项目文件,我们需要将这两个文件加入到项目中来。

编辑项目文件

如果你看不到上图中的“编辑项目文件”选项,则需要升级项目文件到 SDK 风格,详见:

然后,将这两个文件添加为 AdditionalFiles

  <Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
      <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="2.9.3" />
    </ItemGroup>

+   <ItemGroup>
+     <AdditionalFiles Include="PublicAPI.Shipped.txt" />
+     <AdditionalFiles Include="PublicAPI.Unshipped.txt" />
+   </ItemGroup>

  </Project>

如果你把这两个文件放到了其他的路径,那么上面也需要改成对应的路径。

这时,这两个文件内容还是空的。

第三步:添加 API 记录

这个时候,你会看到库中的 public 类、方法、属性等都会发出修改建议,说此符号并不是已声明 API 的一部分。

类型

属性

点击小灯泡,即可将点击所在的 API 加入到 PublicAPI.Unshipped.txt 中。

我将两个 API 都添加之后,PublicAPI.Unshipped.txt 文件中现在是这样的(注意有一个隐式构造函数哦):

Walterlv.PackageDemo.ApiTracking.Class1
Walterlv.PackageDemo.ApiTracking.Class1.Class1() -> void
Walterlv.PackageDemo.ApiTracking.Class1.Foo.get -> string

体验 API 的追踪

现在,我们将 Foo 属性改名成 Foo2 属性,于是就会出现编译警告:

编译警告

RS0016 Symbol ‘Foo2.get’ is not part of the declared API.
RS0017 Symbol ‘Walterlv.PackageDemo.ApiTracking.Class1.Foo.get -> string’ is part of the declared API, but is either not public or could not be found

提示 Foo2 属性不是已声明 API 的一部分,而 Foo 属性虽然是已声明 API 的一部分,但已经找不到了。

这种提示对于保持库的兼容性是非常有帮助的。

将警告变成错误

在分析器的规则上面右键,可以为某项规则设置严重性。

将警告设置为错误

这时,再编译即会报告编译错误。

编译错误

项目中也会多一个规则集文件:

  <Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
      <TargetFramework>netstandard2.0</TargetFramework>
+     <CodeAnalysisRuleSet>Walterlv.PackageDemo.ApiTracking.ruleset</CodeAnalysisRuleSet>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="2.9.3" />
    </ItemGroup>

    <ItemGroup>
      <AdditionalFiles Include="PublicAPI.Shipped.txt" />
      <AdditionalFiles Include="PublicAPI.Unshipped.txt" />
    </ItemGroup>

  </Project>

第四步:将 API 打包

前面我们都是在 PublicAPI.Unshipped.txt 文件中追踪 API。但是如果我们的库需要发布一个版本的时候,我们就需要跟上一个版本比较 API 的差异。

上一个发布版本的 API 就记录在 PublicAPI.Shipped.txt 文件中,这两个文件的差异即是这两个版本的 API 差异。在一个新的版本发布后,就需要将 API 归档到 PublicAPI.Shipped.txt 文件中。


参考资料

07-27 2019

找出 .NET Core SDK 是否使用预览版的全局配置文件在哪里(探索篇)

你是否好奇 Visual Studio 2019 中的 .NET Core SDK 预览版开关是全局生效的,那个全局的配置在哪里呢?

本文将和你一起探索找到这个全局的配置文件。


使用 Process Monitor 探索

下载 Process Monitor

Process Monitor 是微软极品工具箱的一部分,你可以在此页面下载:

打开 Process Monitor

当你一开始打开 Process Monitor 的时候,列表中会立刻刷出大量的进程的操作记录。这么多的记录会让我们找到目标进程操作的文件有些吃力,于是我们需要设置规则。

Process Monitor 的工具栏按钮并不多,而且我们这一次的目标只会用到其中的两个:

  • 清除列表(将已经记录的所有数据清空,便于聚焦到我们最关心的数据中)
  • 设置过滤器(防止大量无关的进程操作进入列表中干扰我们的查找)

Process Monitor 的工具栏按钮

设置过滤规则

在工具栏上点击“设置过滤器”,然后,添加我们感兴趣的两个进程名称:

  • devenv.exe
  • MSBuild.exe

前者是 Visual Studio 的进程名,后者是 MSBuild.exe 的进程名。我们使用这两个进程名称分别找到 Visual Studio 2019 是如何设置全局 .NET Core 预览配置的,并且在命令行中运行 MSBuild.exe 来验证确实是这个全局配置。

然后排除除了文件意外的所有事件类型,最终是如下过滤器:

设置过滤器

捕获 devenv.exe

现在,我们打开 Visual Studio 2019,然后停留到下面这个界面中。改变一下 .NET Core SDK 预览版选项的勾选状态。

设置 Visual Studio 2019 使用 .NET Core SDK 预览版

现在,我们点击一下“确定”,将立即可以在 Process Monitor 中看到一些文件的修改:

捕获到的文件修改

上面是在点击“确定”按钮一瞬间 Visual Studio 2019 的所有文件操作。你可以注意到左侧的时间,我的截图中从 45 秒到 48 秒是可能有效的文件读写,再后面已经延迟了 10 秒了,多半是其他的操作。

在这些文件中,可以很明显看到文件分为三类:

  • sdk.txt 一个不知名的文件,但似乎跟我们的 .NET Core SDK 相关
  • SettingsLogs 一看就是给设置功能用的日志
  • VSApplicationInsights 一看就是数据收集相关

通过排除法,我们能得知最关键的文件就是那个 sdk.txt。去看一看那个文件的内容,发现只有一行:

UsePreviews=True

这基本上可以确认 Visual Studio 2019 设置是否使用 .NET Core SDK 预览版就是在这个文件中。

不过,这带来一个疑惑,就是这个路径特别不像是 .NET Core SDK 的配置路径,倒像是 Visual Studio 自己的设置配置。

于是必须通过其他途径来确认这是否就是真实的全局配置。

捕获 MSBuild.exe

现在,我们清除一下 Process Monitor 中的已经记录的数据,然后,我们在命令行中对一个项目敲下 msbuild 命令。

> msbuild

然后在 Process Monitor 里面观察事件。这次发现事件相当多,于是换个方式。

因为我们主要是验证 sdk.txt 文件,但同时希望看看是否还有其他文件。于是我们将 sdk.txt 文件相关的事件高亮。

点击 Filter -> Highlight...,然后选择 Path contains sdk.txt 时则 Include

打开 Highlight

高亮 sdk.txt 文件

这时,再看捕获到的事件,可以发现编译期间确实读取了这个文件。

MSBuild.exe 读取了 sdk.txt

此举虽不能成为此文件是全局配置的铁证,但至少说明这个文件与全局配置非常相关。

另外,继续在记录中翻找,还可以发现与此配置可能相关的两个 dll:

  • Microsoft.Build.NuGetSdkResolver.dll
  • Microsoft.DotNet.MSBuildSdkResolver.dll

可能与此相关的 dll

验证结论

要验证此文件确实是全局配置其实也很简单,自行改一改配置,然后使用 MSBuild.exe 编译试试即可。

现在,将 sdk.txt 文件内容改为:

UsePreviews=False

编译一下使用了 .NET Core 3.0 新特性的项目(我使用了 Microsoft.NET.Sdk.WindowsDesktop,这是 3.0 才有的 SDK)。

不使用预览版编译

编译错误,提示 Microsoft.NET.Sdk.WindowsDesktop 这个 SDK 没有找到。

现在,将 sdk.txt 文件内容改为:

UsePreviews=True

编译相同的项目,发现可以正常编译通过了。

使用预览版编译

这可以证明,此文件正是决定是否使用预览版的决定性证据。

其他

但值得注意的是,打开 Visual Studio 2019 后,发现其设置界面并没有应用此文件最新的修改,这可以说 Visual Studio 2019 的配置是不止这一处。

反编译探索

通过反编译探索的方式感谢小伙伴 KodamaSakuno (神樹桜乃) 彻夜寻找。

相关的代码在 cli/VSSettings.cs at master · dotnet/cli 中,你可以前往查看。

VSSettings 的构造函数中,为字段 _settingsFilePath 赋值,拼接了 sdk.txt 文件的路径。

_settingsFilePath = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "Microsoft",
    "VisualStudio",
    version.Major + ".0_" + instanceId,
    "sdk.txt");

读取时,使用此路径下的 sdk.txt 文件读取了 UsePreviews 变量的值。

private void ReadFromDisk()
{
    using (var reader = new StreamReader(_settingsFilePath))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            int indexOfEquals = line.IndexOf('=');
            if (indexOfEquals < 0 || indexOfEquals == (line.Length - 1))
            {
                continue;
            }

            string key = line.Substring(0, indexOfEquals).Trim();
            string value = line.Substring(indexOfEquals + 1).Trim();

            if (key.Equals("UsePreviews", StringComparison.OrdinalIgnoreCase)
                && bool.TryParse(value, out bool usePreviews))
            {
                _disallowPrerelease = !usePreviews;
                return;
            }
        }
    }

    // File does not have UsePreviews entry -> use default
    _disallowPrerelease = _disallowPrereleaseByDefault;
}

07-25 2019

nuget.exe 还原解决方案 NuGet 包的时候出现错误:调用的目标发生了异常。Error parsing the nested project section in solution file.

我这里使用 Visual Studio 2019 能好好编译的一个项目,发现在另一个小伙伴那里却编译不通过,是在 NuGet 还原那里报告了错误:

调用的目标发生了异常。Error parsing the nested project section in solution file.

本文介绍如何解决这样的问题。


原因

此问题的原因可能有多种:

  1. 解决方案里面 ProjectEndProject 不成对,导致某个项目没有被识别出来
  2. 解决方案中 Global 部分的项目 Id 没有在 Project 部分发现对应的项目
  3. 解决方案里面出现了当前 MSBuild 版本不认识的项目类型

解决方法

ProjectEndProject 不成对

ProjectEndProject 不成对通常是合并分支时,自动解冲突解错了导致的,例如像下面这样:

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
EndProject

而解决方法,就是补全缺失的 EndProject 行:

    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
++  EndProject
    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
    EndProject

Global 部分的项目 Id 没有在 Project 部分发现对应的项目

这是说,如果在 Global 部分通过项目 Id 引用了一些项目,但是这些项目没有在前面 Project 部分定义。例如下面的 sln 片段:

    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
    EndProject
    Global
    	GlobalSection(SolutionConfigurationPlatforms) = preSolution
    		Debug|Any CPU = Debug|Any CPU
    		Release|Any CPU = Release|Any CPU
    	EndGlobalSection
    	GlobalSection(ProjectConfigurationPlatforms) = postSolution
    		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
    		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
    		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
    		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.Build.0 = Release|Any CPU
    	EndGlobalSection
    	GlobalSection(SolutionProperties) = preSolution
    		HideSolutionNode = FALSE
    	EndGlobalSection
    	GlobalSection(NestedProjects) = preSolution
--  		{DC0B1D44-5DF4-4590-BBFE-072183677A78} = {20B61509-640C-492B-8B33-FB472CCF1391}
    		{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {20B61509-640C-492B-8B33-FB472CCF1391}
    	EndGlobalSection
    	GlobalSection(ExtensibilityGlobals) = postSolution
    		SolutionGuid = {F2F1AD1B-207B-4731-ABEB-92882F89B155}
    	EndGlobalSection
    EndGlobal

上面红框标注的项目 Id {DC0B1D44-5DF4-4590-BBFE-072183677A78} 在前面的 Project 部分是没有定义的,于是出现问题。这通常也是合并冲突所致。

解决方法是删掉这个多于的配置,或者在前面加回误删的 Project 节点,如:

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
EndProject

出现了当前 MSBuild 版本不认识的项目类型

可能是 nuget 识别出来的 MSBuild 版本过旧,也可能是没有安装对应的工作负载。

检查你的项目是否安装了需要的工作负载,比如做 Visual Studio 插件开发需要插件工作负载。可以阅读:

我在另外的博客中写了解决方案中项目类型的内容:

而如果是 nuget 自动识别出来的 MSBuild 版本过旧,则你会同时看到下面的这段提示:

NuGet Version: 5.1.0.6013

MSBuild auto-detection: using msbuild version ‘15.9.21.664’ from ‘C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin’. Use option -MSBuildVersion to force nuget to use a specific version of MSBuild.

Error parsing solution file at C:\walterlv\Walterlv.Demo\Walterlv.Demo.sln: 调用的目标发生了异常。 Error parsing the nested project section in solution file.

于是解决方法是使 NuGet 能够找到正确的 MSBuild.exe 的版本。

我在另一篇博客中有写一些决定 MSBuild.exe 版本的方法:

可以通过设置环境变量的方式来解决自动查找版本错误的问题。

你可以看到本文后面附带了很多的参考资料,但实际上这里的所有资料都没有帮助我解决掉任何问题。这个问题的本质是 nuget 识别到了旧版本的 MSBuild.exe。


参考资料

07-25 2019

为 NuGet 指定检测的 MSBuild 路径或版本,解决 MSBuild auto-detection: using msbuild version 自动查找路径不合适的问题

使用 nuget restore 命令还原项目的 NuGet 包的时候,NuGet 会尝试自动检测计算机上已经安装的 MSBuild。不过,如果你同时安装了 Visual Studio 2017 和 Visual Studio 2019,那么 NuGet 有可能找到错误版本的 MSBuild。

本文介绍如何解决自动查找版本错误的问题。


问题

当我们敲下 nuget restore 命令的时候,命令行的第 2 行会输出自动检测到的 MSBuild 版本号,就像下面的输出一样:

NuGet Version: 5.0.2.5988
MSBuild auto-detection: using msbuild version ‘15.9.21.664’ from ‘C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin’. Use option -MSBuildVersion to force nuget to use a specific version of MSBuild.

实际上我计算机上同时安装了 Visual Studio 2017 和 Visual Studio 2019,我有两个不同版本的 MSBuild:

  • 15.9.21.664
    • C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin
  • 16.1.76.45076
    • C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin

要让 NuGet 找到正确版本的 MSBuild.exe,我们有三种方法。

使用命令行参数解决

实际上前面 nuget restore 命令的输出中就已经可以看出来其中一个解决方法了,即使用 -MSBuildVersion 来指定 MSBuild 的版本号。

虽然命令行输出中推荐使用了 -MSBuildVersion 选项来指定 MSBuild 的版本,但是实际上实现同样功能的有两个不同的选项:

  • -MSBuildPath 自 NuGet 4.0 开始新增的选项,指定 MSBuild 程序的路径。
  • -MSBuildVersion

当同时指定上面两个选项时,-MSBuildPath 选项优先级高于 -MSBuildVersion 选项。

于是我们的 nuget restore 命令改成这样写:

> nuget restore -MSBuildPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin"

输出现在会使用期望的 MSBuild 了:

Using Msbuild from 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin'.

修改环境变量解决

NuGet 的命令行自动查找 MSBuild.exe 时,是通过环境变量中的 PATH 变量来找的。会找到 PATH 中第一个包含 msbuild.exe 文件的路径,将其作为自动查找到的 MSBuild 的路径。

所以,我们的解决方法是,如果找错了,我们就把期望正确的 MSBuild 所在的路径设置到不期望的 MSBuild 路径的前面。就像下图这样,我们把 2019 版本的 MSBuild 设置到了 2017 版本的前面。

设置环境变量

以下是 NuGet 项目中自动查找 MSBuild.exe 文件的方法,源代码来自 https://github.com/NuGet/NuGet.Client/blob/2b45154b8568d6cbf1469f414938f0e3e88e3704/src/NuGet.Clients/NuGet.CommandLine/MsBuildUtility.cs#L986

private static string GetMSBuild()
{
    var exeNames = new [] { "msbuild.exe" };

    if (RuntimeEnvironmentHelper.IsMono)
    {
        exeNames = new[] { "msbuild", "xbuild" };
    }

    // Try to find msbuild or xbuild in $Path.
    var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);

    if (pathDirs?.Length > 0)
    {
        foreach (var exeName in exeNames)
        {
            var exePath = pathDirs.Select(dir => Path.Combine(dir, exeName)).FirstOrDefault(File.Exists);
            if (exePath != null)
            {
                return exePath;
            }
        }
    }

    return null;
}

我故意在桌面上放了一个老旧的 MSBuild.exe,然后将此路径设置到环境变量 PATH 的前面,出现了编译错误。

编译错误


参考资料

07-24 2019

理解 Visual Studio 解决方案文件格式(.sln)

一般情况下我们并不需要关心 Visual Studio 解决方案文件格式(.sln),因为 Visual Studio 对解决方案文件的自动修复能力是非常强的。但是如果遇到自动解冲突错误或者编译不通过了,那么此文件还是需要手工修改的。

本文介绍 Visual Studio 解决方案(.sln)文件的格式。


基本概念

Visual Studio 的解决方案文件由这三个部分组成:

  • 版本信息
    • Microsoft Visual Studio Solution File, Format Version 12.00
    • # Visual Studio Version 16
    • VisualStudioVersion = 16.0.28606.126
    • MinimumVisualStudioVersion = 10.0.40219.1
  • 项目信息
    • Project
    • EndProject
  • 全局信息
    • Global
    • EndGlobal

虽然看起来是三个独立的部分,但其实除了版本号之外,项目信息和全局信息还是有挺多耦合部分的。

比如我们来看一个 sln 文件的例子,是一个最简单的只有一个项目的 sln 文件:

只有一个项目的 sln 文件

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {F2F1AD1B-207B-4731-ABEB-92882F89B155}
	EndGlobalSection
EndGlobal

下面我们来一一说明。

结构

版本信息

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1

记录文件的格式版本是 12.0。使用 Visual Studio 2019 编辑/创建。

这里有一个小技巧,这里的 VisualStudioVersion 版本号设置为 15.0 会使得打开 sln 文件的时候默认使用 Visual Studio 2017,而设置为 16.0 会使得打开 sln 文件的时候默认使用 Visual Studio 2019。

项目信息

Project

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
EndProject

项目信息至少由两行组成,第一行标记项目信息开始,而最后一行表示信息结束。

其格式为:

Project("{项目类型}") = "项目名称", "项目路径", "项目 Id"
EndProject

你可以在我的另一篇博客中找到项目类型:

但是本文列举几个 .NET/C# 项目中的常见类型:

  • 9A19103F-16F7-4668-BE54-9A1E7A4F7556 SDK 风格的 C# 项目文件
  • FAE04EC0-301F-11D3-BF4B-00C04F79EFBC 传统风格的 C# 项目文件
  • 2150E333-8FDC-42A3-9474-1A3956D46DE8 解决方案文件夹

关于 SDK 风格的项目文件,可以阅读我的另一篇博客:

项目名称和项目路径不必多说,都知道。对于文件夹而言,项目名称就是文件夹的名称,而项目路径也是文件夹的名称。

项目 Id 是在解决方案创建项目的过程中生成的一个新的 GUID,每个项目都不一样。对于 SDK 风格的 C# 项目文件,csproj 中可以指定项目依赖,而如果没有直接的项目依赖,而只是解决方案编译级别的依赖,那么也可以靠 sln 文件中的项目 Id 来指定项目的依赖关系。另外,也通过项目 Id 来对项目做一些编译上的解决方案级别的配置。

ProjectSection

ProjectEndProject 的内部还可以放 ProjectSection

比如对于解决方案文件夹,可以包含解决方案文件:

Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B002382D-4C9E-4F08-85E5-F12E2C061F5A}"
	ProjectSection(SolutionItems) = preProject
		.gitattributes = .gitattributes
		.gitignore = .gitignore
		README.md = README.md
		build\Version.props = build\Version.props
	EndProjectSection
EndProject

这个解决方案文件夹中包含了四个文件,其路径分别记录在了 ProjectSection 节点里面。

ProjectSection 还可以记录项目依赖关系(非项目之间的真实依赖,而是解决方案级别的编译依赖):

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
	ProjectSection(ProjectDependencies) = postProject
		{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {98FF9756-B95A-4FDB-9858-5106F486FBF3}
	EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
EndProject

在这一段节点里面,我们的 Walterlv.Demo 项目依赖于另外一个 Walterlv.Demo2 项目。依赖是以 项目 Id = 项目 Id 的方式写出来的;如果有多个依赖,那么就写多行。不用吐槽为什么一样还要写两遍,因为这是一个固定的格式,后面我们会介绍一些全局配置里面会有两个不一样的。

关于设置项目依赖关系的方法,除了 sln 文件里面的设置之外,还有通过设置项目依赖属性的方式,详情可以阅读:

全局信息

一个全局信息的例子如下:

Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.Build.0 = Release|Any CPU
		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {F2F1AD1B-207B-4731-ABEB-92882F89B155}
	EndGlobalSection
EndGlobal

在这个全局信息的例子中,为解决方案指定了两个配置(Configuration),DebugRelease,平台都是 Any CPU。同时也为每个项目指定了单独的配置种类,可供选择,每一行都是 项目的配置 = 解决方案的配置 表示此项目的此种配置在解决方案的某个全局配置之下。

如果我们将这两个项目放到文件夹中,那么我们可以额外看到一个新的全局配置 NestedProjects 字面意思是说 {DC0B1D44-5DF4-4590-BBFE-072183677A78}{98FF9756-B95A-4FDB-9858-5106F486FBF3} 两个项目在 {20B61509-640C-492B-8B33-FB472CCF1391} 项目中嵌套,实际意义代表 Walterlv.DemoWalterlv.Demo2 两个项目在 Folder 文件夹下。

GlobalSection(NestedProjects) = preSolution
    {DC0B1D44-5DF4-4590-BBFE-072183677A78} = {20B61509-640C-492B-8B33-FB472CCF1391}
    {98FF9756-B95A-4FDB-9858-5106F486FBF3} = {20B61509-640C-492B-8B33-FB472CCF1391}
EndGlobalSection

在同一个文件夹下

上图解决方案下的整个解决方案全部内容如下:

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
	ProjectSection(ProjectDependencies) = postProject
		{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {98FF9756-B95A-4FDB-9858-5106F486FBF3}
	EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Folder", "Folder", "{20B61509-640C-492B-8B33-FB472CCF1391}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.Build.0 = Release|Any CPU
		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(NestedProjects) = preSolution
		{DC0B1D44-5DF4-4590-BBFE-072183677A78} = {20B61509-640C-492B-8B33-FB472CCF1391}
		{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {20B61509-640C-492B-8B33-FB472CCF1391}
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {F2F1AD1B-207B-4731-ABEB-92882F89B155}
	EndGlobalSection
EndGlobal

07-24 2019

两种方法设置 .NET/C# 项目的编译顺序,而不影响项目之间的引用

当 A 项目引用 B 项目,那么使用 Visual Studio 或者 MSBuild 编译 A 项目之前就会确保 B 项目已经编译完毕。通常我们指定这种引用是因为 A 项目确实在运行期间需要 B 项目生成的程序集。

但是,现在 B 项目可能仅仅只是一个工具项目,或者说 A 项目编译之后的程序集并不需要 B,仅仅只是将 B 打到一个包中,那么我们其实需要的仅仅是 B 项目先编译而已。

本文介绍如何影响项目的编译顺序,而不带来项目实际引用。


方法一:设置 ReferenceOutputAssembly

依然在项目中使用往常习惯的方法设置项目引用:

设置项目引用

但是,在项目引用设置完成之后,需要打开项目的项目文件(.csproj)给 ProjectReference 节点加上 ReferenceOutputAssembly 的属性设置,将其值设置为 false。这表示仅仅是项目引用,而不将项目的任何输出程序集作为此项目的依赖。

<ItemGroup>
  <ProjectReference Include="..\Walterlv.Demo.Analyzer\Walterlv.Demo.Analyzer.csproj" ReferenceOutputAssembly="false" />
  <ProjectReference Include="..\Walterlv.Demo.Build\Walterlv.Demo.Build.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>

上面的 ProjectReference 是 Sdk 风格的 csproj 文件中的项目引用。即便不是 Sdk 风格,也是一样的加这个属性。

当然,你写多行也是可以的:

<ItemGroup>