|
dotnet
本文和大家介绍 Serialize.OpenXml.CodeGen 这个支持从某个文档生成用于创建出这个文档的 C# 或 VB 代码的库。作用就是可以让小伙伴在拿到一份模版文件之后,可以通过 Serialize.OpenXml.CodeGen 生成能创建出这份文档的 C# 或 VB 的代码,用于在这份代码上面更改功能,做到创建定制 Docx 或 PPTX 或 Xlsx 文档的功能
现在有很多函数计算服务,通过函数计算服务可以在服务上运行自己的代码,而不需要理会服务器的部署以及运行代码存在的并发问题。这些问题都会在云进行计算。使用阿里的函数计算服务可以使用 dotnet core 2.1 的框架,同时测试使用免费。本文告诉搭建如何在阿里的函数计算服务上运行自己的代码
在不联网情况下,从 url 获取文件名也就是只能靠 url 本身了,如果是 get 的链接很多链接后面都是带上了文件名,那么如何用最短的代码获取链接里面的文件名
如果我使用的是具体的数组而我的数组是基类数组,而我传入子类的元素进行 json 序列化,可能发现 Json.NET 序列化没有包含子类元素的属性。如果要包含子类的属性或字段,可以在序列化的类数组定义为 object 数组的方式
通过推送 Tag 才打 NuGet 包的方法的作用不仅仅是让打包方便,让打包这个动作可以完全在本地执行,无需关注其他系统的使用步骤。更重要的是可以强制每个可能被安装的 NuGet 包版本都能有一个和他对应的 Tag 号,原因是为了解决回退到某个版本发现有一个坑,这个坑是因为某个依赖库的版本问题,此时我期望最小改动,我虽然能拿到这个库的代码,但是我很难知道我这个版本安装的 NuGet 库对应依赖库的哪个 commit 的代码
在写 asp dotnet core 时,如果没有单元测试保证,需要每个方法都从 web api 的入口开始运行,此时的执行效率是很低的。而如果写单元测试,又有一个坑的问题是写单元测试也是需要时间的。本文告诉大家一些提高效率的方法,这些方法不是正经的用法,但是能提升效率。至于能不能用好不好用就请观众老爷自己决定
本文将从最简单开始,新从全控制台开始创建一个 WebApi 服务,然后再使用 VisualStudio 创建服务
现在是 2020.06.21 我来聊聊我所关注的 dotnet 的几个大开源项目的活跃以及定位情况。包括 WPF 和 WinForms 和 Xamarin 和 ASP.NET Core 和 Blazor 仓库
被微软收购的 GitHub 越来越好用,拥有大量免费的工具资源和构建服务器资源,再加上私有项目的无限制使用,我有大量的项目都在向 GitHub 迁移。通过 GitHub 的 Action 的自动构建,可以用上微软土豪的服务器资源,进行自动化测试和构建。对于 CBB 来说,发布就是打出 NuGet 包然后上传到内部 NuGet 服务器。此时遇到的问题是,如何在 GitHub 上执行打包,打包的时候如何指定 NuGet 包的版本号。因为 CBB 的特殊性,我要求每个 NuGet 正式发布的包都应该有一个对应的 Tag 号,这样将 NuGet 库安装到项目里面,之后发现问题了还能找到对应版本的代码
在解析 PPT 文档的时候,文本是比较复杂的一部分,而因为很多有趣的坑,即使是微软大法也有一些诡异的设计,例如项目符号的是否显示
我现在的团队内部用的是 Gitlab 工具,在此工具上提供了 Gitlab CI CD 用于做自动化测试和构建。对于 CBB 来说,发布就是打出 NuGet 包然后上传到内部 NuGet 服务器。此时遇到的问题是,如何在 Gitlab 上执行打包,打包的时候如何指定 NuGet 包的版本号。因为 CBB 的特殊性,我要求每个 NuGet 正式发布的包都应该有一个对应的 Tag 号,这样将 NuGet 库安装到项目里面,之后发现问题了还能找到对应版本的代码 本文告诉大家如何配合 Gitlab 做自动推 Tag 时打包 NuGet 包。也就是本地打一个 Tag 号,推送到 Gitlab 上,就会出发 Gitlab 的自动构建,自动构建里面将会获取 Tag 版本号,然后打出 NuGet 包推送到服务器
本来程序员的开始应该是从命令行开始,但是能看到博客的小伙伴,预计都了解命令行了。但是如果是一个空命令行,能做的事情实在不够清真,能提升的效率也有限。如何站在巨人的键盘(没写错)上,用大佬写的各个工具提升日常逗比的效率?答案是从安装 dotnet 开始
我们的项目中会包含有很多文件,但是可能我们没有注意到的,我们的文件的编码不一定是 UTF-8 编码,这就可能让构建出来的应用程序在别人电脑运行时出现乱码,或者别人拉下来代码,却发现代码里面的中文都是乱码。为了解决文件编码的问题,咱需要一个编码规范工具,本文将告诉大家在 GitHub 上仓库,可以利用 GitHub 的 Action 部署自动代码文件编码规范的机器人,这个机器人可以自动协助咱规范文件的编码规范。可以设置为每次上传代码的时候,自动帮忙设置文件编码为 UTF-8 编码。或者在每次代码合并到主分支之后,机器人将会尝试修复文件的编码,如存在文件需要修复的,那机器人将会创建一个代码审查
在咱项目里面,大家是否有关注过文件的编码,一个文件是作为 Ascii 编码保存的,还是作为 GBK 编码保存的,还是 UTF8 编码保存的?不同的编码是否对应用有影响?其实是会有影响的,在 C# 里面的字符串常量等都会受到文件编码的影响。例如我的应用主输出是 UTF8 编码,此时我在二进制可执行文件里面保存的输出字符串的二进制是采用 GBK 编码的内容,在一些用户的设备上也许就会显示出乱码 本文来安利大家一个 dotnet 工具,这个工具可以用来协助大家找到项目里面的编码不规范文件
是不是大家也会觉得代码审查里面审查代码格式化问题是无意义的,但是不审查又觉得过不去?是否有个专门的工具人,用来协助修复代码格式化的问题?本文来安利大家一个特别好用的方法,使用 dotnet 完全开源的专业格式化工具 dotnet format 配合 GitHub 的自动构建 Action 做的自动代码格式化机器人,这个机器人可以被指定到特定时机,如每天晚上或者每次代码合并等,进行代码格式化,格式化完成之后,可以选择直接推送或者提代码审查
本文来安利大家一个超强的库,这个库可以让你的 C# 代码利用上 GPU 显卡的性能,进行一些并行计算。这个库是基于 DirectX12GameEngine 的 ComputeSharp 库。在这个库里面将会动态生成 HLSL 代码,使用着色器的方式在 GPU 上跑起来
我在安装 dotnet tool 工具时发现所有的工具都安装失败,全部都提示 Could not find a part of the path 安装失败。我重新安装了 dotnet SDK 也没有用,更新到了3.1.402 版本也没有修复
我有一个上古的库,我使用这个库用来上报日志,而刚才日志服务挂了。然后我就发现了我的应用拒绝响应了,通过 VisualStudio 断点调试可以发现线程池的线程全部被占用了。因为没有可用线程因此所有对 asp dotnet core 应用的访问全部都不会收到响应,为什么我的另一个应用日志服务挂了会让我的业务应用拒绝响应?为什么我的业务应用会使用线程池所有的线程,为什么线程池的所有线程被占用将会让应用拒绝响应
我在写一个文件下载库,这个下载库利用断点续传机制,支持多线程下载一个文件。但是文件写入只能支持单线程,我不想让网络下载需要等待磁盘写入,因此我需要先在内存做缓存,然后让磁盘写入。配合 DirectX 渲染的设计方法,采用双缓存数据结构设计,也就是有两个集合,其中一个集合用来被其他模块写入,另一个集合用来作为当前使用。此时能做到网络下载使用的集合和文件写入的集合不是相同的一个集合,因此两部分的速度差异将不会相互影响
颜色变换是对基础颜色的相关属性的修改。 例如,透明度是与颜色相关的属性。 颜色转换被指定为任何颜色模型标签的子标签,可以在任何颜色加上颜色变换
我在一台设备上安装了 UOS 系统,但是我如何在我的主开发设备上和 UOS 系统传输文件?通过 dotnet tool 工具可以完成大部分的工作,当然,使用 dotnet tool 不仅做文件传输,还能做很多特别强大的应用。本文就使用文件传输作为例子告诉大家如何使用 dotnet tool 在多个平台里面使用相同的一套技术和命令行作为工具
在前面几篇博客告诉大家如何部署 GTK 应用,此时的应用是特别弱的,大概只是到拖控件级。尽管和 WinForms 一样也能写出特别强大的应用,但是为了提升一点开发效率,咱开始使用 xaml 神器写界面。本文告诉大家如何在 UOS 国产系统上,通过 Xamarin.Forms 使用 XAML 写界面逻辑,构建出 GTK 应用
本文告诉大家如何在 UOS 上安装 MonoDevelop 开发工具。本文使用的 UOS 是 UOS 20 x64 版本,这个系统版本是基于 debian 10 的,默认的 MonoDevelop 开发工具是没有提供 UOS 版本,但是有提供 debian 10 版本
先从一个 Hello World 应用开始,试试和古老的 WinForms 一样的拖控件式开发
本文告诉大家如何在 UOS 国产系统上开始使用 MonoDevelop 开发,通过创建 GTK# 应用,进入界面开发的第一步
在使用 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 内之前的容器没有关闭
本文使用的 UOS 是 UOS 20 x64 版本,这个系统版本是基于 debian 10 的。而默认 mono-devel 是没有提供 UOS 版本,但是有提供 debian 10 版本,而 mono-devel 有很多依赖包,因此手动安装不是很靠谱
在 OpenXML 的 PPT 的文本的字体是一个比较复杂的概念,即使在 OpenXML SDK 的辅助下,也需要自己写很多的逻辑。通过 ECMA 376 文档里面散落在各地的描述,和 dotnetCampus.OfficeDocumentZipper 工具的实验帮助之下,我摸到了本文将要告诉大家的规则,以及逻辑的实现
在 Office 里面的文本解析最全的范围是 Word 文本,就是属性数量本身就特别多。本文只是简单和大家聊聊 Office 里面的 PPT 的文本的解析入门。尽管 PPT 的文本也是采用 DrawingDL 的文本属性为主,不过会用到的属性将比 Word 少很多。本文将和小伙伴介绍 PPT 的文本存放的方式
在 OpenXML 里面的文本排版里面使用到 spcPct (Spacing Percent) 和 spcPts(Spacing Points)两个不同的单位用来表示段前空白和段后空白以及行间距
本文将来聊聊 Microsoft.Extensions.DependencyInjection 这个依赖注入框架的 Scoped 功能的一个应用,这个框架是默认 ASP.NET Core 的核心库将会默认被引用。而其他 .NET 的应用如 WPF 或 Xamarin 等也可以使用这个库。因此本文标题就是 dotnet 而不是具体哪个框架 在开发的时候,咱会有一些复杂的逻辑需要多个类合作进行执行,而在使用多个类进行执行的时候,就涉及到上下文信息的传递。例如最简单的追踪 Id 的值,假定在多个类组成的多个步骤里面,因为存在多线程调用的问题,咱在定位问题的时候需要在日志里面输出当前步骤所使用的追踪 Id 是哪个,这样就运行进行并行多次任务同时执行,同时日志不会乱
在我的博客里面会添加很多引用,但是有一大部分的链接失修,访问的时候访问不到了,或者需要更新。于是我写了一个工具,可以协助找到所有的已失败的链接
最近需要支持国产的 UOS 系统,这个系统我采用了 Xamarin 加上 GTK 开发,而我的日志系统有一个功能是记录日志的时候传入当前的文件路径,此时 NuGet 包是在 Windows 下构建的,因此传入的路径是 Window 构建服务器路径。此时在 Linux 上尝试获取文件名就炸了
使用 Thread.GetCurrentProcessorId 可以获取当前线程处理器的 Id 是哪个,而通过 Process.GetCurrentProcess().Id 可以获取当前进程的 Id 号,这两个的差别从上面描述就能看出
我尝试找到一个 B 树的实现,尽管这个东西十分简单,但是依然网上有很多不同的版本。我在 justcoding121 的版本上魔改了一下,就是本文可以用来给大家的版本
我在给 dotnet 的 runtime 仓库提PR时,小伙伴告诉我可以使用 TryAdd 方法减少判断,但是我修改这个代码发现 100 个自动化测试都失败了,都告诉我没有找到这个方法
在 CLR 中提供这样的支持,某个类从程序集 A 放到了程序集 B 里面,可以通过一些黑科技让类型就像原本就在程序集 A 一样。用这个方法可以比较好的解决兼容性的问题
在 dotnet 里面可以通过安装 System.Text.Encodings.Web 库拿到 UnicodeRanges 这个包含了 Unicode 标准的平面映射。但是我还没有找到如何判断一个字符是落在哪个平面的逻辑,本文就告诉大家一个可以使用的方法,这个方法同时稍微兼顾了性能
本文用的是 世纪互联 的 Azure.cn 版本,这个版本因为是在国内,所以网速会快超级超级多。使用 世纪互联 的版本需要一块钱哦,用一块钱就能进入一个月的免费试用。本文主要告诉小伙伴如何使用 Azure 无服务器 Function 函数计算服务
在分析 DUMP 进行自动化调试的时候,很多时候只能通过 WinDbg 和命令行调用的方式,这样的方式很难做到灵活。同时编写各个命令行的难度也特别高,这在需要对命令行的输出进行不同的分支的判断时候,难度会更大。于是找到了 Microsoft.Diagnostics.Runtime 库,这个库提供了简单的方式,可以在 C# 里面用代码写分析 DUMP 的代码
在 OpenXML SDK 里面对于很多值,如字符串等的值,都包含一个 HasValue 属性。刚入门的小伙伴会认为这就是一个和可空类型一样的属性,表示这个属性是不是为空。其实这句话只是对一半,其实这个属性表示的是在重新定义的字符串参数里面的属性是否为空
我在写一个有趣的 WPF 应用,我想要测试这个 WPF 应用的一个功能,这个功能就是一键点击自动推送 NuGet 包到服务器。我想要做一点自动化的测试,我需要有某个假装是 NuGet 的服务器用来接收我这个应用推送的 NuGet 包。用 ASP.NET Core 写一个假装的 NuGet 服务器,支持被 NuGet 推送包是特别简单的,本文就来和大家说说这个后台如何写
在 2013 微软开源了 OpenXml 解析库,在微软的 Excel 文档,使用的文档格式就是国际规范的 OpenXml 格式。这个格式有很多版本,详细请看百度。因为表格文稿使用的是 OpenXml 在 .NET 开发可以非常简单将 Excel 文档进行解析,大概只需要两句话
在 C# 里面配合 dotnet 的 Task 可以作出 AsyncAutoResetEvent 高性能多线程工具,从命名可以看到 AsyncAutoResetEvent 的意思就是支持异步的自动线程等待事件,用于多线程竞争访问执行权,可以用在消费队列或用在限制有限线程执行的业务上
在阅读本文之前,我期望你能了解基础的 PPT 解析内容,或看我的入门级博客。本文将告诉大家如何从 PPT 里面解析出通用元素的 x 和 y 的值,以及元素的宽度和高度的值
我不能用官方的镜像,因为我需要在小伙伴构建的 debian 镜像上面安装 dotnet sdk 用来做构建,其实在 docker 里面需要找到一个个文件,然后复制代码
我写了一个有趣的工具,我如何将这个工具给到小伙伴予力众生呢?只需要设定这个工具是 dotnet tool 工具就可以通过 NuGet 分发出去啦。几乎所有的 dotnet 开发者都能用上 NuGet 服务,也就是此工具可以被几乎所有的 dotnet 开发者下载使用。那么制作难度有多大呢?基本上有一个现成的项目前提下,只需两句代码,一句命令行,就能完成制作
在开发 dotnet tool 时,我将规范编码的库作为 dotnet tool 发布,但是在发布的时候本地进行安装提示DotnetToolReference 项目类型仅可包含 DotnetTool 类 型的引用
我在团队内的几乎所有 dotnet 项目,包括 UWP 和 WPF 桌面端以及 Xamarin 移动端和 ASP.NET Core 后端等需要用到多语言的项目,我的多语言都是通过 YAML 写的,这样相对来说在项目比较小的时候维护方便。但是 YAML 写的文件要读取需要用到 YAML 解析等,这部分的解析速度不够快,于是我就写了一个工具,用于在软件构建的时候自动将 YAML 多语言文件构建为代码。这样不仅能提升软件的执行速度,还能减少软件发布时需要带出去 YAML 解析库
在当前的 IT 领域,我推荐每个开发设备都应该安装 dotnet 这个工具,在 dotnet 这个工具上有大量开发者提供了无数好用的工具。本文要告诉小伙伴的工具是 natemcmaster 大佬提供了 serve 工具,可以用来开启本地文件服务器,使用非常简单。这个方案适合在 Windows 系统和 Linux 系统和 MAC 上使用,发布的 HTTP 服务可以在任何能访问到的设备上使用
在使用 using 等新语法时,在 VisualStudio 2019 会自动判断框架版本,如在 net 45 就不会自动使用最新版本的语法,需要修改项目文件
我在写一个简单的文件服务器,想要用来做客户端下载器的测试服务器,但是返回的方法提示 ObjectDisposedException: Cannot access a disposed object. Object name: ‘Cannot access a closed file.’ 原因是我的文件被释放
在 Linux 等系统下,没有和 Windows 下这么好的 VisualStudio 支持。在客户端的环境,也不太好在用户端安装一个 VisualStudio 调试。在遇到需要在服务器端或客户端调试应用程序的内存占用时,可以尝试使用 dotnet 的 gcdump 工具进行调试,这个工具使用十分简单,功能也很强大
我需要对一个文件做二进制差分压缩,我有一个文件的起始点,在之后的每次更改我都记录文件的二进制的差分,这样就可以通过起始点和差分文件计算修改后的文件。通过二进制差分可以用来提高文件保存磁盘读写速度,也可以减少软件自动更新需要的文件大小
在进行文件读写时,如果进行单元测试,是需要很多设计,因为本地的文件可能因为单元测试之间的占用,以及还原数据,会影响业务。本文告诉大家使用注入的方式,让文件读写一个抽象的对象
本文收藏我所了解的从 dotnet core 3 到 2020.11.10 发布的 dotnet 5 的 WPF 的变更
我有一个强大的功能,这个功能就是在 Linux 下使用 GDI 转换 EMF 格式图片为 png 图片,但是有一些有趣的图片会让转换的进程炸掉。因此我就想让转换服务放在独立的进程,通过进程间调用,也就是命令行调用传入参数的方式,让另一个进程转换图片。而此时就会遇到一个问题,如何让这个进程也被构建,然后输出到输出路径
在做日志库设计的时候,我会特别考虑日志里面需要带上时间和 TracerId 和 SessionId 两个属性,本文告诉大家带上这两个属性的意义和优势
在 Office 的文本排版里面,会根据字符选择使用哪个字体插槽。也就是实际上在 Office 里面可以在一个文本段里面指定多个字体,会根据实际的字符使用不同的字体
默认在列表以及字典哈希这些都会先看对象是否有自己实现的等于判断,如果有就调用对象的。因此对象可以做到两个不同的对象返回相等。而如果需要判断对象引用相等,也就是只有相同的对象才返回相等,此时需要用到 ReferenceEquals 方法。这个判断方法是最快的判断相等的方法,只有在传入的两个参数是相同的对象的时候才会返回 true 的值
在构建提示 Error CS0012 和 Error CS0246 说找不到 CompositionContainer 类,原因是没有引用 System.ComponentModel.Composition 库
之前版本是通过安装 Blend SDK 支持 Behaviors 库的,但是这个方法都是通过引用 dll 的方式,不够优雅。在升级到 dotnet core 3.0 的时候就需要使用 WPF 官方团队开源的 Microsoft.Xaml.Behaviors.Wpf 库代替
在 2017 我在社区问了一个问题,如何让 .NET Framework 的 WPF 等程序使用 .NET Native 构建以提升速度。在 2019.06 的时候,强大的微软提供了一个好用的库,支持将 .NET Framework 的桌面应用构建时添加 native images 本机映像支持
我昨天将 Mac 构建机器也升级到了 dontet 5 最新版。但是在升级之后,所有的 Xamarin 项目都在 Mac 版本的 VisualStudio 构建不通过,提示 error MSB4186: 静态方法调用语法无效。解决方法就是将 VisualStudio 更新到最新版本
在很多大团队开发的时候,将需要用到很多项目的组合开发一个软件,一个软件需要用到的项目有很多个,很少会用到一个项目就能做到。但多个项目一起开发,在配置管理和团队管理有不同的策略,一个就是让项目拆分为多个代码仓库,另一个就是将这些项目合在一个代码仓库。两个策略不能说哪个更好,本文和大家分享我所在的团队和我参与的其他团队的策略
是否有小伙伴好奇如果没有在代码调用垃圾回收,那么框架会在什么时候调用垃圾回收。本文是读还没出版的伟民哥翻译的 .NET内存管理宝典 - 提高代码质量、性能和可扩展性 这本书的笔记
假定我有一个文件的路径,我将这个文件路径放在文件夹判断方法里面,请问此时返回的是存在还是不存在?答案是返回不存在
咱今天来聊聊一个毁灭世界的故事,哦,不,是一个使用 TaskCompletionSource 让偷核武器,哦,又说错了,是让事件转换为异步的方法,让咱可以在一个方法里面顺序写下毁灭世界的逻辑
使用 Gitlab 的 CI 但是任务没有执行,提示找不到 Runner 或者找错了 Runner 服务器,请看本文,从上到下看,是否有坑
在 dotnet 中,通过 dotnetCampus.ClrAttachedProperty 库,可以实现给任意对象附加任意属性。以及实现创建对象的附加属性定义,使用相同的附加属性定义才能访问相同的附加属性值。在使用过 WPF 的小伙伴一定对附加属性不陌生,在 WPF 框架中很强大的一个功能就是附加属性,而针对于 WPF 框架外的 dotnet 其实也能做到相同的设计,支持定义对象附加属性
在 C# 里面,可以使用 dotnet 的 TaskCompletionSource 方法自己实现一个异步任务,这个任务可以方便设置任务是否完成等做到让等待的过程继续或等待。根据这个功能可以解决在复杂的功能里面通过异步等待的方法实现暂停。做法就是等待一个异步任务,如果这个功能处于暂停,那么只需要让这个异步任务不结束,此时整个功能就会在等待,直到设置了异步任务完成
今天在少珺小伙伴的协助下,使用了 gitlab 的 runner 给全组的项目做自动的构建。为什么需要使用 Gitlab 的 Runner 做自动构建,原因是之前是用的是 Jenkins 而新建一个底层库项目想要接入自动构建等,需要来回在 Gitlab 和 Jenkins 上配置,大概步骤差不多有 20 步,同时还有一堆 Jenkins 的坑。另外服务器是共有的,有其他组的小伙伴安装了诡异的工具让我的打包不断炸掉。于是我就和头像大人商量使用虚拟机环境的方法,我在空闲的服务器上安装了 VirtualBox 虚拟机,然后在虚拟机部署 Runner 接着在项目接入,这样就可以确定打包的环境,同时迁移服务器也比较方便
其实 dotnet 是全栈的首选,原因是因为可以开发的方向太多,比如大本营PC端,以及后台。还有移动端,包括 IOS 和安卓端。现在还能用来写前端,本文就来告诉大家如何在前端使用现有的C#代码,通过 WebAssembly 使用 C# 的代码支持完全静态的网页,也就是不需要任何后台的存在。同时使用 C# 编写的 WebAssembly 可以省去 js 编译时间,同时使用二进制的本地指令,运行效率也有极大的提升。兼顾了开发的友好以及更高的性能
今天是 2020.11.13 我在 CI 服务器上更新 dotnet 到 dotnet 5 以及 VS 到 16.8.1 最新版本,但是我在刚刚不得不回滚了环境…… 因为构建不通过
在 dotnet 里面,可以使用 Interlocked 进行原子命令更改 int 等的值,利用这个特性可以在一个固定足够长长度的数组里面,让多线程无锁等待写入值。因为没有锁的存在,无法保证读取时的安全,因此这样的集合只能被设计为只写的集合,只有在业务上完成了所有的写之后,才能作为可读的集合取出来
在将原本的 dotnet framework 版本的 WPF 项目迁到 dotnet core 版本,在进行单元测试,发现在 XmlSerializer 抛出了 System.Private.CoreLib.XmlSerializers.dll 找不到的异常,其实这个只是在 XmlSerializer 的内部异常,可以忽略
在本机相同进程中创建生产者消费者队列,可以解决很多线程安全以及高性能需求问题。本文告诉大家如何通过在 GitHub 完全开源的 AsyncWorkerCollection 库的 AsyncQueue 类创建在内存中的高性能低资源占用的生产者消费者队列
在 dotnet 开源的 runtime 运行时仓库里面,有微软的大佬说运行时仓库的代码应该减少使用 Linq 语句,那这又是为什么呢
小伙伴做了一个很好用的 dotnet tool 工具,但是这个工具仅在他的设备上能运行,在我的设备上运行就会退出提示 An assembly specified in the application dependencies manifest (LindexiDoubi.deps.json) was not found 找不到依赖
在 OpenXML 文档格式里面,所有的资源以及页面之间的引用等,都是通过 Relationship 的引用,如资源需要通过 GetReferenceRelationship 的方法才能拿到。那为什么要这样设计呢
在使用 dotnet 打 NuGet 包的时候会发现是每个项目都生成独立的一个 NuGet 包,通过 NuGet 引用依赖指向依赖的项目。那为什么不是我最终只打出一个 NuGet 包,这个 NuGet 包,包含了所有的项目的输出文件?每个项目独立输出是为了解决什么问题?下面让我为你解答
本文和大家介绍一个使用超级简单,但是功能特别强大的自然语言关键词提取库,可以根据输入的自然语言提取出里面的信息。例如我在一句话里面说了哪些数值变量或者说了手机号码等
在 Office 文档的一些有趣的设计,颜色和画刷是可以继承的,这个继承包括了属性的继承。在形状填充里面使用的渐变色是可以一部分属性放在主题里面,主要找到主题里面的画刷,替换掉形状自己定义的内容,才是形状的画刷
现在可以使用 .NET 构建一切应用。富有生产力的 .NET 是当前数百万的开发者选择这个平台的理由。在 .NET 5 微软统一了整个 .NET 平台,将 .NET Core 和 Mono 以及基于 Mono 的 Xamarin 放在一起,于是就可以使用相同的一个 BCL 基础库和 SDK 工具链
在使用 OpenXML SDK 解析 PPT 文档的文本占位符的时候,需要对 PPT 的格式有一定的了解,尽管整个 OpenXML SDK 包括文档等都很详细。但是有一些细节文档上虽然有写,但是没有强调一下,就被我忽略了
最近很火的是国产的操作系统 UOS 系统,这个系统现在存在的问题就是生态没搭建起来。作为 dotnet 开发者,可以通过很多不同方向的技术为 UOS 系统生态贡献应用,如 Xamarin 和 GTK# 或 wine 和 win32 应用,或 mono 和 WinForms 应用等。本文将安利大家使用 Avalonia 框架开发 UOS 上的原生应用
现在小伙伴说的人工智能都是弱智能,可以基于神经网络来做。而神经网络是有多层网络,每一层网络都有多个神经元。那么最简单的神经网络就是只有一层,而这一层只有一个神经元,也就是整个神经网络只是有一个神经元。一个神经元可以用来做什么?可以用来做基础的与或逻辑运算器。在我没有告诉神经元与或的逻辑时,只是通过我传输的输入的值和输出的值,此时神经元经过训练就能自己学会与或的逻辑。本文就在不使用现成的人工智能框架下一步步和大家写这一个神经元和告诉大家这是如何运行的。本文特别适合小伙伴入门神经网络,或者适合小伙伴入手写代码
小伙伴都知道,使用 JS 的坑在于执行效率过低,速度过慢。如果是在客户端中,还开启一个浏览器,整个应用程序就会特别重。有没有什么方法可以让 dotnet 作为容器,执行 JScript 代码而不需要浏览器?其实有的,因为 VBScript 和 JS 代码都很好解析,所以有 ClearScript 支持使用 dotnet 解析代码执行
在 SublimeText 搭建 C# 环境可以找到的博客基本都是使用 csc 进行构建,而我期望在 dotnet 下编译整个项目。通过 dotnet 编译整个项目可以解决编译大项目时需要打开一个控制台降低效率
现在 dotnet 的服务创建十分具有效率,本文的前提要求是电脑上面已经安装了 dotnet 程序,接下来就是三句命令行的事情
在 MAC 系统下,如果在运行过程中,应用程序的文件被删除了,那么此时如果应用程序执行了 Process.Start 方法时,将会抛出 Win32Exception 异常
本文简单告诉大家如何在 .NET Core 里面使用 AOP 切面编程,使用 DispatchProxy 做任意接口的动态代理实现
在使用 dotnet 的时候,有时候会期望某个类多添加一个属性,但是这个类可能是放在引用库里面不能直接修改,或者添加一个属性会影响这个类的设计。那么有没有方法和 WPF 一样支持给任意对象附加任意的属性?本文告诉大家一个好用的库,用来给任意的对象附加任意的属性
在 dotnet 中有一个特殊的类,这个类能够做到附加属性一样的功能。也就是给某个对象附加一个属性,当这个对象被回收的时候,自然解除附加的属性的对象的引用。本文就来聊聊这个类的底层原理
在使用 dotnet core 版本的 WPF 可以将发布文件打包为一个exe文件,但是此时小伙伴发现 log4net 无法运行,因为 log4net 说找不到配置文件
在 WPF 下,可以使用和 UWP 一样的 Pointer 触摸架构,只是开启的方式和 .NET Framework 版本有细微的差异
这是一个套娃,在编译 Rolsyn 编译器之前需要有 Rolsyn 编译器,用 Rolsyn 编译器来编译 Rolsyn 编译器。本文来告诉大家如何构建 Rolsyn 编译器,步骤特别少,很简单
在 Windows 下,想要获取指定进程或所有进程的命令行参数,此时需要一些工具的辅助。本文安利大家一个好用的 dotnet 工具,用于获取 Win32 进程的命令行参数
本文来安利大家一个我做的好用的工具,这个工具可以更新某个文件夹下所有 Git 仓库,通过调用命令行的 Git 命令实现更新。这是一个 dotnet tool 工具,所有代码在 GitHub 完全开源
在使用 dotnet 构建的时候提示 error : SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true 构建失败
如果一个项目想要让其他某个指定的项目可以使用到 internal 的类或成员,可以通过标记 InternalsVisibleToAttribute 的方式实现
在创建一个 TaskCompletionSource 期望让等待的逻辑只会被调用一次,而调用的是多线程,可以使用 TrySetResult 方法,这个方法是线程安全,只会让 TaskCompletionSource 被调用一次
默认在 dotnet 里面框架提供了 Microsoft.Extensions.Logging 可以和依赖注入做日志框架,而有些业务,如需要自己定制日志行为,此时就需要定制日志
现在依靠 dotnet 平台,可以方便分发工具,利用 NuGet 服务进行分发和使用工具。打一个 dotnet tool 包,可以将这个包上传到 NuGet 上,小伙伴通过和安装 NuGet 相同方式就可以将工具安装在本机上。本文将告诉大家如何手工打一个 dotnet tool 包,方便小伙伴自己写工具用来创建代码
我在协助小伙伴调试一个只有在 GitHub 的 Action 自动测试时才会炸的问题,而我发现默认的控制台输出是不会在 GitHub 的 Action 显示的,换句话说,在使用 dotnet test 时,代码里面使用的控制台输出不会进行输出
随着 .NET 5 的发布,我在给团队内部打一个特殊的 .NET 5 的 docker image 过程,发现了提示 Insufficient space in download directory 磁盘空间不足
本文将告诉大家在 SmartSql 的 SQL 语句的属性前缀 ParameterPrefix 的默认值和用法以及原理
我想要在 UOS 上支持上古的图片格式,也就是差不多废弃了 20 年的 EMF 和 WMF 增强图形格式,这两个格式十分古老,而在 Windows 下也存在一些不兼容的图片。我在 Windows 下是使用 GDI+ 的方法支持的,可以将 EMF 转 PNG 或 jpg 等格式。而在 UOS 下,因为 GDI+ 是跨平台的,可以使用跨平台的 System.Drawing.Common 库进行转换
使用 dotnet tool 可以方便分发给小伙伴很多有趣的工具,而小伙伴们也可以通过 dotnet tool 一句命令安装上强大的工具。但有一个问题是,我如何知道有这样的工具? 当前可以在 nuget.org 上搜寻到 dotnet tool 工具
我从 dotnet core 1 的版本到 3.1 的版本,中间安装了超级多的预览版,此时我的硬盘已经空间不够了。干的漂亮的 dotnet 提供了 .NET Core 卸载工具专门用来卸载 .NET Core 的 SDK 和运行时
在开发库以及框架的时候,持续维护会遇到兼容性的问题,如发现了旧版本有一些接口设计不合理,或者方法命名不符合逻辑等。此时如果直接更改原有的属性名或方法名甚至类名等,将会导致上层业务的开发者们在升级库之后构建不通过,因为缺少对应的方法。此时就需要上层业务的开发者们查阅文档才能了解如何应对升级之后带来的变动
在发布 CBB 作为 NuGet 包的时候,我期望开发者在使用我的库进行调试,可以自动链接代码到对应打包的 GitHub 上的代码,可以从本地拿到对应的源代码进行调试。这样的调试方式对于开源项目来说,将会很方便
在使用 SemaphoreSlim 这个锁,能做到的是指定让任务执行几次,同时提供异步方法,减少线程占用。但异步的方法如果没有用对,会因为异步状态机的引用,而存在内存泄露
做 Office 解析,是需要进行不断的测试才能了解 OpenXML 里面的属性的作用。根据 Ecma 376 的定义,文档其实只是一个压缩文件,可以使用压缩工具进行解压缩。但是我需要不断进行修改文档里面的属性,然后用 Office 打开,测试属性的效果,此时就需要有一个工具用来提升效率
本文来告诉大家在解析 PPT 文档的时候,元素继承以及占位符需要用到的 Slide Master 和 Slide Layout 是什么
在使用 OpenXML SDK 进行 Office 文档的解析时,对幻灯片 PPTX 文档的页面解析也许会遇到页面顺序的问题,本文告诉大家在 Office 文档里面页面的序号和顺序之间的关系以及如何读取页面序号
颜色是一个大的主题,在 ECMA 376 里面用了 19 页 A4 描述了颜色,但仅是简单的描述。在 OpenXML 定义了 Scheme Color (schemeClr) 是用来表示主题的颜色,可以跟随主题的更改而更改颜色。例如我的文本设置为主题的文本颜色,那么在我更改文档主题的文本色就可以更改我的文本颜色
在 OpenXML 格式里面,其实不存在文本这个元素,所有都是形状。但是在 PPT 界面看到的文本框是什么呢?其实他是特别的形状。而几乎所有的形状都可以输入文本,因此区分形状和文本的意义不会特别大,只是在做解析的时候才会碰到
在 PPT 和 Word 排版里面,小伙伴会接触到 Kerning 字间距这个属性,本文将告诉大家这个属性的功能,以及为什么需要在 OpenXml 里面设置这个属性
使用 OpenXML 的格式的 Office 文档的元素,使用 p:cNvPr Non-Visual Drawing Properties 的 Id 属性作为标识,在标准协议这个标识是唯一的,但实际很多文档都存在重复的标识。本文告诉大家在使用 Office 2016 版本测试重复 id 的行为
本文收集元素属性 cNvPr Non-Visual Drawing Properties 的属性的作用
在 dotnet 的输出路径里面,可以看到有一个有趣的可执行文件,这个可执行文件是如何在框架发布和独立发布的时候,找到 dotnet 程序的运行时的,这个可执行文件里面包含了哪些内容
我在 Office 的 Open-XML-SDK 库里面找到有代码线程不安全,代码里面使用了 TryGetValue 加 TryAdd 的方法添加对象,而线程安全的方法是通过 GetOrAdd 方法。不过在小伙伴的评论我找到了 GetOrAdd 性能其实在有闭包的时候不如使用 TryGetValue 加 TryAdd 调用这两个方法,但是 GetOrAdd 的优势在于能做到只创建一次对象
本文将安利大家一个好用的工具,用来解决这样的问题,我有一个任务,要求这个任务在执行过程中不能被重入,只有在任务执行完成之后才能重置状态重新执行一次。换句话说就是在此任务正在执行过程中,不能重复进入此任务。同时在任务执行过程中,不能重置任务状态。在任务执行完成之后,可以保存任务的状态,直接返回任务结果。在任务执行完成之后,可以调用重置状态方法,让任务可以再次重新调用
昨天鹏飞哥问了我一个问题,为什么在控制台程序的主线程等待某个线程执行完成之后回来,是在其他线程执行的。而 WPF 在等待某个线程执行完成之后,可以回到主线程执行。其实这是因为在 WPF 和 WinForms 和 ASP.NET 框架里面都自己实现了线程同步上下文,通过线程同步上下文做到调度线程执行。本文就来和小伙伴聊一下如何自己实现一个线程同步上下文
本文告诉大家在 OpenXML SDK 里面文本框的文本带自动编号的项目符号,不同的编号在 OpenXML SDK 上的枚举值
本文来安利大家一个好用的库 AsyncEx 库。这个库有一个强大的功能是支持 AsyncLock 异步锁。小伙伴都知道,在 C# 里面的 lock 关键字,在 2020 年也就是 C# 9.0 都没有让这个关键字能支持锁内部添加异步方法。此时在一些需要做异步线程安全的业务就比较坑了,而 AsyncEx 库提供了 AsyncLock 异步锁的功能,可以支持在锁里面使用异步
在代码审查 WPF 仓库的时候,小伙伴告诉我说使用 dotnet core 版本的 WPF 开了 ReadyToRun 的提升还不够大,他准备开始一大波业务无关的优化,其中就包含了 xaml 中的字符串相关优化。我在 davidwrighton 大大的优化代码和 pentp 大大的代码审查里面学到了使用 startIndex 提升 IndexOf 的性能,本文就来和大家分享一下
我有一个古老的 dotnet core 3.1 的 asp dotnet core 项目,现在我准备将他升级到 dotnet 5 了。但是我不想和博客园一样翻车,因此我需要做一点集成测试的辅助,尽管依然还是翻车了,但是我要学习博客园伟大的精神,将在这个项目里面所做的所有自动化测试项目的方法写下来
本文是分析 .NET Framework 源代码的系列,主要告诉大家微软做笔迹用的思路,怎么做的笔迹才是高性能的,用户体验比较好的。我会告诉大家源代码的思想,当然这个文章会比较无聊。如果你是想做笔迹的,即使不是 WPF 开发,不是 C# 开发的,也可以看看,因为这个思想是微软的,相对还是比较好的。
本文是分析 .net Framework 源代码的系列,主要告诉大家微软做 ScrollViewer 的思路,分析很简单。 看完本文,可以学会如何写一个 ScrollViewer ,如何定义一个 IScrollInfo 或者给他滚动添加动画
大家在使用多线程的时候,是否有关注过线程安全的问题。如果咱的代码在使用多线程时,在相同的时间有多个线程同时执行相同的方法,此时也许就存在数据安全的问题,如多个线程之间对相同的内存进行同时的读取和修改。而让方法在多线程调用中,相同的时间会被多个线程同时执行某段代码逻辑的技术称为方法重入调用技术,而禁止方法被同时调用也就是禁止方法重入调用。在 dotnet 里面有多个方式可以做到禁止方法重入调用,本文将告诉大家如何做到禁止方法重入调用
在网络编程可以使用 PostAsJsonAsync 这个方便的方法将一个对象作为 json 推送到服务器,这个方法是一个扩展方法,在之前的框架,可以在 System.Net.Http.dll 找到这个好用的扩展方法,但是在 4.5 的时候就被移除了。本文告诉大家如何安装这个扩展方法
本文聊的 Word 是 docx 格式,这个格式遵循 ECAM 376 全球标准,使用的格式是 OpenXML 格式,在 2013 微软开源了 OpenXml 解析库。这个库里面包含了海量代码,可以使用 MB 计算的代码量,通过这个解析库,咱可以使用几行代码完成对 Word 文件的解析,从文件到内存模型
在 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
在 asp dotnet core 3.0 默认的 webapi 返回接口都是返回 json 格式,同时这个 json 格式使用的是 CamelCase 属性名风格。如果想要兼容之前的格式,让 webapi 返回的 json 的属性名使用 PascalCase 格式,那么请看本文
在 2013 微软开源了 OpenXml 解析库,在微软的 PPTX 文档,使用的文档格式就是国际规范的 OpenXml 格式。这个格式有很多版本,详细请看百度。因为演示文稿使用的是 OpenXml 在 .NET 开发可以非常简单将 PowerPoint 文档进行解析,大概只需要两句话
看到标题的小伙伴是不是想知道什么是魔法书,如果你需要写一段代码,这段代码是在做神奇的业务,只有你查询到了魔法书你才能找到这个对象,同时你还需要实现自己的接口,通过自己实现的接口调用才能用到有趣的方法
在 sdk style 的项目格式支持使用多框架开发,此时需要在代码里面通过宏判断,在编译的时候执行不同的代码。本文告诉大家在框架里面对应的预定义的条件编译符有哪些
其实MatterMost是有插件可以用来订阅RSS博客的,但是这个订阅插件需要一个个网址输入,而不是一次性输入很多个,于是我去就自己写了一个控制台程序做订阅
本文将会从简单到高级,告诉大家如何调试 dotnet 的代码,特别是桌面端。本文将会使用到 VisualStudio 大量的功能,通过各种好用的功能提高调试方法
有一些程序是不想通过管理员权限运行的,因为在很多文件的读写,如果用了管理员权限程序写入的程序,其他普通权限的程序是无法直接访问的。 本文告诉大家如何判断当前的程序是通过管理员权限运行,然后通过资源管理器使用普通权限运行
在 dotnet 有很多方法可以获取当前程序所在的路径,但是这些方法获取到的路径有一点不相同,特别是在工作路径不是当前的程序所在的路径的时候
在 dotnet 可以使用 Take 获取指定数量的元素,获取顺序是从前向后,而获取到的数量是小于等于传入的指定数量。如数组中元素的数量小于传入的指定数量,则返回数组中的所有元素。如果数组中元素的数量大于等于传入的数量,则按照数组或列表顺序返回指定数量的元素
我在写一个简单的功能,需要将两个不同的数组合并到一起,但是我的功能只是做只读,如果合并的方法需要申请额外的内存空间,将降低性能。本文写了一个简单的方法,通过判断下标的方法做遍历多个数组组合在一起,通过判断当前获取的下标在对应哪个数组下标范围内,返回对应数组的元素
如果你使用的是旧版本的 Visual Studio,那么默认的代码分析规则集是“最小建议规则集”。基于这个,写出来的代码其实只能说是能跑通过而已。随着 Roslyn 的发布,带来了越来越多更强大的代码分析器,可以为编写高质量的代码带来更多的帮助。
作为 .NET/C# 开发者,强烈建议安装本文推荐的几款代码分析器。
这里的分析器分为 Visual Studio 扩展形式的分析器和 NuGet 包形式的分析器。
Visual Studio 扩展形式的分析器可以让你一次安装对所有项目生效,但缺点是不能影响编译过程,只能作为在 Visual Studio 中编写代码时给出提示。
NuGet 包形式的分析器可以让某个项目中的所有成员享受到同样的代码分析提示(无论是否安装插件),但缺点是仅针对单个项目生效。
上图生效的分析器就是 Visual Studio 2019 自带的分析器。在可能有问题的代码上,Visual Studio 的代码编辑器会显示一些文字效果来提醒你代码问题。比如这张图就是提示私有成员 Foo
未使用。
Visual Studio 2019 自带的分析器的诊断 ID 都是以 IDE
开头,因此你可以通过这个前缀来区分是否是 Visual Studio 2019 自带的分析器提示的。
另外,自带的分析器可谓非常强大,除了以上这种提示之外,还可以提示一些重复代码的修改。比如你修改了某段代码,它会提示你相似的代码也可能需要修改。
Microsoft Code Analysis 分为两种用法,一个是 Visual Studio 扩展的形式,你可以去这里下载安装或者去 Visual Studio 的扩展管理界面搜索安装;另一个是 NuGet 包的形式,你可以直接在项目的 NuGet 管理界面安装 Microsoft.CodeAnalysis.FxCopAnalyzers。
这款分析器也是微软主推的代码分析器,可以分析 API 设计问题、全球化与本地化问题、稳定性问题、性能问题、安全性问题、代码使用问题等非常多的种类。
比如下图是稳定性的一个问题,直接 catch
了一个 Exception
基类:
虽然你可以通过配置规则严重性来消除提示,但是这样写通常代码也比较容易出现一些诡异的问题而难以定位。
Microsoft Code Analysis 分析器的诊断 ID 都是以 CA
开头,因此你可以通过这个前缀来区分是否是 Microsoft Code Analysis 分析器提示的。
Microsoft.CodeAnalysis.FxCopAnalyzers 的 NuGet 包实际上是一组分析器的合集,包括:
如果你想安装这款 NuGet 包,并不需要特别去 NuGet 包管理器中安装,也不需要命令行,只需要去项目的属性页面,选择“安装”就好了。如下图:
是第三方开发者开发的,代码已在 GitHub 上开源,社区非常活跃:
提供了 500 多个代码分析和重构。更值得推荐的一个原因是他为 Visual Studio 原本的很多报告了问题的代码提供了生成解决问题代码的能力。
Code Cracker 是第三方开发者开发的,代码已在 GitHub 上开源:
由于这款分析器的出现比 Visual Studio 2019 早很多,所以待 Visual Studio 2019 出现的时候,他们已经出现了一些规则的重复(意味着你可能同一个问题会被 Visual Studio 报一次,又被 Code Cracker 报一次)。
虽然部分重复,但 Code Cracker 依然提供了很多 Visual Studio 2019 和 Microsoft Code Analysis 都没有带的代码质量提示。
比如,如果你代码中的文档注释缺少了某个参数的注释,那么它会给出提示:
Code Cracker 支持的所有种类的代码分析都可以在这里查得到:
这款插件是对其他几款分析器的重要补充。如果说其他几款分析器可以帮你解决一些基本设计问题或者 Bug 的话,这款分析器可以帮你发现更大范围的问题。
最典型的,也是我推荐这款分析器的最大原因是 —— 区域和本地化!
你的每一个 ToString()
,每一个字符串比较,每一个字典的构造……他都提醒你需要考虑区域问题,然后提供给你区域问题的推荐代码!
你的项目中对于某项规则严重性的看法也许跟微软或其他第三方分析器不一样,因此你需要自己配置规则集的严重性。
关于如何配置代码分析严重程度,你可以阅读:
我是一个小白,但我想做全栈开发,我想从桌面端写到移动端,想从后台写到前端。但是我不想学习,怎么破。没关系,用 dotnet 做,这个技术方向的教程文档特别齐全,入门难度超级低。同时各个方向的开发都是差不多的,至少在 VisualStudio 安装完成之后,几乎哪个方向都能在半个小时之内做自己的修改跑起来
在打开一个 sln 项目包含 dotnet core 的时候,可能在打开的时候提示找不到 sdk 一般是没有在安装的时候安装对应的开发
在 dotnet core 3.0 为了支持 IEEE 浮点数计算标准,修改了 Math.Max 的算法,于是在 WPF 的 Track 里面的布局依赖于之前的计算,于是在 dotnet core 3.0 的修改就让布局计算不对了。改动现有 API 的行为会让现有的代码出现不兼容问题,那么要让一个框架能稳定支持升级需要满足什么条件
本文是一个非常简单的博客,让大家知道如何使用 asp dot net core 做后台,使用 UWP 或 WPF 等做前台。 本文因为没有什么业务,也不想做管理系统,所以看到起来是很简单。
本文告诉大家如何在 UWP 或 WPF 客户端通过将类转换为 json 发送到 asp dotnet core 作为方法的参数
在使用 sdk style 的 csproj 项目格式,会发现右击引用找不到程序集,此时有一些命名空间没有找到。本文收集一些命名空间所在的引用
在使用 dotnet 的 sdk style 项目格式,会默认在项目上引用文件,此时如果是从旧格式迁移,那么会发现文件冲突。多次引用相同文件
在写命令行程序的时候,会遇到命令行解析的问题,以及参数的使用和规范化等坑。现在社区开源了命令行项目,可以帮助小伙伴快速开发命令行程序,支持自动的命令行解析和规范的参数
本文告诉大家如果复制网上一段代码发现 System.ServiceProcess 提示找不到方法或定义,需要手动添加引用
在写 DateTime 排序时,按照时间的先后,离现在过去越远的越小。按照从小到大排序,将会先排最过去的时间,最后的值的时间是最大的。将时间按照从1970开始计算秒数,可以算出数值,数值代表值大小
如果在开发的时候遇到非泛型 类型“IEnumerable”不能与类型参数一起使用,那么就是变量的命名空间没弄对
在 dotnet 程序提供了一个好用的特性,可以让字段作为线程的静态字段,也就是在相同线程的所有代码访问的静态字段是相同对象,但不同线程访问的时候是不同的
我在 Linq 很多函数都看到 __DynamicallyInvokable
这个特性,这是一个没有官方文档的特性,也许是用来优化反射
在看到 dotnet 框架里面有很多方法里面用了 ToXx 和 AsXx 好像都是从某个类转换为另一个类,那么这两个方法命名有什么不同
在 C# 的语法,可以提供自动将某个类的数组自动转这个类的基类数组的方法,但是这样的转换在 Resharper 会提示 Co-variant array conversion 这是什么问题?
在使用 dotnet 读写 Sqlite 可以通过 EF Core 的方法,但是在 EF Core 创建的数据库可能和读写的数据库不是相同的文件
如果使用 Newtonsoft Json 解析字符串,字符串里面有不清真的格式,那么默认的解析将会炸掉。如果想要自己解决字符串中的不清真格式,可以使用传入 JsonSerializerSettings 的方法
默认删除文件的时候 File.Delete 是将文件永久删除,如果是一些文档,建议删除到回收站,这样用户可以自己还原 通过 SHFileOperation 可以将文件放在回收站
在 dotnet 程序有很好用的 Mock 框架,可以用来模拟各种接口和抽象类,可以用来测试某个注入接口的被调用次数和被调用时传入参数。本文告诉大家如何在 Mock 里面模拟一个 Func 同时模拟返回对象,获取调用次数
用2分钟提升十分之一的启动性能,通过在桌面程序启动 JIT 多核心编译提升启动性能 在 dotnet 可以通过让 JIT 进行多核心编译提升软件的启动性能,在默认托管的 ASP.NET 程序是开启的,对 WPF 等桌面程序需要手动开启
在 dotnet 可以使用 LZ4 这个无损的压缩算法,这个压缩算法的压缩率不高但是速度很快。这个库支持在 .NET Standard 1.6 .NET Core .NET Framework Mono Xamarin 和 UWP 运行
很多时候写出来的网站只能自己内网访问,本文告诉大家如何通过 Frp 将自己的 asp dotnet core 网站发布到外网,让小伙伴访问自己的网站 通过 frp 的方式,可以解决自己的服务器性能太差的问题,通过 frp 的方式需要先存在一个外网的服务器
在客户端开发时可以通过轮询的方式拿到服务器端的数据,同时在客户端开发时,如果是将客户端也作为服务器端,那么之间的通讯将会十分简单。有个逗比的小伙伴想要用我的客户端魔改,但是他又不想学习什么知识,此时他需要拿到我客户端的实时信息,好在他知道一点 html 的知识,于是让我通过服务器发送事件 Server-Sent Events 而他写一个简陋的 html 去拿到我客户端的数据 这是一个简陋的开发端的工具,开源的好处就是,你觉得不爽,自己改哇。自己改不动就等开发者下班协助啦,本文就使用一个简单的方式在 asp dotnet core 实现服务器发送事件。虽然标题是 asp dotnet core 而实际上我的软件是一个桌面端软件
现在微软有一个开源项目 CoreRT 能通过将托管的 .NET Core 编译为单个无依赖的 Native 程序 这个项目现在还没发布,但是能尝试使用,可以带来很多的性能提升
在 csdn 的访问统计里面,只能用 csdn 提供的访问统计,因为在 csdn 中不支持在博客加上 js 代码,也就是无法使用友盟等工具统计。 通过在 asp dotnet core 创建一个图片链接的方式,将这个链接作为图片放在 csdn 的博客,可以在链接被访问的时候统计用户访问
微软很久就在做 Blazor 但是我现在才开始创建一个测试项目,我想用 C# 去控制 HTML 界面。小伙伴也许会问现在前端不是烂大街么,为什么还需要 Blazor 来做。可能原因只有一个,就是可以使用 C# 写脚本,代码比较清真
在做P2P的时候,如何让设备发现是整个开发里面最重要的部分。可以采用的方式有组播、扫描局域网、追踪服务器发现等方法。其中效率最高,发现效果最好的也就是使用中央服务器了。本文告诉大家如何使用 ASP.NET Core 写一个简单的 P2P 追踪服务器
和很多序列化库一样,可以通过 MessagePack 序列化和反序列化,和 json 相比这个库提供了二进制的序列化,序列化之后的内容长度比 json 小很多
在运行到一些诡异的代码,这时的程序已经无法继续运行,需要退出,那么如何在记完日志之后在退出程序记录更多信息?可以通过 Environment.FailFast 里面添加字符串告诉用户当前的进程无法继续运行
本文来告诉大家 dotnet core 里面使用的黑科技,如何提高String.IndexOf(char)
的性能
在找了很久的团队交流工具,发现了 Mattermost 最好用,但是还需要做一些定制化的功能,于是就找到了 Mattermost 插件开发,还找到了如何自己写服务集成到 Mattermost 里面 本文告诉大家,通过写一个 ASP .NET Core 程序集成到 Mattermost 里面,在里面添加很多有趣的功能
本文告诉大家如何在 dotnet core 通过 Host PowerShell 的方法使用 PowerShell 脚本
在 dotnet framework 4.8 的时候支持调用 GC.GetAllocatedBytesForCurrentThread 获取当前线程分配过的内存大小
本文告诉大家一个令人震惊的消息,Windows Community Toolkit 有一个大更新,现在的版本是 3.0 。最大的提升就是 WinForm 和 WPF 程序可以使用部分 UWP 控件。 而且可以在 WPF 和 WinForms 使用微软最新 Edge 浏览器
How can I output my target message when I using dotnet build in command line.
在开发完成一个好用的轮子就想将这个轮子发布到 nuget 让其他小伙伴可以来使用,但是 nuget.org 的登陆速度太慢,本文介绍一个命令行发布的方法,通过命令行发布的方法可以配合 Jenkins 自动打包
有人问,复制一个类所有属性到另一个类有多少方法?这也就是问深克隆有多少个方法,容易想的有三个。直接复制,反射复制,序列化复制。但是性能比较快的有表达式树复制 IL复制两个,本文主要讲最后一个
很多逻辑都会使用内存做缓存,这样可以提高运行效率。但是有一些逻辑很少会执行,但是如果有执行就是频繁调用。如我写了文本编辑器,在我打开文件的逻辑,将会不断调用正则判断逻辑,而平时编辑很少会调用。如果将这部分的正则逻辑缓存了,那么可以提升打开文件速度,但是在打开文件之后这部分就成为内存垃圾了。本文给大家一个弱引用缓存,也就是在频繁使用时从内存获取,在不使用时会被回收,这样可以提升性能也能减少内存使用
每次发布 dotnet core 应用都会觉得发布文件太大,而如果使用极限压缩,用 CoreRT 能让发布的控制台文件到 5KB 左右,不需要带框架就能在其他设备运行
在 C++ 需要使用 GetSystemFirmwareTable 的方法来获得 PC 的序列号,需要写的代码很多,但是在 C# 可以使用 WMI 来拿到序列号
编写 .NET 程序的时候,我们经常会在项目的输出目录下发现一大堆的文件。除了我们项目自己生成的程序集之外,还能找到这个项目所依赖的一大堆依赖程序集。有没有什么方法可以把这些依赖和我们的程序集合并到一起呢?
本文介绍四种将程序集和依赖打包合并到一起的方法,每一种方法都有其不同的原理和优缺点。我将介绍这些方法的原理并帮助你决定哪种方法最适合你想要使用的场景。
目前我已知的将 .NET 程序集与依赖合并到一起的方法有下面四种:
如果你还知道有其他的方法,欢迎评论指出,非常感谢!
上面的第五种方法我也会做一些介绍,要么是因为无法真正完成任务或者适用场景非常有限,要么是其原理我还不理解,因此只进行简单介绍。
.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 中对于发布程序集的三种处理方式可以放在一起使用:
关于 .NET Core 3.0 中发布仅一个 exe 的方法、原理和实践,可以参见林德熙的博客:
.NET Core 在 GitHub 上开源:
在你的项目中安装一个 NuGet 包 Costura.Fody。一般来说,安装完之后,你编译的时候就会生成仅有一个 exe 的程序集了。
如果你继续留意,可以发现项目中多了一个 Fody 的专属配置文件 FodyWeavers.xml
,内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<Costura/>
</Weavers>
仅仅到此为止你已经足够利用 Fody 完成程序集的合并了。
但是,如果希望对 Fody 进行更精细化的配置,可以阅读叶洪的博客:
Fody 在 GitHub 上开源:
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 基于 Mono.Ceil 来进行 IL 合并,其使用方法可以参见我的博客:
你可以在以下网址中找到 ILMerge-GUI 的下载链接:
ILMerge-GUI 工具在 Bitbucket 上开源:
可以将 .NET Core 编译为单个无依赖的 Native 程序。
你需要先安装一个预览版的 NuGet 包 Microsoft.DotNet.ILCompiler
关于 Microsoft.DotNet.ILCompiler 的使用,你可以阅读林德熙的博客:
dnSpy 支持添加一个模块到程序集,也可以创建模块,还可以将程序集转换为模块。因此,一个程序集可以包含多个模块的功能就可以被充分利用起来。
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 更好的编写和调试体验。
表格之外还有一些特别需要说明的:
最后说一下,以上所说的所有方法全部是开源的,有问题欢迎在社区讨论一起解决:
MSBuild 中写在 <ItemGroup />
中的每一项是一个 Item
,Item
除了可以使用 Include
/Update
/Remove
来增删之外,还可以定义其他的元数据(Metadata)。
使用 %
可以引用 Item
的元数据,本文将介绍如何正确使用 %
来引用每一个项中的元数据。
就像下面这样,当引用一个 NuGet 包时,可以额外使用 Version
来指定应该使用哪个特定版本的 NuGet 包。这里的 Version
和 PrivateAssets
就是 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=""$(_WalterlvToolFile)" PackContent --content-file " $(_WalterlvContentArgsFilePath) "" />
</Target>
这段代码的含义是:
Content
项和它的元数据;Content
项,然后把所有项中的 PublishState
和 CopyToOutputDirectory
一起拼接成这个样子:
Content|PublishState|CopyToOutputDirectory
关于使用 exe 进行自定义编译的部分可以参考我的另一篇博客:
关于写文件的部分可以参考我的另一篇博客:
一些已知的元数据:
参考资料
本文整理 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
指定的路径上。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>
我的电脑上每天会跑一大堆控制台程序,于是管理这些程序的运行就成了一个问题。或者说你可能也在考虑启动一个控制台程序来完成某些特定的任务。
如果我们需要结束掉这个控制台程序怎么做呢?直接杀进程吗?这样很容易出问题。我正在使用的一个控制台程序会写文件,如果直接杀进程可能导致数据没能写入到文件。所以本文介绍如何使用 .NET/C# 代码向控制台程序发送 Ctrl+C
来安全地结束掉程序。
如果直接用 Process.Kill
杀掉进程,进程可能来不及保存数据。所以无论是窗口程序还是控制台程序,最好都让控制台程序自己去关闭。
▲ 使用 Process.Kill
结束程序,程序退出代码是 -1
▲ 使用 Ctrl+C
结束程序,程序退出代码是 0
Windows API 提供了方法可以将当前进程与目标控制台进程关联起来,这样我们便可以向自己发送 Ctrl+C
信号来结束掉关联的另一个控制台进程。
关联和取消关联的方法是下面这两个,AttachConsole
和 FreeConsole
:
[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)
{
}
}
参考资料
HTTP GET 请求时携带的参数直接在 URL 中,形式如 ?key1=value&key2=value&key3=value
。如果是 POST 请求时,我们可以使用一些库序列化为 json 格式作为 BODY 发送,那么 GET 请求呢?有可以直接将其序列化为 HTTP GET 请求的 query 字符串的吗?
一个典型的 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
,于是我们需要:
query.GetType().GetProperties()
property.GetValue(query, null)
string.Join("&", properties)
然而真实场景可能比这个稍微复杂一点:
DataContract
的类,按照 DataMember
来序列化所以,我写出了下面的方法:
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 包来获得更多可空引用类型契约的支持,详见:
如果你的屏幕分辨率是 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)
测量的时候,是让手指近乎在屏幕外触摸,不断触摸到能够在屏幕上看到的最小或最大值为止。
发现上面实验中有趣的现象了吗?明明只有 1920×1080 的屏幕分辨率,窗口明明只有 1920×1080 那么大,鼠标下收到正常范围内的输入坐标,而触摸下我们能收到超出我们窗口大小 1 像素的触摸事件!
问题并没有完——
如果说,触摸给了你超出窗口大小的坐标,那么你能如何使用这个坐标呢?虽然程序里收到什么坐标都无所谓(至少不崩),但如果你真拿它来渲染,就会在屏幕之外。
更有趣的是,虽然你能收到这个“在屏幕边缘之外”的坐标,但这个消息并不总会发送到你的程序里。更多的时候,你的程序根本就不会收到这个触摸事件,于是我们也就不能在程序里面更新窗口上显示的坐标到 1920 了,就像鼠标一样。
于是,你可能遇到的问题是:
林德熙小伙伴告诉我说可以特意把窗口的尺寸做大一个像素。我试过了,确实能够让触摸在整个屏幕上生效,但对于双屏用户来说,就能在另外一个屏幕上看到“露馅儿”了的窗口,对于我这种强迫症患者来说,显然是不能接受的。
我的建议是,并不需要对这种情况进行什么特殊的处理。
使用 MSBuild 中的 Copy
这个编译目标可以在 .NET 项目编译期间复制一些文件。不过使用默认的参数复制的时候文件夹结构会丢失,所有的文件会保留在同一级文件夹下。
那么如何在复制文件的时候保持文件夹结构与原文件夹结构一样呢?
下面是一个典型的使用 MSBuild 在编译期间复制文件的一个编译目标。
<Target Name="_WalterlvCopyDemo" AfterTargets="AfterBuild">
<ItemGroup>
<_WalterlvToCopyFile Include="$(OutputPath)**" />
</ItemGroup>
<Copy SourceFiles="@(_WalterlvToCopyFile)" DestinationFolder="bin\Debug\Test" SkipUnchangedFiles="True" />
</Target>
这样复制的文件是不会保留文件夹结构的。
复制之后,所有的文件夹将不存在,所有文件覆盖地到同一层级。
如果希望保留文件夹层级,可以在 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>
你可能需要准确地知道一段字符串是否是域名/网址/URL。虽然可以使用 .
、/
这些来模糊匹配,但会造成误判。
实际上单纯使用正则表达式来精确匹配也是非常复杂的,通过代码来判断会简单很多。不过本文依然从域名的定义出发来尽可能匹配一段字符串是否是域名或者网址,在要求不怎么高的场合,使用本文的正则表达式写的代码会比较简单。
网址实际上是 URL(统一资源定位符),它是由协议、主机名和路径组成。不过我们通常所说的网址中的主机名通常是域名,因此我们在匹配的时候主要考虑域名。
维基百科 中关于域名的描述:
- 域名由一或多个部分组成,这些部分通常连接在一起,并由点分隔。最右边的一个标签是顶级域名,例如zh.wikipedia.org的顶级域名是org。一个域名的层次结构,从右侧到左侧隔一个点依次下降一层。每个标签可以包含1到63个八字节。域名的结尾有时候还有一点,这是保留给根节点的,书写时通常省略,在查询时由软件内部补上。
- 域名里的英文字母不区分大小写。
- 完整域名的所有字符加起来不得超过253个ASCII字符的总长度。因此,当每一级都使用单个字符时,限制为127个级别:127个字符加上126个点的总长度为253。但实际上,某些域名可能具有其他限制;也没有只有一个字符的域名后缀。
后面关于非 ASCII 字符的描述我没有贴出来。这种域名例如“.中国”。
在 中国电信网站备案自助管理系统 中,我们可以找到关于域名的描述:
域名中的标号都由英文字母和数字组成,每一个标号不超过63个字符,也不区分大小写字母。标号中除连字符(-)外不能使用其他的标点符号。级别最低的域名写在最左边,而级别最高的域名写在最右边。由多个标号组成的完整域名总共不超过255个字符。
路径是使用 /
分隔的一段一段字符串。
在确认了完整的网址 URL 的规范之后,使用正则表达式来匹配就会比较精确了。
现在,我们来尝试匹配一下域名
-
a-z
A-Z
0-9
,但是 -
不可作为开头,标签总长度 1-63 个字符,于是
[a-zA-Z0-9][-a-zA-Z0-9]{0,62}
-
,后面的字可以包含 -
(\.[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。
如果你只是打算做一些简单的检查(例如只是语法高亮),那么简单匹配即可;因为当你使用 Chrome 浏览器访问这些 URL 的时候,依然可以正常访问,Chrome 会帮你格式化一下这个 URL。
匹配 URL 跟匹配域名不一样,URL 复杂得多。严格匹配的要求是准确反应出 URL 的标准,但实际上如实反应标准编写的正则表达式会非常复杂,因此相比于 100% 准确匹配,我们还是从简了。
所以如果不是有特别要求,建议还是跳到后面的“宽松”部分来阅读吧!
我们以下面这个网址为例说明。
https://blog.walterlv.com/post/read-32bit-registry-from-x64-process.html
(http(s)?:\/\/)
?
和什么都不加的效果是一样的[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][-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 字符,使用 #
用来指代网址中的某个部分。
因此,我们最终总结应该匹配的特殊字符有 @
:
%
_
\
+
.
~
#
?
&
/
=
。
宽松一点的话,正则表达式就好写多了。
这个正则表达式可以不写 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 不能以 )
结尾;>
同理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 的一部分。
在 GitHub 上还有很多大牛们在写各种匹配 URL 的正则表达式:
最长的一个写了 1347 个字符,最短的有 38 个字符。
有人将其整理成一张表格,一图说明各种正则表达式能匹配到什么程度:
参考资料
C# 8.0 引入了可空引用类型,你可以通过 ?
为字段、属性、方法参数、返回值等添加是否可为 null 的特性。
但是如果你真的在把你原有的旧项目迁移到可空类型的时候,你就会发现情况远比你想象当中复杂,因为你写的代码可能只在部分情况下可空,部分情况下不可空;或者传入空时才可为空,传入非空时则不可为空。
在开始迁移你的项目之前,你可能需要了解如何开启项目的可空类型支持:
可空引用类型是 C# 8.0 带来的新特性。
你可能会好奇,C# 语言的可空特性为什么在编译成类库之后,依然可以被引用它的程序集识别。也许你可以理解为有什么特性 Attribute
标记了字段、属性、方法参数、返回值的可空特性,于是可空特性就被编译到程序集中了。
确实,可空特性是通过 NullableAttribute
和 NullableContextAttribute
这两个特性标记的。
但你是否好奇,即使在古老的 .NET Framework 4.5 或者 .NET Standard 2.0 中开发的时候,你也可以编译出支持可空信息的程序集出来。这些古老的框架中没有这些新出来的类型,为什么也可以携带类型的可空特性呢?
实际上反编译一下编译出来的程序集就能立刻看到结果了。
看下图,在早期版本的 .NET 框架中,可空特性实际上是被编译到程序集里面,作为 internal
的 Attribute
类型了。
所以,放心使用可空类型吧!旧版本的框架也是可以用的。
阻碍你将老项目迁移到可空类型的原因,可能还有你原来代码逻辑的问题。因为有些情况下你无法完完全全将类型迁移到可空。
例如:
null
或者获取可空类型时你能确保此时一定不为 null
(待会儿我会解释到底是什么情况);null
那种情况下返回的是非 null
;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
了。
在本文第一小节里面,我们说 Nullable
是编译到目标程序集中的,所以不需要引用什么特别的程序集就能够使用到可空引用的特性。
那么上面这些特性呢?它们并没有编译到目标程序集中怎么办?
实际上,你只需要有一个命名空间、名字和实现都相同的类型就够了。你可以写一个放到你自己的程序集中,也可以把这些类型写到一个自己公共的库中,然后引用它。当然,你也可以用我已经写好的 NuGet 包 Walterlv.NullableAttributes。
微软 .NET 官方的可空特性在这里:
我将其注释翻译成中文之后,也写了一份在这里:
如果你想简单一点,可以直接引用我的 NuGet 包:
源代码包可以在不用引入其他 dll 依赖的情况下完成引用。最终你输出的程序集是不带对此包的依赖的,详见:
参考资料
基类是 MenuItem
,子类是 WalterlvMenuItem
、FooMenuItem
。基类是 Configuration
,子类是 WalterlvConfiguration
、ExtensionConfiguration
。在代码中,我们可能会为了能够一眼看清类之间的继承(从属)关系而在子类名称后缀中带上基类的名称。但是由于这种情况下的基类不参与实际的业务,所以对外(文件/网络)的名称通常不需要带上这个后缀。
本文提供一个简单的方法,让子类中基类的后缀删掉,只取得前面的那部分。
在这段代码中,我们至少需要获得两个传入的参数,一个是基类的名称,一个是子类的名称。但是考虑到让开发者就这样传入两者名称的话会比较容易出问题,因为开发者可能根本就不会按照要求去获取类型的名称。所以我们需要自己通过类型对象来获取名称。
另外,我们还需要有一些约束,必须有一个类型是另外一个类型的子类。于是我们可能必须来使用泛型做这样的约束。
于是,我们可以写出下面的方法:
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
。
要发布 NuGet 包,只需要执行命令 nuget push xxx.nupkg
即可,或者去 nuget.org 点鼠标上传。
不过,如果你有很多的 NuGet 包并且经常需要推送的话,也可以集成到 Directory Opus 或者 Total Commander 中。
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
要执行 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 中,这样,一次点击或者一个快捷键就能发布某个特定版本的所有的 NuGet 包了。
关于使用 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"
含义为:
nuget push *.xxx.nupkg -Source https://api.nuget.org/v3/index.json
{file}
文件名{file}
是 Directory Opus 当前选中的文件,我用正则表达式匹配出其版本号和后面的 .nupkg
后缀nuget push
的包,最终生成的命令会非常类似于本文一开始提到的命令 nuget push *.0.8.0-alpha.nupkg -Source https://api.nuget.org/v3/index.json
于是,当我选中了一个包,按下这个工具栏按钮之后,就可以推送与这个包相同版本的所有的 NuGet 包了。
毕竟我一次编译产生的 NuGet 包太多了,还是需要使用这样的方式来提高一点效率。至于为什么不用持续集成,是因为目前 SourceYard 还不支持在 GitHub 上集成。
参考资料
WPF 程序提供了 Focus
方法和 TraversalRequest
来在 WPF 焦点范围内转移焦点。但如果 WPF 窗口中嵌入了其他框架的 UI(比如另一个子窗口),那么就需要使用其他的方法来设置焦点了。
一个粗略的设置方法是,使用 Win32 API:
SetFocus(hwnd);
传入的是要设置焦点的窗口的句柄。
参考资料
当我们在写 +=
和 -=
事件的时候,我们会在 +=
或 -=
的右边写上事件处理函数。我们可以写很多种不同的事件处理函数的形式,那么这些形式都是一样的吗?如果你不注意,可能出现内存泄漏问题。
本文将讲解事件处理函数的不同形式,理解了这些可以避免编写代码的时候出现内存相关的问题。
事件处理函数本质上是一个委托,比如 FileSystemWatcher
的 Changed
事件是这样定义的:
// 这是简化的代码。
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)
{
}
因为我们可以通过编写事件的 add
和 remove
方法来观察事件 +=
-=
传入的 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)。
然而,你平时众多的编码经验会告诉你,这里的 -=
是一定可以成功的。也就是说,+=
和 -=
时传入的委托实例即便不是同一个,也是可以成功 +=
和 -=
的。
+=
-=
是怎么做的+=
和 -=
到底是怎么做的,可以在不同实例时也能 +=
和 -=
成功呢?
+=
和 -=
实际上是调用了 Delegate
的 Combine
和 Remove
方法,并生成一个新的委托实例赋值给 +=
-=
的左边。
public event FileSystemEventHandler Changed
{
add
{
onChangedHandler = (FileSystemEventHandler)Delegate.Combine(onChangedHandler, value);
}
remove
{
onChangedHandler = (FileSystemEventHandler)Delegate.Remove(onChangedHandler, value);
}
}
而最终的判断也是通过 Delegate
的 Equals
方法来比较委托的实例是否相等的(==
和 !=
也是调用的 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);
}
于是可以看出来,判断相等就是两个关键对象的判断相等:
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
-=
于是什么样的 -=
才可以把 +=
加进去的事件处理函数减掉呢?
所以:
+=
和 -=
的时候无视哪个委托实例,都是可以减掉的;有小伙伴希望在 .NET 代码中使用指针,操作非托管资源,于是可能使用到 unsafe
fixed
关键字。但使用此关键字的前提是需要在项目中开启不安全代码。
本文介绍如何在项目中开启不安全代码。
第一步:在你需要启用不安全代码的项目上点击右键,然后选择属性:
第二步:在“生成”标签下,勾选上“允许不安全代码”:
第三步:切换到 Release 配置,再勾上一次“允许不安全代码”(确保 Debug 和 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 风格的项目文件的,详情请阅读:
我只是增加库的一个 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
类型也将无法在不确定的命名空间中找到。
因此:
using
),要么写全命名空间(从第一段开始写,不要省略任何部分),否则就容易与其他命名空间冲突;是的,即使是单纯的新增 API 也可能会导致使用库的一方在源码级不兼容。当然二进制还是兼容的。
另外,OpportunityLiu 提醒,如果命名空间是 Walterlv.B.Walterlv.A.Diagnostics.Bar
,一样可以让写全了的命名空间炸掉。呃……还是不要在库里面折腾这样的命名空间好……不然代码当中到处充斥着 global::
可是非常难受的。
我们知道,32 位程序在读取注册表的时候,会自动将注册表的路径映射到 32 位路径下,即在 Wow6432Node
子节点下。但是 64 位程序不会映射到 32 位路径下。那么 64 位程序如何读取到 32 位程序写入的注册表路径呢?
对于 32 位程序,读取注册表路径的时候,会读到 Wow6432Node
节点下的项:
这张图读取的就是前面截图中的节点。
那么怎样编译的程序是 32-bit 的程序呢?
对于 64 位程序,读取的时候就不会有 Wow6432Node
路径部分。由于我没有在那个路径放注册表项,所以会得到 null
。
那么怎样编译的程序是 64-bit 的程序呢?
前面我们的例子代码是这样的:
var value = RegistryHive.LocalMachine.Read(@"SOFTWARE\Walterlv");
可以看到,相同的代码,在 32 位和 64 位进程下得到的结果是不同的:
Wow6432Node
。那么如何在 64 位进程中读取 32 位注册表路径呢?
方法是在打开注册表项的时候,传入 RegistryView.Registry32
。
RegistryKey.OpenBaseKey(root, RegistryView.Registry32);
可以在我的 GitHub 仓库中查看完整的实现。当然,除了上面那句话,其他都不是关键代码,在哪里都可以找得到的。
参考资料
因为 Win32 的窗口句柄是可以跨进程传递的,所以可以用来实现跨进程 UI。不过,本文不会谈论跨进程 UI 的具体实现,只会提及其实现中的一个重要缓解,使用子窗口的方式。
你有可能在使用子窗口之后,发现拖拽改变窗口大小的时候,子窗口中的内容不断闪烁。如果你也遇到了这样的问题,那么正好可以阅读本文来解决。
你可以看一下下面的这张动图,感受一下窗口的闪烁:
实际上在拖动窗口的时候,是一直都在闪的,只是每次闪烁都非常快,截取 gif 的时候截不到。
如果你希望实际跑一跑项目看看,可以使用下面的代码:
我特地提取了一个提交下的代码,如果你要尝试,不能使用 master
分支,因为 master
分支修复了闪烁的问题。
后来使用 CreateWindowEx
创建了一个纯 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)
{
}
}
正在探索……
参考资料
在 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
,此方法要求窗口句柄依然有效,然而此时窗口已经关闭,句柄已经销毁。
如果你得到了一个来自于其他进程或者其他模块的 Direct3D11 的共享资源,即 SharedHandle 句柄,那么可以使用本文提到的方法将其转换成 Direct3D11 的设备和纹理,这样你可以进行后续的其他处理。
本文的代码会使用到 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" />
Direct3D 支持在不同的 Direct3D 设备之间共享资源。需要设置 ResourceOptionFlags
为 Shared
的纹理才可以支持共享,当然这不是本文要说的重点。
本文要说的是,如果你拿到了一个来自于其他模块的共享资源句柄的时候,你可以如何使用它。
你的使用可能类似于这样:
public void OnAcceleratedPaint(IntPtr sharedHandle, Int32Rect dirtyRect)
{
// 通过 sharedHandle 进行后续的处理。
}
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);
在得到此共享资源之后,我们可以获得更多关于此资源的描述,以及有限地使用此资源的方法。
可以通过 QueryInterface
获取某个资源相关的 COM 对象的引用。我们拿到的共享资源是 2D 纹理的话,我们可以使用 QueryInterface
获取 SharpDX.Direct3D11.Texture2D
COM 对象的引用。
var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();
可以从 Texture2D
的实例中获取到 Texture2DDescription
,这是用来描述此 2D 纹理创建时的各种信息。
// 在 DirectX 的传统代码中,通常使用 desc 来作为 Texture2DDescription 实例命名的后缀。
// 不过 C# 代码通常不这么干,这是 C++ 代码的习惯。在这里这么写是为了在得到 C++ 搜索结果的时候可以与本文所述的 C# 代码对应起来。
var desc = texture.Description;
或者,我们可以获取到 2D 图面,用于做渲染、绘制等操作。当然,是否能真正进行这些操作取决于 Texture2DDescription
中是否允许相关的操作。
var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>();
在获取到 SharpDX.DXGI.Surface
的 COM 组件引用之后,可以在内存中映射位图用于调试,可以参见:
参考资料
Direct3D11 的使用通常不是应用程序唯一的部分,于是使用 Direct3D11 的代码如何与其他模块正确地组合在一起就是一个需要解决的问题。
本文介绍将 Direct3D11 在 GPU 中绘制的纹理映射到内存中,这样我们可以直接观察到此纹理是否是正确的,而不用担心是否有其他模块影响了最终的渲染过程。
本文的代码会使用到 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 的渲染纹理,不过如果你希望了解,可以:
本文接下来的内容,是在你已经获得了 SharpDX.Direct3D11.Resource
的引用,或者 SharpDX.Direct3D11.Texture2D
的前提之下。当然,如果你获得了其中任何一个实例,可以通过 COM 组件的 QueryInterface
方法获得其他实例。
var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();
var resource = texture.QueryInterface<SharpDX.Direct3D11.Resource>();
要获得 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 访问权在创建纹理的时候就已经确定下来了。
如果前面你得到的纹理是自己创建的,那么恭喜你,你只需要改一下创建纹理的参数就好了。给 Texture2DDescription
的 CpuAccessFlags
属性加上 CpuAccessFlags.Read
标识。
desc.CpuAccessFlags = CpuAccessFlags.Read;
但是,如果此纹理不是由你自己创建的,那么就需要拷贝一份新的纹理了。当然,拷贝过程发生在 GPU 中,占用的也是 GPU 专用内存(即显存,如果有的话)。
拷贝需要做到两点:
Texture2DDescription
(一定要是新的实例,你不能影响原来的实例),然后修改其 CPU 访问权限为 Read
;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),可以参考:
记得打开不安全代码开关哦!详见:
参考资料
我们知道 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\
本文的代码主要参考自 jeff.brown 在 Manipulating NTFS Junction Points in .NET - CodeProject 一文中所附带的源代码。
由于随时可能更新,所以你可以前往 GitHub 仓库打开此代码:
如果希望在代码中创建目录联接,则直接使用:
JunctionPoint.Create("walterlv.demo", @"D:\Developments", true);
后面的 true
指定如果目录联接存在,则会覆盖掉原来的目录联接。
参考资料
WPF 渲染框架并没有对外提供多少可以完全控制渲染的部分,目前可以做的有:
本文将解释如何最大程度压榨 WriteableBitmap
在 WPF 下的性能。
创建一个新的 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 品质的位图,并通过以下步骤来评估:
CompositionTarget.Rendering
逐帧渲染以评估其渲染帧率于是,在 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
的位图缓存中。
虽然我们看不到任何可变的修改,不过 WriteableBitmap 可不这么认为。因为我们调用了 AddDirtyRect
将整个位图空间都加入到了脏区中,这样 WPF 会重新渲染整幅位图。
Visual Studio 中看到的 CPU 占用率大约维持在 16% 左右(跟具体机器相关);并且除了一开始启动的时候之外,完全没有 GC(这点很重要),内存稳定在一个值上不再变化。
也只有本文一开始提及的三种方法才可能做到渲染任何可能的图形的时候没有 GC
查看界面渲染帧率可以发现跑满 60 帧没有什么问题(跟具体机器相关)。
现在,我们把脏区的区域缩小为 100*100,同样看性能数据。
-- _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++ _bitmap.AddDirtyRect(new Int32Rect(0, 0, 100, 100));
可以发现 CPU 占用降低到一半(确实是大幅降低,但是跟像素数量并不成比例);内存没有变化(废话,4K 图像是确定的);帧率没有变化(废话,只要性能够,帧率就是满的)。
现在,我们将脏区清零。
-- _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++ _bitmap.AddDirtyRect(new Int32Rect(0, 0, 0, 0));
在完全没有脏区的时候,CPU 占用直接降为 0,这个性能提升还是非常恐怖的。
如果我们不把 WriteableBitmap 设置为 Image
的 Source
属性,那么无论脏区多大,CPU 占用都是 0。
从前面的测试中我们可以发现,脏区的大小在 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 占用。此占用基本上全是渲染线程的事。
如果我们分析主线程的性能分布,可以发现内存拷贝现在是性能瓶颈:
后面我们会提到 WriteableBitmap 的渲染原理,也会说到这一点。
不过,由于内存数据的拷贝和脏区渲染实际上可以分开到两个不同的线程,如果这两者不同步执行(可能执行次数还有差异)的情况下,内存拷贝也可能成为性能瓶颈的一部分。
于是我将不同的内存拷贝方法进行一个基准测试,便于大家评估使用哪种方法来为 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();
}
++ [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 |
可以发现:
CopyMemory
和 RtMoveMemory
性能是最好的,其性能差不多;综合前面两者的结论,我们可以发现:
另外,如果你有一些特殊的应用场景,可以适当调整下自己写代码的策略:
在调用 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 占用依然是这么高)。
在 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
制作透明窗口的时候设置了 ResizeMode="None"
,所以你拖动窗口在屏幕顶部和左右两边的时候,Windows 不会再帮助你最大化窗口或者靠边停靠窗口,于是你需要自行处理。不过窗口的标题栏拖动功能依然保留了下来,标题栏上的右键菜单也是可以继续使用的。
方案 | WindowChrome | AllowsTransparency |
---|---|---|
拖拽标题栏移动窗口 | 保留 | 自行实现 |
最小化最大化关闭按钮 | 丢失 | 丢失 |
拖拽边缘调整窗口大小 | 丢失 | 丢失 |
移动窗口到顶部可最大化 | 丢失 | 自行实现 |
拖拽最大化窗口标题栏还原窗口 | 保留 | 自行实现 |
移动窗口到屏幕两边可侧边停靠 | 丢失 | 自行实现 |
拖拽摇动窗口以最小化其他窗口 | 保留 | 自行实现 |
窗口打开/关闭/最小化/最大化/还原动画 | 丢失 | 丢失 |
表格中:
保留
表示此功能无需任何处理即可继续支持自行实现
表示此功能已消失,但仅需要一两行代码即可补回功能丢失
表示此功能已消失,如需实现需要编写大量代码另外,以上表格仅针对鼠标操作窗口。如果算上使用触摸来操作窗口,那么所有标记为 自行实现
的都将变为 丢失
。因为虽然你可以一句话补回功能,但在触摸操作下各种 Bug,你解不完……
这两种实现的窗口之间还有一些功能上的区别:
方案 | WindowChrome | AllowsTransparency | 说明 |
---|---|---|---|
点击穿透 | 在完全透明的部分点击依然点在自己的窗口上 | 在完全透明的部分点击会穿透到下面的其他窗口 | 感谢 nocanstillbb (huang bin bin) 提供的信息 |
随着 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 中的分析器严重性内容就像下面这样:
[*.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。
做 Windows 桌面应用开发的小伙伴们对“模态窗口”(Modal Dialog)一定不陌生。如果你希望在模态窗口之上做更多的事情,或者自己实现一套模态窗口类似的机制,那么你可能需要了解模态窗口的本质。
本文不会太深,只是从模态窗口一词出发,抵达大家都熟知的一些知识为止。
在各种系统、语言和框架中,只要有用户可以看见的界面,都存在模态窗口的概念。从交互层面来说,它的形式是在保留主界面作为环境来显示的情况下,打开一个新的界面,用户只能在新的界面上操作,完成或取消后才能返回主界面。从作用上来说,通常是要求用户必须提供某些信息后才能继续操作,或者单纯只是为了广告。
如果你希望自己搞一套模态窗口出来,那么只需要满足这三点即可。你可以随便加戏但那都无关紧要。
拿 Windows 系统中的模态对话框为例子,大概就像下面这两张图片这样:
有一个小的子界面盖住了主界面,要求用户必须进行选择。Windows 系统设置因为让背景变暗了,所以用户肯定会看得到需要进行的交互;而任务管理器没有让主界面变暗,所以用户在操作子界面的时候,模态窗口的边框和标题栏闪烁以提醒用户注意。
对于 Windows 操作系统来说,模态窗口并不是一个单一的概念,你并不能仅通过一个 API 调用就完成显示模态窗口,你需要在不同的时机调用不同的 API 来完成一个模态窗口。如果要完整实现一个自己的模态窗口,则需要编写实现以上三个特点的代码。
当然,你可能会发现实际上你显示一个模态窗口仅仅一句话调用就够了,那是因为你所用的应用程序框架帮你完成了模态窗口的一系列机制。
关于 WPF 框架是如何实现模态窗口的,可以阅读:直击本质:WPF 框架是如何实现模态窗口的
关于如何自己实现一个跨越线程/进程边界的模态窗口,可以阅读:实现 Windows 系统上跨进程/跨线程的模态窗口
如果你希望定制以上第三个特点中强提醒的动画效果,可以阅读:WPF window 子窗口反馈效果(抖动/阴影渐变) - 唐宋元明清2188 - 博客园。
为了在 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 上的提醒是最简单的了,比如:
然而 Windows 和 Mac OS 这些古老的系统由于兼容性负担不能随便那么改,于是需要有其他的提醒方式。
Windows 采用的方式是让标题栏闪烁,让阴影闪烁。
而这些特效的处理,来自于子窗口需要处理一些特定的消息 WM_SETCURSOR
。
详见:WPF window 子窗口反馈效果(抖动/阴影渐变) - 唐宋元明清2188 - 博客园
通常你不需要手工处理这些消息,但是如果你完全定制了窗口样式,则可能需要自行做一个这样的模态窗口提醒效果。
想知道你在 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);
}
}
}
觉得代码还是太长?不要紧,我再简化一下:
EnumThreadWindows
获取当前线程的所有窗口可以注意到禁用掉的窗口是“当前线程”的哦。
ShowHelper
接下来的重点方法是 Window.ShowDialog
中的那句 Show()
。在 Show()
之前设置了 _showingAsDialog
为 true
,于是这里会调用 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();
}
}
可以看到,重点是 PushModal
、PopModal
以及 PushFrame
。
PushFrame
的效果就是让调用 ShowDialog
的代码看起来就像阻塞了一样(实际上就是阻塞了,只不过开了新的消息循环看起来 UI 不卡)。
关于 PushFrame
为什么能够“阻塞”你的代码的同时还能继续响应 UI 操作的原理,可以阅读:
那么 ComponentDispatcher.PushModal
和 ComponentDispatcher.PopModal
呢?可以在这里(ComponentDispatcherThread.cs)看它的代码,实际上是为了模态计数以及引发事件的,对模态的效果没有本质上的影响。
如果你希望知道某台计算机上安装了哪些版本的 .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" },
};
另外,还有一些值也是有意义的(只是不那么精确):
它们分别在注册表的这些位置:
计算机\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
如下:
字典里 Key 是不能共存的主版本,Value 是这个主版本里当前已经安装的具体版本信息。
如果直接使用 ToString()
,是可以生成我们平时经常在各大文档或者社区使用的 .NET Framework 的名称。
第二种,获取当前已安装的最新的 .NET Framework 版本名称:
var currentVersion = NdpInfo.GetCurrentVersionName();
这可以直接获取到一个字符串,比如 .NET Framework 4.8
。对于只是简单获取一下已安装名称而不用做更多处理的程序来说会比较方便。
在 Windows 应用开发中,如果需要操作其他的窗口,那么可以使用 EnumWindows
这个 API 来枚举这些窗口。
你可以使用本文编写的一个类型,查找到所有窗口中你关心的信息。
枚举所有窗口仅需要使用到 EnumWindows
,其中需要定义一个委托 WndEnumProc
作为传入参数的类型。
剩下的我们需要其他各种方法用于获取窗口的其他属性。
GetParent
获取窗口的父窗口,这可以确认找到的窗口是否是顶层窗口。(关于顶层窗口,可以延伸 使用 SetParent 跨进程设置父子窗口时的一些问题(小心卡死) - walterlv。)IsWindowVisible
判断窗口是否可见GetWindowText
获取窗口标题GetClassName
获取窗口类名GetWindowRect
获取窗口位置和尺寸,为此我们还需要定义一个结构体 LPRECT
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;
}
}
从 Windows 10 (1803) 开始,Win32 应用也可以有 API 来实现原生的亚克力效果了。不过相比于 UWP 来说,可定制性会差很多。
本文介绍如何在 WPF 程序中应用 Windows 10 真•亚克力效果。(而不是一些流行的项目里面自己绘制的亚克力效果。)
需要使用的 API 是微软的文档中并未公开的 SetWindowCompositionAttribute
。
我在另一篇博客中有介绍此 API 各种用法的效果,详见:
当然,使用此 API 也可以做 Windows 10 早期的模糊效果,比如:
为了方便地让你的窗口获得亚克力效果,我做了两层不同的 API:
AcrylicBrush
当然,受到 Win32 启用亚克力效果的限制,只能在窗口上设置此属性WindowAccentCompositor
用于更多地控制窗口与系统的叠加组合效果代码请参见:
要使得亚克力效果可以生效,需要:
GradientColor
参考资料
Windows 系统中有一个没什么文档的 API,SetWindowCompositionAttribute
,可以允许应用的开发者将自己窗口中的内容渲染与窗口进行组合。这可以实现很多系统中预设的窗口特效,比如 Windows 7 的毛玻璃特效,Windows 8/10 的前景色特效,Windows 10 的模糊特效,以及 Windows 10 1709 的亚克力(Acrylic)特效。而且这些组合都发生在 dwm 进程中,不会额外占用应用程序的渲染性能。
本文介绍 SetWindowCompositionAttribute
可以实现的所有效果。你可以通过阅读本文了解到与系统窗口可以组合渲染到哪些程度。
本文将创建一个简单的 WPF 程序来验证 SetWindowCompositionAttribute
能达到的各种效果。你也可以不使用 WPF,得到类似的效果。
简单的项目文件结构是这样的:
其中,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
让客户区覆盖非客户区使用 WindowChrome
,你可以用你自己的 UI 覆盖掉系统的 UI 窗口样式。关于 WindowChrome
让客户区覆盖非客户区的知识,可以阅读:
需要注意的是,WindowChrome
的 GlassFrameThickness
属性可以设置窗口边框的粗细,设置为 0
将导致窗口没有阴影,设置为负数将使得整个窗口都是边框。
我们依次来看看效果。
使用 ACCENT_DISABLED
时,GradientColor
叠加色没有任何影响,唯一影响渲染的是 WindowChrome
和操作系统。
不使用 WindowChrome
,在 Windows 10 上:
不使用 WindowChrome
在 Windows 7 上:
在 Windows 10 上,使用 WindowChrome
:
<WindowChrome.WindowChrome>
<WindowChrome />
</WindowChrome.WindowChrome>
在 Windows 7 上,使用 WindowChrome
:
当然,以上边框比较细,跟系统不搭,可以设置成其他值:
在 Windows 10 上,使用 WindowChrome
并且 GlassFrameThickness
设置为 -1
:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>
而在 Windows 7 上,这就是非常绚丽的全窗口的 Aero 毛玻璃特效:
使用 ACCENT_DISABLED
时,GradientColor
叠加色会影响到最终的渲染效果。
还记得我们前面叠加的颜色是什么吗?
接下来别忘了然后把它误以为是我系统的主题色哦!
不使用 WindowChrome
,在 Windows 10 上:
另外,你会注意到左、下、右三个方向上边框会深一些。那是 Windows 10 的窗口阴影效果,因为实际上 Windows 10 叠加的阴影也是窗口区域的一部分,只是一般人看不出来而已。我们叠加了颜色之后,这里就露馅儿了。
另外,这个颜色并不是我们自己的进程绘制的哦,是 dwm 绘制的颜色。
如果不指定 GradientColor
也就是保持为 0
,你将看到上面绿色的部分全是黑色的;嗯,包括阴影的部分……
不使用 WindowChrome
在 Windows 7 上:
可以看出,在 Windows 7 上,GradientColor
被无视了。
而使用 WindowChrome
在 Windows 10 上,则可以得到整个窗口的叠加色:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="16 48 16 16" />
</WindowChrome.WindowChrome>
可以注意到,窗口获得焦点的时候,整个窗口都是叠加色;而窗口失去焦点的时候,指定了边框的部分颜色会更深(换其他颜色叠加可以看出来是叠加了半透明黑色)。
如果你希望失去焦点的时候,边框部分不要变深,请将边框设置为 -1
:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>
使用 WindowChrome
在 Windows 7 上,依然没有任何叠加色的效果:
使用 ACCENT_ENABLE_TRANSPARENTGRADIENT
时,GradientColor
叠加色没有任何影响,唯一影响渲染的是 WindowChrome
和操作系统。
不使用 WindowChrome
,在 Windows 10 上:
依然左、下、右三个方向上边框会深一些,那是 Windows 10 的窗口阴影效果。
不使用 WindowChrome
在 Windows 7 上:
GradientColor
也是被无视的,而且效果跟之前一样。
使用 WindowChrome
在 Windows 10 上,在获得焦点的时候整个背景是系统主题色;而失去焦点的时候是灰色,但边框部分是深色。
依然可以将边框设置为 -1
使得边框不会变深:
使用 WindowChrome
在 Windows 7 上,依然是老样子:
ACCENT_ENABLE_BLURBEHIND
可以在 Windows 10 上做出模糊效果,就跟 Windows 10 早期版本的模糊效果是一样的。你可以看我之前的一篇博客,那时亚克力效果还没出来:
使用 ACCENT_ENABLE_BLURBEHIND
时,GradientColor
叠加色没有任何影响,唯一影响渲染的是 WindowChrome
和操作系统。
在 Windows 10 上,没有使用 WindowChrome
:
你可能需要留意一下那个“诡异”的模糊范围,你会发现窗口的阴影外侧也是有模糊的!!!你能忍吗?肯定不能忍,所以还是乖乖使用 WindowChrome
吧!
在 Windows 7 上,没有使用 WindowChrome
,效果跟其他值一样,依然没有变化:
在 Windows 10 上,使用 WindowChrome
:
使用 WindowChrome
在 Windows 7 上,依然是老样子:
从 Windows 10 (1803) 开始,Win32 程序也能添加亚克力效果了,因为 SetWindowCompositionAttribute
的参数枚举新增了 ACCENT_ENABLE_ACRYLICBLURBEHIND
。
亚克力效果相信大家不陌生,那么在 Win32 应用程序里面使用的效果是什么呢?
不使用 WindowChrome
,在 Windows 10 上:
咦!等等!这不是跟之前一样吗?
嗯,下面就是不同了,亚克力效果支持与半透明的 GradientColor
叠加,所以我们需要将传入的颜色修改为半透明:
var compositor = new WindowAccentCompositor(this);
-- compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++ compositor.Composite(Color.FromArgb(0x3f, 0x18, 0xa0, 0x5e));
那么如果改为全透明会怎么样呢?
不幸的是,完全没有效果!!!
var compositor = new WindowAccentCompositor(this);
-- compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++ compositor.Composite(Color.FromArgb(0x00, 0x18, 0xa0, 0x5e));
接下来是使用 WindowChrome
时:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="16 48 16 16" />
</WindowChrome.WindowChrome>
然而周围有一圈偏白色的渐变是什么呢?那个其实是 WindowChrome
设置的边框白,被亚克力效果模糊后得到的混合效果。
所以,如果要获得全窗口的亚克力效果,请将边框设置成比较小的值:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="0 1 0 0" />
</WindowChrome.WindowChrome>
记得不要像前面的那些效果一样,如果设置成 -1
,你将获得纯白色与设置的 Gradient
叠加色的亚克力特效,是个纯色:
你可以将叠加色的透明度设置得小一些,这样可以看出叠加的颜色:
var compositor = new WindowAccentCompositor(this);
-- compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++ compositor.Composite(Color.FromArgb(0xa0, 0x18, 0xa0, 0x5e));
那么可以设置为全透明吗?
var compositor = new WindowAccentCompositor(this);
-- compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++ compositor.Composite(Color.FromArgb(0x00, 0x18, 0xa0, 0x5e));
很不幸,最终你会完全看不到亚克力效果,而变成了毫无特效的透明窗口:
最上面那根白线,是我面前面设置边框为 0 1 0 0
导致的。
如果在这种情况下,将边框设置为 0
会怎样呢?记得前面我们说过的吗,会导致阴影消失哦!
呃……你将看到……这个……
什么都没有……
是不是找到了一条新的背景透明异形窗口的方法?
还是省点心吧,亚克力效果在 Win32 应用上的性能还是比较堪忧的……
想要背景透明,请参见:
不用考虑 Windows 7,因为大家都知道不支持。实际效果会跟前面的一模一样。
这个值其实不用说了,因为 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,
// 省略其他未使用的字段
}
}
}
启用转为编程设计的连字字体,可以给你的变成带来不一样的体验。
微软随 Windows Terminal 设计了一款新的字体 Cascadia Code,而这是一款连字字体。
你可以看到,在 Windows Terminal 的终端中,=>
==
!=
符号显示成了更容易理解的连字符号:
在 Cascadia Code 发布之前,Fira Code 是一款特别火的连字字体,下面是 Fira Code 连字字体在 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。
下载解压后,你会看到五个不同的文件夹,这是四种不同的字体类型:
对于 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 中启用连字字体需要用到两个选项:
"editor.fontFamily": "Fira Code Light, Consolas, Microsoft YaHei",
"editor.fontLigatures": true,
然后点击新打开的标签右上角的 {}
图标以打开 json 形式编辑的设置:
然后修改把上面两个设置增加或替换进去即可。下面是我的设置的部分截图:
只需要将字体设置成 Fira Code 即可。
参考资料
.NET Core 3 相比于 .NET Core 2 是一个大更新。也正因为如此,即便它长时间处于预览版尚未发布的状态,大家也一直在使用。
Visual Studio 2019 中提供了使用 .NET Core SDK 预览版的开关。但几个更新的版本其开关的位置不同,本文将介绍在各个版本中的位置,方便你找到然后设置。
.NET Core 3.0 已经发布,下载地址:
Visual Studio 16.3 与 .NET Core 3.0 正式版同步发布,因此不再需要 .NET Core 3.0 的预览版设置界面。你只需要安装正式版 .NET Core SDK 即可。
从 Visual Studio 2019 的 16.2 版本,.NET Core 预览版的设置项的位置在:
工具
-> 选项
环境
-> 预览功能
-> Use previews of the .NET Core SDK (需要 restart)
如果你是英文版的 Visual Studio,也可以参考英文版:
Tools
-> Options
Environment
-> Preview Features
-> Use previews of the .NET Core SDK (requires restart)
从 Visual Studio 2019 的 16.1 版本,.NET Core 预览版的设置项的位置在:
工具
-> 选项
环境
-> 预览功能
-> 使用 .NET Core SDK 的预览
如果你是英文版的 Visual Studio,也可以参考英文版:
Tools
-> Options
Environment
-> Preview Features
-> Use previews of the .NET Core SDK
在 Visual Studio 2019 的早期,.NET Core 在设置中是有一个专用的选项的,在这里:
工具
-> 选项
项目和解决方案
-> .NET Core
-> 使用 .NET Core SDK 预览版
如果你是英文版的 Visual Studio,也可以参考英文版:
Tools
-> Options
Projects and solutions
-> .NET Core
-> Use previews of the .NET Core SDK
Visual Studio 2019 中此对于 .NET Core SDK 的预览版的设置是全局生效的。
也就是说,你在 Visual Studio 2019 中进行了此设置,在命令行中使用 MSBuild
或者 dotnet build
命令进行编译也会使用这样的设置项。
那么这个全局的设置项在哪个地方呢?是如何全局生效的呢?可以阅读我的其他博客:
WPF 中可以使用 UIElement.Focus()
将焦点设置到某个特定的控件,也可以使用 TraversalRequest
仅仅移动焦点。本文介绍如何在 WPF 程序中控制控件的焦点。
UIElement.Focus
仅仅需要在任何一个控件上调用 Focus()
方法即可将焦点设置到这个控件上。
但是需要注意,要使 Focus()
能够工作,这个元素必须满足两个条件:
Focusable
设置为 true
IsVisible
是 true
TraversalRequest
如果你并不是将焦点设置到某个特定的控件,而是希望将焦点转移,可以考虑使用 TraversalRequest
类。
比如,以下代码是将焦点转移到下一个控件,也就是按下 Tab 键时焦点会去的控件。
var traversalRequest = new TraversalRequest(FocusNavigationDirection.Next);
// view 是可视化树中的一个控件。
view.MoveFocus(traversalRequest);
键盘焦点就是你实际上按键输入和快捷键会生效的焦点,也就是当前正在工作的控件的焦点。
而 WPF 有多个焦点范围(Focus Scope),按下 Tab 键切换焦点的时候只会在当前焦点范围切焦点,不会跨范围。那么一旦跨范围切焦点的时候,焦点会去哪里呢?答案是逻辑焦点。
每个焦点范围内都有一个逻辑焦点,记录如果这个焦点范围一旦获得焦点后应该在哪个控件获得键盘焦点。
比如默认情况下 WPF 每个 Window
就是一个焦点范围,那么每个 Window
中的当前焦点就是逻辑焦点。而一旦这个 Window
激活,那么这个窗口中的逻辑焦点就会成为键盘焦点,另一个窗口当中的逻辑焦点保留,而键盘焦点则丢失。
参见我的另一篇博客:
参考资料
制作传统 Win32 程序以及 Windows Forms 程序的时候,一个用户看起来独立的窗口本就是通过各种父子窗口嵌套完成的,有大量窗口句柄,窗口之间形成父子关系。不过,对于 WPF 程序来说,一个独立的窗口实际上只有一个窗口句柄,窗口内的所有内容都是 WPF 绘制的。
如果你不熟悉 Win32 窗口中的父子窗口关系和窗口样式,那么很有可能遇到父子窗口之间“抢夺焦点”的问题,本文介绍如何解决这样的问题。
下图中的上下两个部分是两个不同的窗口,他们之间通过 SetParent
建立了父子关系。
注意看下面的窗口标题栏,当我在这些不同区域间点击的时候,窗口标题栏在黑色和灰色之间切换:
这说明当子窗口获得焦点的时候,父窗口会失去焦点并显示失去焦点的样式。
你可以在这篇博客中找到一个简单的例子:
而原因和解决方法仅有一个,就是子窗口需要有一个子窗口的样式。
具体来说,子窗口必须要有 WS_CHILD
样式。
你可以看看 Spyxx.exe 抓出来的默认普通窗口和子窗口的样式差别:
![默认普通窗口]](/static/posts/2019-09-19-10-21-31.png)
▲ 默认普通窗口
▲ 子窗口
参考资料
弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。
本文介绍 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
,订阅了以上 Foo
的 Bar
事件,那么当 a
脱离作用范围后,将可以被垃圾回收机制回收。而如果不这么做,Foo
将始终保留对 a
实例的引用,这将阻止垃圾回收。
弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。网上有很多弱事件的 NuGet 包,不过仅仅支持定义事件的时候写成弱事件而不支持让任意事件变成弱事件,并且存在性能问题。
本文将设计一套弱事件机制,不止可以让任意一个 CLR 事件成为弱事件,还具有近乎原生事件的性能。
系列博客:
本文主要为了设计一套弱事件机制而编写,因此如果你感兴趣,应该已经理解了我试图做什么事情。
当然,如果并不理解,可以阅读这个机制的应用篇,里面有具体的应用场景:
在我进行此设计之前,已有如下种类的弱事件机制:
WeakEventManager
WeakEventManager<TEventSource, TEventArgs>
Weak Event
的高赞回答
由于我希望编写的弱事件机制尽可能减少对非预期框架的依赖,而且具有很高的性能,所以我打算自己实现一套。
这三个原则,从上到下优先级依次降低。
要支持所有类型的 CLR 事件,意味着我的设计中必须要能够直接监听到任意事件,而不能所有代码都从我自己编写的代码开始。
要有很高的性能,就意味着我几乎不能使用“反射”,也不能使用委托的 DynamicInvoke
方法,还不能生成 IL 代码(首次生成很慢),也不能使用表达式树(首次编译很慢)。那么可以使用的也就只剩下两个了,一个是纯 C#/.NET 带的编译期就能确定执行的代码,另一个是使用 Roslyn 编译期在编译期间进行特殊处理。
类的使用者要编写极少量的代码,意味着能够抽取到框架中的代码就尽量抽取到框架中。
俗话说,一个好的名字是成功的一半。
因为我希望为任意 CLR 事件添加弱事件支持,所以其职责有点像“代理、中间人、中继、中转”,对应英文的 Proxy
Agent
Relay
Transfer
。最终我选择名称 Relay
(中继),因为好听。
对于 API 的设计,我有一个小原则:
我总结了好的 API 设计的一些原则:
不得不说,此类型设计的技术难度还是挺大的。虽然我们知道有 WeakReference<T>
可用,但依然存在很多的技术难点。于是 API 的设计可能要退而求其次优先满足前两个优先级更高的目标。
我们期望 API 足够简单,因此在几个备选方案中选择:
WeakEventRelay.Subscribe("Changed", OnChanged)
WeakEventRelay.Subscribe(o => o.Changed, OnChanged)
Action
来做,会遇到 o.Changed
必须出现在 +=
左边的编译错误o.Changed
必须出现在 +=
左边的编译错误,同时还会出现少量性能问题因此,直接一个方法就能完成事件注册是不可能的了,我们改用其他方法——继承自某个基类:
internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
public event FileSystemEventHandler Changed
{
add => /*实现弱事件订阅*/;
remove => /*实现弱事件注销*/;
}
}
那么实现的难点就都在 add
和 remove
方法里面了。
我们究竟需要哪些信息才可以完成弱事件机制呢?
object sender
的形式出现在你的代码中)FileSystemWatcher.Changed
事件)add
和 remove
方法中的 value
)然而事情并没有那么简单:
一
在框架通用代码中,我不可能获取到要订阅的事件。因为事件要求只能出现在 +=
的左边,不能以任何其他形式使用(包括但不限于通过参数传递,伪装成 Lambda 表达式,伪装成表达式树)。这意味着 o.Changed += OnChanged
这样的事件订阅完全写不出来通用代码(除非牺牲性能)。
那么还能怎么做呢?只能将这段写不出来的代码留给业务编写者来编写了。
也就是说,类似于 o.Changed += OnChanged
这样的代码只能交给业务开发者来实现。与此同时也注定了 OnChanged
必须由业务开发者编写(因为无法写出通用的高性能的事件处理函数,并且还能在 +=
和 -=
的时候保持同一个实例。
二
我没有办法通过抽象的办法引发一个事件。具体来说,无法在抽象的通用代码中写出 Changed.Invoke(sender, e)
这样代码。因为委托的基类 Delegate
MultiCastDelegate
没有 Invoke
方法可以使用,只有耗性能的 DynamicInvoke
方法。各种不同的委托定义虽然可以有相同的参数和返回值类型,但是却不能相互转换,因此我也不能将传入的委托转换成 Action<TSender, TArgs>
这样的通用委托。
庆幸的是,C# 提供了将方法组隐式转换委托的方法,可以让两个参数和返回值类型相同的委托隐式转换。但注意,这是隐式转换,没有运行时代码可以高性能地完成这件事情。
在 add
和 remove
方法中,value
参数就是使用方传入的事件处理函数,value.Invoke
就是方法组,可以隐式转换为通用的 Action<TSender, TArgs>
。
这意味着,我们可以将 value.Invoke
传入来以通用的方式调用事件处理函数。但是请特别注意,这会导致新创建委托实例,导致 -=
的时候实例与 +=
的时候不一致,无法注销事件。因此,我们除了传入 value.Invoke
之外,还必须传入 value
本身。
API 半残品预览
internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
public event FileSystemEventHandler Changed
{
add => Subscribe(o => o.Changed += OnChanged, value, value.Invoke);
remove => Unsubscribe(o => o.Changed -= OnChanged, value);
}
private void OnChanged(object sender, FileSystemEventArgs e) => /* 引发弱事件 */;
}
这已经开始让业务方的代码变得复杂起来了。
我们还需要能够注册、注销和引发弱事件,而这部分就没那么坑了。因为:
o.Changed += OnChanged
,value
,value.Invoke
都传进来了;我写了一个 WeakEvent<TSender, TArgs>
泛型类专门用来定义弱事件。
不过,这让业务方的代码压力更大了:
internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
private readonly WeakEvent<FileSystemEventArgs> _changed = new WeakEvent<FileSystemEventArgs>();
public event FileSystemEventHandler Changed
{
add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
remove => _changed.Remove(value);
}
private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e);
}
最后,订阅事件所需的实例,我认为最好不要能够让业务方直接能访问。因为弱事件的实现并不简单(看上面如此复杂的公开 API 就知道了),如果能够直接访问,势必带来更复杂的使用问题。所以我仅在部分方法和 Lambda 表达式参数中开放实例。
所以,构造函数需要传入事件源。
最后还留下了一个问题
虽然中继的类实例小得多,但这确实依然也是泄漏,因此需要回收。
于是我在任何可能执行代码的时机加上了回收检查:如果发现所有订阅者都已经被回收,那么“中继”也就可以被回收了,将注销所有事件源的订阅。(当然要允许重新开始订阅。)
所以最后业务方编写的中继代码又多了一些:
using System.IO;
using Walterlv.WeakEvents;
namespace Walterlv.Demo
{
internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }
private readonly WeakEvent<FileSystemEventArgs> _changed = new WeakEvent<FileSystemEventArgs>();
public event FileSystemEventHandler Changed
{
add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
remove => _changed.Remove(value);
}
private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e);
protected override void OnReferenceLost(FileSystemWatcher source)
{
source.Changed -= OnChanged;
}
}
}
虽然弱事件中继的代码复杂了点,但是:
1 最终用户的使用可是非常简单的:
public class WalterlvDemo
{
public WalterlvDemo()
{
_watcher = new FileSystemWatcher(@"D:\Desktop\walterlv.demo.md")
{
EnableRaisingEvents = true,
};
_watcher.Created += OnCreated;
_watcher.Changed += OnChanged;
_watcher.Renamed += OnRenamed;
_watcher.Deleted += OnDeleted;
}
private readonly FileSystemWatcher _watcher;
private void OnCreated(object sender, FileSystemEventArgs e) { }
private void OnChanged(object sender, FileSystemEventArgs e) { }
private void OnRenamed(object sender, RenamedEventArgs e) { }
private void OnDeleted(object sender, FileSystemEventArgs e) { }
}
2 是在懒得写,我可以加上 Roslyn 编译器生成中继代码的方式,这个我将在不久的将来加入到 Walterlv.WeakEvents 库中。
更具体的使用场景和示例代码,请阅读:
本文所涉及的全部源代码,已在 GitHub 上开源:
注意开源协议:
参考资料
弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。网上有很多弱事件的 NuGet 包,不过仅仅支持定义事件的时候写成弱事件而不支持让任意事件变成弱事件,并且存在性能问题。
本文介绍 Walterlv.WeakEvents 库来做弱事件。你可以借此将任何一个 CLR 事件当作弱事件来使用。
系列博客:
了解一下场景,你就能知道这是否是适合你的方案。
比如我正在使用 FileSystemWatcher
来监听一个文件的改变,我可能会使用到这些事件:
Created
在文件被创建时引发Changed
在文件内容或属性发生改变时引发Renamed
在文件被重命名时引发Deleted
在文件被删除时引发更具体一点的代码是这样的:
public class WalterlvDemo
{
public WalterlvDemo()
{
_watcher = new FileSystemWatcher(@"D:\Desktop\walterlv.demo.md")
{
EnableRaisingEvents = true,
};
_watcher.Created += OnCreated;
_watcher.Changed += OnChanged;
_watcher.Renamed += OnRenamed;
_watcher.Deleted += OnDeleted;
}
private readonly FileSystemWatcher _watcher;
private void OnCreated(object sender, FileSystemEventArgs e) { }
private void OnChanged(object sender, FileSystemEventArgs e) { }
private void OnRenamed(object sender, RenamedEventArgs e) { }
private void OnDeleted(object sender, FileSystemEventArgs e) { }
}
private void Foo()
{
var demo = new WalterlvDemo();
// 使用 demo
// 此方法结束后,demo 将脱离作用域,本应该可以被回收的。
}
但是,一旦我们这么写,那么我们这个类型 WalterlvDemo
的实例 demo
将无法被回收,因为 FileSystemWatcher
将始终通过事件引用着这个实例。即使你已经不再引用这个类型的任何一个实例,此实例也会被 _watcher
的事件引用着,而 FileSystemWatcher
的实例也因为 EnableRaisingEvents
而一直存在。
一个可行的解决办法是调用 FileSystemWatcher
的 Dispose
方法。不过有些时候很难决定到底在什么时机调用 Dispose
合适。
现在,我们希望有一种方法,能够在 WalterlvDemo
的实例失去作用域后被回收,最好 FileSystemWatcher
也能够自动被 Dispose
释放掉。
如果你试图解决的是类似这样的问题,那么本文就可以帮到你。
总结一下:
FileSystemWatcher
);Dispose
);demo
变量脱离作用域。)。目前有 WPF 自带的 WeakEventManager
机制,网上也有很多可用的 NuGet 包,但是都有限制:
而 Walterlv.WeakEvents 除了解决了给任一类型引入弱事件的问题,还具有非常高的性能,几乎跟定义原生事件无异。
在你需要做弱事件的项目中安装 NuGet 包:
现在,我们需要编写一个自定义的弱事件中继类 FileSystemWatcherWeakEventRelay
,即专门为 FileSystemWatcher
做的弱事件中继。
下面是一个简单点的例子,为其中的 Changed
事件做了一个中继:
using System.IO;
using Walterlv.WeakEvents;
namespace Walterlv.Demo
{
internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }
private readonly WeakEvent<FileSystemEventArgs> _changed = new WeakEvent<FileSystemEventArgs>();
public event FileSystemEventHandler Changed
{
add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
remove => _changed.Remove(value);
}
private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e);
protected override void OnReferenceLost(FileSystemWatcher source)
{
source.Changed -= OnChanged;
}
}
}
你可能会看到代码有点儿多,但是我向你保证,这是除了采用 Roslyn 编译器技术以外最高性能的方案了。如果你对弱事件的性能有要求,那么还是接受这些代码会比较好。
不要紧张,我来一一解释这些代码。另外,如果你不想懂这些代码,就按照模板一个个敲就好了,都是模板化的代码(特别适合使用 Roslyn 编译器生成,我可能接下来就会做这件事情避免你写出这些代码)。
FileSystemWatcherWeakEventRelay
,继承自库 Walterlv.WeakEvents 中的 WeakEventRelay<FileSystemWatcher>
类型。带上的泛型参数表明是针对 FileSystemWatcher
类型做弱事件中继。public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }
。这个构造函数是可以用 Visual Studio 生成的,快捷键是 Ctrl + .
或者 Alt + Enter
(快捷键功效详见:提高使用 Visual Studio 开发效率的键盘快捷键)WeakEvent<FileSystemEventArgs>
,名为 _changed
,这个就是弱事件的核心。泛型参数是事件参数的类型(注意,为了极致的性能,这里的泛型参数是事件参数的名称,而不是大多数弱事件框架中提供的事件处理委托类型)。public event FileSystemEventHandler Changed
。
add
方法固定调用 Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
。其中 Changed
是 FileSystemWatcher
中的事件,OnChanged
是我们即将定义的事件处理函数,_changed
是前面定义好的弱事件字段,而后面的 value
和 value.Invoke
是固定写法。remove
方法固定调用弱事件的 Remove
方法,即 _changed.Remove(value);
。OnChanged
,并在里面固定调用 TryInvoke(_changed, sender, e)
。OnReferenceLost
方法,用于在对象已被回收后反注册 FileSystemWatcher
中的事件。希望看了上面这 6 点之后你还能理解这些代码都是在做啥。如果依然不能理解,可以考虑:
FileSystemWatcherWeakEventRelay
的完整代码来理解哪些是可变部分哪些是不可变部分,自己替换就好;using System.IO;
using Walterlv.WeakEvents;
namespace Walterlv.Demo
{
internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay<FileSystemWatcher>
{
public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { }
private readonly WeakEvent<FileSystemEventArgs> _created = new WeakEvent<FileSystemEventArgs>();
private readonly WeakEvent<FileSystemEventArgs> _changed = new WeakEvent<FileSystemEventArgs>();
private readonly WeakEvent<RenamedEventArgs> _renamed = new WeakEvent<RenamedEventArgs>();
private readonly WeakEvent<FileSystemEventArgs> _deleted = new WeakEvent<FileSystemEventArgs>();
public event FileSystemEventHandler Created
{
add => Subscribe(o => o.Created += OnCreated, () => _created.Add(value, value.Invoke));
remove => _created.Remove(value);
}
public event FileSystemEventHandler Changed
{
add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke));
remove => _changed.Remove(value);
}
public event RenamedEventHandler Renamed
{
add => Subscribe(o => o.Renamed += OnRenamed, () => _renamed.Add(value, value.Invoke));
remove => _renamed.Remove(value);
}
public event FileSystemEventHandler Deleted
{
add => Subscribe(o => o.Deleted += OnDeleted, () => _deleted.Add(value, value.Invoke));
remove => _deleted.Remove(value);
}
private void OnCreated(object sender, FileSystemEventArgs e) => TryInvoke(_created, sender, e);
private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e);
private void OnRenamed(object sender, RenamedEventArgs e) => TryInvoke(_renamed, sender, e);
private void OnDeleted(object sender, FileSystemEventArgs e) => TryInvoke(_deleted, sender, e);
protected override void OnReferenceLost(FileSystemWatcher source)
{
source.Created -= OnCreated;
source.Changed -= OnChanged;
source.Renamed -= OnRenamed;
source.Deleted -= OnDeleted;
source.Dispose();
}
}
}
当你把上面这个自定义的弱事件中继类型写好了之后,使用它就非常简单了,对我们原有的代码改动非常小。
public class WalterlvDemo
{
public WalterlvDemo()
{
_watcher = new FileSystemWatcher(@"D:\Desktop\walterlv.demo.md")
{
EnableRaisingEvents = true,
};
++ var weakEvent = new FileSystemWatcherWeakEventRelay(_watcher);
-- _watcher.Created += OnCreated;
-- _watcher.Changed += OnChanged;
-- _watcher.Renamed += OnRenamed;
-- _watcher.Deleted += OnDeleted;
++ weakEvent.Created += OnCreated;
++ weakEvent.Changed += OnChanged;
++ weakEvent.Renamed += OnRenamed;
++ weakEvent.Deleted += OnDeleted;
}
private readonly FileSystemWatcher _watcher;
private void OnCreated(object sender, FileSystemEventArgs e) { }
private void OnChanged(object sender, FileSystemEventArgs e) { }
private void OnRenamed(object sender, RenamedEventArgs e) { }
private void OnDeleted(object sender, FileSystemEventArgs e) { }
}
我写了一个程序,每 1 秒修改一次文件;每 5 秒回收一次内存。然后使用 FileSystemWatcher
来监视这个文件的改变。
可以看到,在回收内存之后,将不会再监视文件的改变。当然,如果你期望一直可以监视改变,当然也不希望用到本文的弱事件。
一句话解答:为了高性能!
请参见我的另一篇博客:
参考资料
在 .NET 中创建进程时,可以传入 ProcessStartInfo
类的一个新实例。在此类型中,有一个 UseShellExecute
属性。
本文介绍 UseShellExecute
属性的作用,设为 true
和 false
时,分别有哪些进程启动行为上的差异。
Process.Start
本质上是启动一个新的子进程,不过这个属性的不同,使得启动进程的时候会调用不同的 Windows 的函数。
UseShellExecute = true
UseShellExecute = false
当然,如果你知道这两个函数的区别,那你自然也就了解此属性设置为 true
和 false
的区别了。
ShellExecute
的用途是打开程序或者文件或者其他任何能够打开的东西(如网址)。
也就是说,你可以在 Process.Start
的时候传入这些:
PATH
环境变量中的各种程序不过,此方法有一些值得注意的地方:
而 CreateProcess
则会精确查找路径来执行,不支持各种非可执行程序的打开。但是:
UseShellExecute
在 .NET Framework 中的的默认值是 true
,在 .NET Core 中的默认值是 false
。
如果有以下需求,那么建议设置此值为 false
:
如果你有以下需求,那么建议设置此值为 true
或者保持默认:
参考资料
在 .NET Framework 4.8 中,try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃。而 .NET Core 3.0 中不会出现这样的问题。
本文涉及的 Bug 已经报告给了微软,并且得到了微软的回复。是 .NET Framework 4.8 为了解决一个安全性问题而强行结束了进程。
This post is written in multiple languages. Please select yours:
你可以前往官方文档:
在其中,你可以找到这样一段话:
用户筛选的子句的表达式不受任何限制。 如果在执行用户筛选的表达式期间发生异常,则将放弃该异常,并视筛选表达式的值为 false。 在这种情况下,公共语言运行时继续搜索当前异常的处理程序。
即当 when
块中出现异常时,when
表达式将视为值为 false
,并且此异常将被忽略。
鉴于官方文档中的描述,我们可以编写一些示例程序来验证这样的行为。
using System;
using System.IO;
namespace Walterlv.Demo.CatchWhenCrash
{
internal class Program
{
private static void Main(string[] args)
{
try
{
try
{
Console.WriteLine("Try");
throw new FileNotFoundException();
}
catch (FileNotFoundException ex) when (ex.FileName.EndsWith(".png"))
{
Console.WriteLine("Catch 1");
}
catch (FileNotFoundException)
{
Console.WriteLine("Catch 2");
}
}
catch (Exception)
{
Console.WriteLine("Catch 3");
}
Console.WriteLine("End");
}
}
}
很显然,我们直接 new
出来的 FileNotFoundException
的 FileName
属性会保持为 null
。对其解引用会产生 NullReferenceException
。很显然代码不应该这么写,但可以用来验证 catch
-when
语句的行为。
按照官网描述,输出应该为 Try
-Catch 2
-End
。因为 when
中的异常被忽略,因此不会进入到外层的 catch
块中;因为 when
中出现异常导致表达式值视为 false
,因此进入了更合适的异常处理块 Catch 2
中。
下面两张图分别是这段代码在 .NET Core 3.0 和 .NET Framework 4.8 中的输出:
可以注意到,只有 .NET Core 3.0 中的行为符合官方文档的描述,而 .NET Framework 4.8 中甚至连 End
都没有输出!几乎可以确定,程序在 .NET Framework 4.8 中出现了致命的崩溃!
如果我们以 Visual Studio 调试启动此程序,可以看到抛出了 CLR 异常:
以下是在 Visual Studio 中单步跟踪的步骤:
由于本人金鱼般的记忆力,我竟然给微软报了三次这个 Bug:
此问题是 .NET Framework 4.8 为了修复一个安全性问题才强行结束了进程:
Process corrupting exceptions in exception filter (like access violation) now result in aborting the current process. [110375, clr.dll, Bug, Build:3694]
请参见:
获取 WPF 的依赖项属性的值时,会依照优先级去各个级别获取。这样,无论你什么时候去获取依赖项属性,都至少是有一个有效值的。有什么方法可以获取哪些属性被显式赋值过呢?如果是 CLR 属性,我们可以自己写判断条件,然而依赖项属性没有自己写判断条件的地方。
本文介绍如何获取以及显式赋值过的依赖项属性。
需要用到 DependencyObject.GetLocalValueEnumerator()
方法来获得一个可以遍历所有依赖项属性本地值。
public static void DoWhatYouLikeByWalterlv(DependencyObject dependencyObject)
{
var enumerator = dependencyObject.GetLocalValueEnumerator();
while (enumerator.MoveNext())
{
var entry = enumerator.Current;
var property = entry.Property;
var value = entry.Value;
// 在这里使用 property 和 value。
}
}
这里的 value
可能是 MarkupExtension
可能是 BindingExpression
还可能是其他一些可能延迟计算值的提供者。因此,你不能在这里获取到常规方法获取到的依赖项属性的真实类型的值。
但是,此枚举拿到的所有依赖项属性的值都是此依赖对象已经赋值过的依赖项属性的本地值。如果没有赋值过,将不会在这里的遍历中出现。
参考资料
本文介绍如何在 WPF 中获取一个依赖对象的所有依赖项属性。
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);
参考资料
在使用 .NET Remoting 开发跨进程应用的时候,你可能会遇到一些异常。因为这些异常在后验的时候非常简单但在一开始有各种异常烦扰的时候却并不清晰,所以我将这些异常整理到此文中,方便小伙伴们通过搜索引擎查阅。
System.Runtime.Remoting.RemotingException:“连接到 IPC 端口失败: 系统找不到指定的文件。”
或者英文版:
System.Runtime.Remoting.RemotingException: Failed to connect to an IPC Port: The system cannot find the file specified.
出现此异常时,说明你获取到了一个远端对象,但是在使用此对象的时候,甚至还没有注册 IPC 端口。
比如,下面的代码是注册一个 IPC 端口的一种比较粗暴的写法,传入的 portName
是 IPC 的 Uri 路径前缀。例如我可以传入 walterlv
,这样一个 IPC 对象的格式大约类似 ipc://walterlv/xxx
。
private static void RegisterChannel(string portName)
{
var serverProvider = new BinaryServerFormatterSinkProvider
{
TypeFilterLevel = TypeFilterLevel.Full,
};
var clientProvider = new BinaryClientFormatterSinkProvider();
var properties = new Hashtable
{
["portName"] = portName
};
var channel = new IpcChannel(properties, clientProvider, serverProvider);
ChannelServices.RegisterChannel(channel, false);
}
当试图访问 ipc://walterlv/foo
对象并调用其中的方法的时候,如果连 walterlv
端口都没有注册,就会出现 连接到 IPC 端口失败: 系统找不到指定的文件。
异常。
如果你已经注册了 walterlv
端口,但是没有 foo
对象,则会出现另一个错误 找不到请求的服务
,请看下一节。
System.Runtime.Remoting.RemotingException:“找不到请求的服务”
或者英文版:
System.Runtime.Remoting.RemotingException: Requested Service not found
当出现此异常时,可能的原因有三个:
更具体来说,对于第一种情况,就是当你试图跨进程访问某对象的时候,此对象还没有创建。你需要做的,是控制好对象创建的时机,创建对象的进程需要比访问它的进程更早完成对象的创建和封送。也就是下面的代码需要先调用。
RemotingServices.Marshal(@object, typeof(TObject).Name, typeof(TObject));
而对于第二种情况,你可能需要手动处理好封送对象的生命周期。重写 InitializeLifetimeService
方法并返回 null
是一个很偷懒却有效的方法。
namespace Walterlv.Remoting.Framework
{
public abstract class RemoteObject : MarshalByRefObject
{
public sealed override object InitializeLifetimeService() => null;
}
}
而对于第三种情况,你需要检查你是如何注册 .NET Remoting 通道的,创建和访问方式必须匹配。
System.Runtime.Remoting.RemotingException:“信道“ipc”已注册。”
在同一个进程中,IpcChannel
类的默认信道名称 IpcChannel.ChannelName
值是字符串 "ipc"
。如果你不通过它的参数 properties
来指定 ["name"] = "另一个名称"
,那么你就不能重复调用 ChannelServices.RegisterChannel
来调用这个信道。
说简单点,就是上面的方法 RegisterChannel
你不能在一个进程中调用两次,即便 "portName"
不同也不行。通常你也不需要去调用两次,如果一定要,请通过 HashTable
修改 name
属性。
参考资料
在 Windows 系统中,一段时间不操作键盘和鼠标,屏幕便会关闭,系统会进入睡眠状态。但有些程序(比如游戏、视频和演示文稿)在运行过程中应该阻止屏幕关闭,否则屏幕总是关闭,会导致体验会非常糟糕。
本文介绍如何编写 .NET/C# 代码临时阻止屏幕关闭以及系统进入睡眠状态。
我们需要使用到一个 Windows API:
/// <summary>
/// Enables an application to inform the system that it is in use, thereby preventing the system from entering sleep or turning off the display while the application is running.
/// </summary>
[DllImport("kernel32")]
private static extern ExecutionState SetThreadExecutionState(ExecutionState esFlags);
使用到的枚举用 C# 类型定义是:
[Flags]
private enum ExecutionState : uint
{
/// <summary>
/// Forces the system to be in the working state by resetting the system idle timer.
/// </summary>
SystemRequired = 0x01,
/// <summary>
/// Forces the display to be on by resetting the display idle timer.
/// </summary>
DisplayRequired = 0x02,
/// <summary>
/// This value is not supported. If <see cref="UserPresent"/> is combined with other esFlags values, the call will fail and none of the specified states will be set.
/// </summary>
[Obsolete("This value is not supported.")]
UserPresent = 0x04,
/// <summary>
/// Enables away mode. This value must be specified with <see cref="Continuous"/>.
/// <para />
/// Away mode should be used only by media-recording and media-distribution applications that must perform critical background processing on desktop computers while the computer appears to be sleeping.
/// </summary>
AwaymodeRequired = 0x40,
/// <summary>
/// Informs the system that the state being set should remain in effect until the next call that uses <see cref="Continuous"/> and one of the other state flags is cleared.
/// </summary>
Continuous = 0x80000000,
}
以上所有的注释均照抄自微软的官方 API 文档:
如果你擅长阅读英文,那么以上的 API 函数、枚举和注释足够你完成你的任务了。
不过,我这里提供一些封装,以应对一些常用的场景。
using System;
using System.Runtime.InteropServices;
namespace Walterlv.Windows
{
/// <summary>
/// 包含控制屏幕关闭以及系统休眠相关的方法。
/// </summary>
public static class SystemSleep
{
/// <summary>
/// 设置此线程此时开始一直将处于运行状态,此时计算机不应该进入睡眠状态。
/// 此线程退出后,设置将失效。
/// 如果需要恢复,请调用 <see cref="RestoreForCurrentThread"/> 方法。
/// </summary>
/// <param name="keepDisplayOn">
/// 表示是否应该同时保持屏幕不关闭。
/// 对于游戏、视频和演示相关的任务需要保持屏幕不关闭;而对于后台服务、下载和监控等任务则不需要。
/// </param>
public static void PreventForCurrentThread(bool keepDisplayOn = true)
{
SetThreadExecutionState(keepDisplayOn
? ExecutionState.Continuous | ExecutionState.SystemRequired | ExecutionState.DisplayRequired
: ExecutionState.Continuous | ExecutionState.SystemRequired);
}
/// <summary>
/// 恢复此线程的运行状态,操作系统现在可以正常进入睡眠状态和关闭屏幕。
/// </summary>
public static void RestoreForCurrentThread()
{
SetThreadExecutionState(ExecutionState.Continuous);
}
/// <summary>
/// 重置系统睡眠或者关闭屏幕的计时器,这样系统睡眠或者屏幕能够继续持续工作设定的超时时间。
/// </summary>
/// <param name="keepDisplayOn">
/// 表示是否应该同时保持屏幕不关闭。
/// 对于游戏、视频和演示相关的任务需要保持屏幕不关闭;而对于后台服务、下载和监控等任务则不需要。
/// </param>
public static void ResetIdle(bool keepDisplayOn = true)
{
SetThreadExecutionState(keepDisplayOn
? ExecutionState.SystemRequired | ExecutionState.DisplayRequired
: ExecutionState.SystemRequired);
}
}
}
如果你对这段封装中的 keepDisplayOn
参数,也就是 ExecutionState.DisplayRequired
枚举不了解,看看下图直接就懂了。一个指的是屏幕关闭,一个指的是系统进入睡眠。
此封装后,使用则相当简单:
// 阻止系统睡眠,阻止屏幕关闭。
SystemSleep.PreventForCurrentThread();
// 恢复此线程曾经阻止的系统休眠和屏幕关闭。
SystemSleep.RestoreForCurrentThread();
或者:
// 重置系统计时器,临时性阻止系统睡眠和屏幕关闭。
// 此效果类似于手动使用鼠标或键盘控制了一下电脑。
SystemSleep.ResetIdle();
在使用 PreventForCurrentThread
这个 API 的时候,你需要避免程序对空闲时机的控制不好,导致屏幕始终不关闭。
如果你发现无论你设置了多么短的睡眠时间和屏幕关闭时间,屏幕都不会关闭,那就是有某个程序阻止了屏幕关闭,你可以:
参考资料
最近总是收到一个异常 “System.InvalidOperationException: 转换不可逆。
”,然而看其堆栈,一点点自己写的代码都没有。到底哪里除了问题呢?
虽然异常堆栈信息里面没有自己编写的代码,但是我们还是找到了问题的原因和解决方法。
这就是抓到的此问题的异常堆栈:
System.InvalidOperationException: 转换不可逆。
在 System.Windows.Media.Matrix.Invert()
在 MS.Internal.PointUtil.TryApplyVisualTransform(Point point, Visual v, Boolean inverse, Boolean throwOnError, Boolean& success)
在 MS.Internal.PointUtil.TryClientToRoot(Point point, PresentationSource presentationSource, Boolean throwOnError, Boolean& success)
在 System.Windows.Input.MouseDevice.LocalHitTest(Boolean clientUnits, Point pt, PresentationSource inputSource, IInputElement& enabledHit, IInputElement& originalHit)
在 System.Windows.Input.MouseDevice.GlobalHitTest(Boolean clientUnits, Point pt, PresentationSource inputSource, IInputElement& enabledHit, IInputElement& originalHit)
在 System.Windows.Input.StylusWisp.WispStylusDevice.FindTarget(PresentationSource inputSource, Point position)
在 System.Windows.Input.StylusWisp.WispLogic.PreNotifyInput(Object sender, NotifyInputEventArgs e)
在 System.Windows.Input.InputManager.ProcessStagingArea()
在 System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
在 System.Windows.Input.StylusWisp.WispLogic.InputManagerProcessInput(Object oInput)
在 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
在 System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
可以看到,我们的堆栈结束点是 ExceptionWrapper.TryCatchWhen
可以得知此异常是通过 Dispatcher.UnhandledException
来捕获的。也就是说,此异常直接通过 Windows 消息被我们间接触发,而不是直接通过我们编写的代码触发。而最顶端是对矩阵求逆,而此异常是试图对一个不可逆的矩阵求逆。
如果你不想看分析过程,可以直接移步至本文的最后一节看原因和解决方案。
因为 .NET Framework 版本的 WPF 是开源的,.NET Core 版本的 WPF 目前还处于按揭开源的状态,所以我们看 .NET Framework 版本的代码来分析原因。
我按照调用堆栈从顶到底的顺序,将前面三帧的代码贴到下面。
PointUtil.TryApplyVisualTransform
public static Point TryApplyVisualTransform(Point point, Visual v, bool inverse, bool throwOnError, out bool success)
{
success = true;
if(v != null)
{
Matrix m = GetVisualTransform(v);
if (inverse)
{
if(throwOnError || m.HasInverse)
{
m.Invert();
}
else
{
success = false;
return new Point(0,0);
}
}
point = m.Transform(point);
}
return point;
}
PointUtil.TryClientToRoot
[SecurityCritical,SecurityTreatAsSafe]
public static Point TryClientToRoot(Point point, PresentationSource presentationSource, bool throwOnError, out bool success)
{
if (throwOnError || (presentationSource != null && presentationSource.CompositionTarget != null && !presentationSource.CompositionTarget.IsDisposed))
{
point = presentationSource.CompositionTarget.TransformFromDevice.Transform(point);
point = TryApplyVisualTransform(point, presentationSource.RootVisual, true, throwOnError, out success);
}
else
{
success = false;
return new Point(0,0);
}
return point;
}
你可能会说,在调用堆栈上面看不到 PointUtil.ClientToRoot
方法。但其实如果我们看一看 MouseDevice.LocalHitTest
的代码,会发现其实调用的是 PointUtil.ClientToRoot
方法。在调用堆栈上面看不到它是因为方法足够简单,被内联了。
[SecurityCritical,SecurityTreatAsSafe]
public static Point ClientToRoot(Point point, PresentationSource presentationSource)
{
bool success = true;
return TryClientToRoot(point, presentationSource, true, out success);
}
下面我们一步一步分析异常的原因。
我们先看看是什么代码在做矩阵求逆。下面截图中的方法是反编译的,就是上面我们在源代码中列出的 TryApplyVisualTransform
方法。
先获取了传入 Visual
对象的变换矩阵,然后根据参数 inverse
来对其求逆。如果矩阵可以求逆,即 HasInverse
属性返回 true
,那么代码可以继续执行下去而不会出现异常。但如果 HasInverse
返回 false
,则根据 throwOnError
来决定是否抛出异常,在需要抛出异常的情况下会真实求逆,也就是上面截图中我们看到的异常发生处的代码。
那么接下来我们需要验证三点:
Visual
是哪里来的;Visual
的变换矩阵什么情况下不可求逆;throwOnError
确定传入的是 true
吗。于是我们继续往上层调用代码中查看。
可以很快验证上面需要验证的两个点:
throwOnError
传入的是 true
;Visual
是 PresentationSource
的 RootVisual
。而 PresentationSource
的 RootVisual
是什么呢?PresentationSource
是承载 WPF 可视化树的一个对象,对于窗口 Window
,是通过 HwndSource
(PresentationSource
的子类)承载的;对于跨线程 WPF UI,可以通过自定义的 PresentationSource
子类来完成。这部分可以参考我之前的一些博客:
不管怎么说,这个指的就是 WPF 可视化树的根:
Window
来显示 WPF 窗口,那么根就是 Window
类;Popup
来承载一个弹出框,那么根就是 PopupRoot
类;对于绝大多数 WPF 开发者来说,只会碰到前面第一种情况,也就是仅仅有 Window
作为可视化树的根的情况。一般人很难直接给 PopupRoot
设置变换矩阵,一般 WPF 程序的代码也很少做跨线程或跨进程 UI。
于是我们几乎可以肯定,是有某处的代码让 Window
的变换矩阵不可逆了。
什么样的矩阵是不可逆的?
发生异常的代码是 WPF 中 Matrix.Invert
方法,其发生异常的代码如下:
首先判断矩阵的行列式 Determinant
是否为 0
,如果为 0
则抛出矩阵不可逆的异常。
WPF 的 2D 变换矩阵 \(M\) 是一个 \(3\times{3}\) 的矩阵:
\[\begin{bmatrix} M11 & M12 & 0 \\ M21 & M22 & 0 \\ OffsetX & OffsetY & 1 \end{bmatrix}\]其行列式 \(det(M)\) 是一个标量:
\[\left | A \right | = \begin{vmatrix} M11 & M12 & 0 \\ M21 & M22 & 0 \\ OffsetX & OffsetY & 1 \end{vmatrix} = M11 \times M22 - M12 \times M21\]因为矩阵求逆的时候,行列式的值会作为分母,于是会无法计算,所以行列式的值为 0 时,矩阵不可逆。
前面我们计算 WPF 的 2D 变换矩阵的行列式的值为 \(M11 \times M22 - M12 \times M21\),因此,只要使这个式子的值为 0 即可。
那么 WPF 的 2D 变换的时候,如何使此值为 0 呢?
其中,原矩阵在我们的场景下就是恒等的矩阵,即 Matrix.Identity
:
接下来缩放和旋转我们都不考虑变换中心的问题,因为变换中心的问题都可以等价为先进行缩放和旋转后,再单纯进行平移。由于平移对行列式的值没有影响,于是我们忽略。
缩放矩阵。如果水平和垂直分量分别缩放 \(ScaleX\) 和 \(ScaleY\) 倍,则缩放矩阵为:
\[\begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix}\]原矩阵点乘缩放矩阵结果为:
\[\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix}\]于是,只要 \(ScaleX\) 和 \(ScaleY\) 任何一个为 0 就可以导致新矩阵的行列式必定为 0。
旋转矩阵。假设用户设置的旋转角度为 angle
,那么换算成弧度为 angle * (Math.PI/180.0)
,我们将弧度记为 \(\alpha\),那么旋转矩阵为:
旋转矩阵点乘原矩阵的结果为:
\[\begin{bmatrix} \cos{\alpha} & \sin{\alpha} & 0 \\ -\sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} \cos{\alpha} & \sin{\alpha} & 0 \\ -\sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 1 \end{bmatrix}\]对此矩阵的行列式求值:
\[\cos^{2}{\alpha} + \sin^{2}{\alpha} = 1\]也就是说其行列式的值恒等于 1,因此其矩阵必然可求逆。
对于 WPF 的 2D 变换矩阵:
现在,我们寻找问题的方向已经非常明确了:
ScaleTransform
的 Window
,检查其是否给 ScaleX
或者 ScaleY
属性赋值为了 0
。然而,真正写一个 demo 程序来验证这个问题的时候,就发现没有这么简单。因为:
我们发现,不止是 ScaleX
和 ScaleY
属性不能设为 0
,实际上设成 0.5
或者其他值也是不行的。
唯一合理值是 1
。
那么为什么依然有异常呢?难道是 ScaleTransform
的值一开始正常,然后被修改?
编写 demo 验证,果然如此。而只有变换到 0
才会真的引发本文一开始我们提到的异常。一般会开始设为 1
而后设为 0
的代码通常是在做动画。
一定是有代码正在为窗口的 ScaleTransform
做动画。
结果全代码仓库搜索 ScaleTransform
真的找到了问题代码。
<Window x:Name="WalterlvDoubiWindow"
x:Class="Walterlv.Exceptions.Unknown.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1" />
</Window.RenderTransform>
<Window.Resources>
<Storyboard x:Key="Storyboard.Load">
<DoubleAnimation Storyboard.TargetName="WalterlvDoubiWindow"
Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
From="0" To="1" />
<DoubleAnimation Storyboard.TargetName="WalterlvDoubiWindow"
Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
From="0" To="1" />
</Storyboard>
</Window.Resources>
<Grid>
<!-- 省略的代码 -->
</Grid>
</Window>
不过,这段代码并不会导致每次都出现异常,而是在非常多次尝试中偶尔能出现一次异常。
Window
类是不可以设置 RenderTransform
属性的,但允许设置恒等(Matrix.Identity
)的变换;Window
类缩放分量设置为 0
,就会出现矩阵不可逆异常。不要给 Window
类设置变换,如果要做,请给 Window
内部的子元素设置。比如上面的例子中,我们给 Grid
设置就没有问题(而且可以做到类似的效果。
WPF 的 Window
类是不允许设置变换矩阵的。不过,总会有小伙伴为了能够设置一下试图绕过一些验证机制。
不要试图绕过,因为你会遇到更多问题。
当你试图给 Window
类设置变换矩阵的时候,会出现异常:
System.InvalidOperationException:“转换对于 Window 无效。”
无论是缩放还是旋转,都一样会出现异常。
我们在 WPF 不要给 Window 类设置变换矩阵(分析篇) 一文中已经证明在 WPF 的 2D 变换中,旋转一定不会造成矩阵不可逆,因此此验证是针对此属性的强验证。
只有做设置的变换是恒等变换的时候,才可以完成设置。
this.RenderTransform = new TranslateTransform(0, 0);
this.RenderTransform = new ScaleTransform(1, 1);
this.RenderTransform = new RotateTransform(0);
this.RenderTransform = new MatrixTransform(Matrix.Identity);
然而你可以通过先设置变换,再修改变换值的方式绕过验证:
var scaleTransform = new ScaleTransform(1, 1);
this.RenderTransform = scaleTransform;
scaleTransform.ScaleX = 0.5;
scaleTransform.ScaleY = 0.5;
实际上,你绕过也没有关系,可是这样的设置实际上是没有任何效果的。
不过为什么还是会有小伙伴这么设置呢?
是因为小伙伴同时还设置了窗口透明 AllowsTransparency="True"
、WindowStyle="None"
和 Background="Transparent"
,导致看起来好像这个变换生效了一样。
此设置不仅没有效果,还会引发异常,请阅读我的另一篇博客了解:
在 WPF 程序中,可能会存在 Application.Current.Dispatcher.Xxx
这样的代码让一部分逻辑回到主 UI 线程。因为发现在调用这句代码的时候出现了 NullReferenceException
,于是就有三位小伙伴告诉我说 Current
和 Dispatcher
属性都可能为 null
。
然而实际上这里只可能 Current
为 null
而此上下文的 Dispatcher
是绝对不会为 null
的。(当然我们这里讨论的是常规编程手段,如果非常规手段,你甚至可以让实例的 this
为 null
呢……)
当你的应用程序退出时,所有 UI 线程的代码都不再会执行,因此这是安全的;但所有非 UI 线程的代码依然在继续执行,此时随时可能遇到 Application.Current
属性为 null。
由于本文所述的两个部分都略长,所以拆分成两篇博客,这样更容易理解。
Application.Current
静态属性Application
类型的源代码会非常长,所以这里就不贴了,可以前往这里查看:
其中,Current
返回的是 _appInstance
的静态字段。因此 _appInstance
字段为 null
的时机就是 Application.Current
为 null
的时机。
/// <summary>
/// The Current property enables the developer to always get to the application in
/// AppDomain in which they are running.
/// </summary>
static public Application Current
{
get
{
// There is no need to take the _globalLock because reading a
// reference is an atomic operation. Moreover taking a lock
// also causes risk of re-entrancy because it pumps messages.
return _appInstance;
}
}
由于 _appInstance
字段是私有字段,所以仅需调查这个类本身即可找到所有的赋值时机。(反射等非常规手段需要排除在外,因为这意味着开发者是逗比——自己闯的祸不能怪 WPF 框架。)
_appInstance
的赋值时机有两处:
Application
的实例构造函数(注意哦,是实例构造函数而不是静态构造函数);Application.DoShutdown
方法。在 Application
的实例构造函数中:
_appInstance
的赋值是线程安全的,这意味着多个 Application
实例的构造不会因为线程安全问题导致 _appInstance
字段的状态不正确。_appCreatedInThisAppDomain
为 true
那么,将抛出异常,组织此应用程序域中创建第二个 Application
类型的实例。/// <summary>
/// Application constructor
/// </summary>
/// <SecurityNote>
/// Critical: This code posts a work item to start dispatcher if in the browser
/// PublicOk: It is ok because the call itself is not exposed and the application object does this internally.
/// </SecurityNote>
[SecurityCritical]
public Application()
{
// 省略了一部分代码。
lock(_globalLock)
{
// set the default statics
// DO NOT move this from the begining of this constructor
if (_appCreatedInThisAppDomain == false)
{
Debug.Assert(_appInstance == null, "_appInstance must be null here.");
_appInstance = this;
IsShuttingDown = false;
_appCreatedInThisAppDomain = true;
}
else
{
//lock will be released, so no worries about throwing an exception inside the lock
throw new InvalidOperationException(SR.Get(SRID.MultiSingleton));
}
}
// 省略了一部分代码。
}
也就是说,此类型实际上是设计为单例的。在第一个实例构造出来之后,单例的实例即可开始使用。
此单例实例的唯一结束时机就是 Application.DoShutdown
方法。这是唯一将 _appInstance
赋值为 null
的代码。
/// <summary>
/// DO NOT USE - internal method
/// </summary>
///<SecurityNote>
/// Critical: Calls critical code: Window.InternalClose
/// Critical: Calls critical code: HwndSource.Dispose
/// Critical: Calls critical code: PreloadedPackages.Clear()
///</SecurityNote>
[SecurityCritical]
internal virtual void DoShutdown()
{
// 省略了一部分代码。
// Event handler exception continuality: if exception occurs in ShuttingDown event handler,
// our cleanup action is to finish Shuttingdown. Since Shuttingdown cannot be cancelled.
// We don't want user to use throw exception and catch it to cancel Shuttingdown.
try
{
// fire Applicaiton Exit event
OnExit(e);
}
finally
{
SetExitCode(e._exitCode);
// By default statics are shared across appdomains, so need to clear
lock (_globalLock)
{
_appInstance = null;
}
_mainWindow = null;
_htProps = null;
NonAppWindowsInternal = null;
// 省略了一部分代码。
}
}
可以调用到此代码的公共 API 有:
Application.Shutdown
实例方法Window
关闭的若干方法(InternalDispose
)IBrowserHostServices.PostShutdown
接口方法因此,所有直接或间接调用到以上方法的地方都会导致 Application.Current
属性被赋值为 null
。
从以上的分析可以得知,只要你还能在 Application.DoShutdown
执行之后继续执行代码,那么这部分的代码都将面临着 Application.Current
为 null
风险。
那么,到底有哪些时机可能遇到 Application.Current
为 null
呢?这部分就与读者项目中所用的业务代码强相关了。
但是这部分业务代码会有一些公共特征帮助你判定你是否可能写出遭遇 Application.Current
为 null
的代码。
此特征是:此代码与 Application.Current
不在同一线程。
Application.Current
不在同一线程对于 WPF 程序,你的多数代码可能是由用户交互产生,即便有后续代码的执行,也依然是从 UI 交互产生。这样的代码不会遇到 Application.Current
为 null
的情况。
但是,如果你的代码由非 UI 线程触发,例如在 Usb
设备改变、与其他端的通信、某些异步代码的回调等等,这些代码不受 Dispatcher
是否调度影响,几乎一定会执行。因此 Application.Current
就算赋值为 null
了,它们也不知道,依然会继续执行,于是就会遭遇 Application.Current
为 null
。
这本质上是一个线程安全问题。
Invoke/BeginInvoke/InvokeAsync
的代码不会出问题Application.DoShutdown
方法被 ShutdownImpl
包装,且所有调用均从此包装进入,因此,所有可能导致 Application.Current
为 null
的代码,均会调用此方法,也就是说,会调用 Dispatcher.CriticalInvokeShutdown
实例方法。
/// <summary>
/// This method gets called on dispatch of the Shutdown DispatcherOperationCallback
/// </summary>
///<SecurityNote>
/// Critical: Calls critical code: DoShutdown, Dispatcher.CritcalInvokeShutdown()
///</SecurityNote>
[SecurityCritical]
private void ShutdownImpl()
{
// Event handler exception continuality: if exception occurs in Exit event handler,
// our cleanup action is to finish Shutdown since Exit cannot be cancelled. We don't
// want user to use throw exception and catch it to cancel Shutdown.
try
{
DoShutdown();
}
finally
{
// Quit the dispatcher if we ran our own.
if (_ownDispatcherStarted == true)
{
Dispatcher.CriticalInvokeShutdown();
}
ServiceProvider = null;
}
}
所有的关闭 Dispatcher
的调用有两类,Application
关闭时调用的是内部方法 CriticalInvokeShutdown
。
CriticalInvokeShutdown
,即以 Send
优先级 Invoke
关闭方法,而 Send
优先级调用 Invoke
几乎等同于直接调用(为什么是等同而不是直接调用?因为还需要考虑回到 Dispatcher
初始化时所在的线程)。BeginInvokeShutdown
,即以指定的优先级 InvokeAsync
关闭方法。而关闭 Dispatcher
意味着所有使用 Invoke/BeginInvoke/InvokeAsync
的任务将终止。
//<SecurityNote>
// Critical - as it accesses security critical data ( window handle)
//</SecurityNote>
[SecurityCritical]
private void ShutdownImplInSecurityContext(Object state)
{
// 省略了一部分代码。
// Now that the queue is off-line, abort all pending operations,
// including inactive ones.
DispatcherOperation operation = null;
do
{
lock(_instanceLock)
{
if(_queue.MaxPriority != DispatcherPriority.Invalid)
{
operation = _queue.Peek();
}
else
{
operation = null;
}
}
if(operation != null)
{
operation.Abort();
}
} while(operation != null);
// 省略了一部分代码。
}
由于此终止代码在 Dispatcher
所在的线程执行,而所有 Invoke/BeginInvoke/InvokeAsync
代码也都在此线程执行,因此这些代码均不会并发。已经执行的代码会在此终止代码之前,而在此终止代码之后也不会再执行任何 Invoke/BeginInvoke/InvokeAsync
的任务了。
Invoke/BeginInvoke/InvokeAsync
或间接通过此方法(如 WPF 控件相关事件)调用的代码,均不会遭遇 Application.Current
为 null
。async
/ await
并使用默认上下文执行的代码,均不会遭遇 Application.Current
为 null
。(这意味着你没有使用 .ConfigureAwait(false)
,详见在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁 - walterlv。)using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace Walterlv.Demo.ApplicationDispatcher
{
class Program
{
[STAThread]
static void Main(string[] args)
{
var app = new Application();
Task.Delay(1000).ContinueWith(t =>
{
app.Dispatcher.InvokeAsync(() => app.Shutdown());
});
Task.Delay(2000).ContinueWith(t =>
{
Application.Current.Dispatcher.InvokeAsync(() => { });
});
app.Run();
Thread.Sleep(2000);
}
}
}
总结以上所有的分析:
Application
不在同一个线程的代码,都可能遭遇 Application.Current
为 null
。Application
在同一个线程的代码,都不可能遇到 Application.Current
为 null
。这其实是一个线程安全问题。用所有业务开发者都可以理解的说法描述就是:
当你的应用程序退出时,所有 UI 线程的代码都不再会执行,因此这是安全的;但所有非 UI 线程的代码依然在继续执行,此时随时可能遇到 Application.Current
属性为 null。
因此,记得所有非 UI 线程的代码,如果需要转移到 UI 线程执行,记得判空:
private void OnUsbDeviceChanged(object sender, EventArgs e)
{
// 记得这里需要判空,因为此上下文可能在非 UI 线程。
Application.Current?.InvokeAsync(() => { });
}
Application.Dispatcher
实例属性关于 Application.Dispatcher
是否可能为 null
的分析,由于比较长,请参见我的另一篇博客:
参考资料
在 WPF 程序中,可能会存在 Application.Current.Dispatcher.Xxx
这样的代码让一部分逻辑回到主 UI 线程。因为发现在调用这句代码的时候出现了 NullReferenceException
,于是就有三位小伙伴告诉我说 Current
和 Dispatcher
属性都可能为 null
。
然而实际上这里只可能 Current
为 null
而此上下文的 Dispatcher
是绝对不会为 null
的。(当然我们这里讨论的是常规编程手段,如果非常规手段,你甚至可以让实例的 this
为 null
呢……)
由于本文所述的两个部分都略长,所以拆分成两篇博客,这样更容易理解。
Application.Dispatcher
实例属性Application.Dispatcher
实例属性来自于 DispatcherObject
。
为了分析此属性是否可能为 null
,我现在将 DispatcherObject
的全部代码贴在下面:
using System;
using System.Windows;
using System.Threading;
using MS.Internal.WindowsBase; // FriendAccessAllowed
namespace System.Windows.Threading
{
/// <summary>
/// A DispatcherObject is an object associated with a
/// <see cref="Dispatcher"/>. A DispatcherObject instance should
/// only be access by the dispatcher's thread.
/// </summary>
/// <remarks>
/// Subclasses of <see cref="DispatcherObject"/> should enforce thread
/// safety by calling <see cref="VerifyAccess"/> on all their public
/// methods to ensure the calling thread is the appropriate thread.
/// <para/>
/// DispatcherObject cannot be independently instantiated; that is,
/// all constructors are protected.
/// </remarks>
public abstract class DispatcherObject
{
/// <summary>
/// Returns the <see cref="Dispatcher"/> that this
/// <see cref="DispatcherObject"/> is associated with.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
public Dispatcher Dispatcher
{
get
{
// This property is free-threaded.
return _dispatcher;
}
}
// This method allows certain derived classes to break the dispatcher affinity
// of our objects.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void DetachFromDispatcher()
{
_dispatcher = null;
}
// Make this object a "sentinel" - it can be used in equality tests, but should
// not be used in any other way. To enforce this and catch bugs, use a special
// sentinel dispatcher, so that calls to CheckAccess and VerifyAccess will
// fail; this will catch most accidental uses of the sentinel.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void MakeSentinel()
{
_dispatcher = EnsureSentinelDispatcher();
}
private static Dispatcher EnsureSentinelDispatcher()
{
if (_sentinelDispatcher == null)
{
// lazy creation - the first thread reaching here creates the sentinel
// dispatcher, all other threads use it.
Dispatcher sentinelDispatcher = new Dispatcher(isSentinel:true);
Interlocked.CompareExchange<Dispatcher>(ref _sentinelDispatcher, sentinelDispatcher, null);
}
return _sentinelDispatcher;
}
/// <summary>
/// Checks that the calling thread has access to this object.
/// </summary>
/// <remarks>
/// Only the dispatcher thread may access DispatcherObjects.
/// <p/>
/// This method is public so that any thread can probe to
/// see if it has access to the DispatcherObject.
/// </remarks>
/// <returns>
/// True if the calling thread has access to this object.
/// </returns>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public bool CheckAccess()
{
// This method is free-threaded.
bool accessAllowed = true;
Dispatcher dispatcher = _dispatcher;
// Note: a DispatcherObject that is not associated with a
// dispatcher is considered to be free-threaded.
if(dispatcher != null)
{
accessAllowed = dispatcher.CheckAccess();
}
return accessAllowed;
}
/// <summary>
/// Verifies that the calling thread has access to this object.
/// </summary>
/// <remarks>
/// Only the dispatcher thread may access DispatcherObjects.
/// <p/>
/// This method is public so that derived classes can probe to
/// see if the calling thread has access to itself.
/// </remarks>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public void VerifyAccess()
{
// This method is free-threaded.
Dispatcher dispatcher = _dispatcher;
// Note: a DispatcherObject that is not associated with a
// dispatcher is considered to be free-threaded.
if(dispatcher != null)
{
dispatcher.VerifyAccess();
}
}
/// <summary>
/// Instantiate this object associated with the current Dispatcher.
/// </summary>
protected DispatcherObject()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
private Dispatcher _dispatcher;
private static Dispatcher _sentinelDispatcher;
}
}
代码来自:DispatcherObject.cs。
Dispatcher
属性仅仅是在获取 _dispatcher
字段的值,因此我们只需要看 _dispatcher
字段的赋值时机,以及所有给 _dispatcher
赋值的代码。
由于 _dispatcher
字段是私有字段,所以仅需调查这个类本身即可找到所有的赋值时机。(反射等非常规手段需要排除在外,因为这意味着开发者是逗比——自己闯的祸不能怪 WPF 框架。)
先来看看 dispatcher
字段的赋值时机。
DispatcherObject
仅有一个构造函数,而这个构造函数中就已经给 _dispatcher
赋值了,因此其所有的子类的初始化之前,_dispatcher
就会被赋值。
protected DispatcherObject()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
那么所赋的值是否可能为 null
呢,这就要看 Dispatcher.CurrentDispatcher
是否可能返回一个 null
了。
以下是 Dispatcher.CurrentDispatcher
的属性获取代码:
public static Dispatcher CurrentDispatcher
{
get
{
// Find the dispatcher for this thread.
Dispatcher currentDispatcher = FromThread(Thread.CurrentThread);;
// Auto-create the dispatcher if there is no dispatcher for
// this thread (if we are allowed to).
if(currentDispatcher == null)
{
currentDispatcher = new Dispatcher();
}
return currentDispatcher;
}
}
可以看到,无论前面的方法得到的值是否是 null
,后面都会再给 currentDispatcher
局部变量赋值一个新创建的实例的。因此,此属性是绝对不会返回 null
的。
由此可知,DispatcherObject
自构造起便拥有一个不为 null
的 Dispatcher
属性,其所有子类在初始化之前便会得到不为 null
的 Dispatcher
属性。
现在我们来看看在初始化完成之后,后面是否有可能将 _dispatcher
赋值为 null。
给 _dispatcher
字段的赋值代码仅有两个:
// This method allows certain derived classes to break the dispatcher affinity
// of our objects.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void DetachFromDispatcher()
{
_dispatcher = null;
}
// Make this object a "sentinel" - it can be used in equality tests, but should
// not be used in any other way. To enforce this and catch bugs, use a special
// sentinel dispatcher, so that calls to CheckAccess and VerifyAccess will
// fail; this will catch most accidental uses of the sentinel.
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void MakeSentinel()
{
_dispatcher = EnsureSentinelDispatcher();
}
第一个 DetachFromDispatcher
很好理解,让 DispatcherObject
跟 Dispatcher
无关。在整个 WPF 的代码中,使用此方法的仅有以下 6 处:
Freezable.Freeze
实例方法BeginStoryboard.Seal
实例方法Style.Seal
实例方法TriggerBase.Seal
实例方法StyleHelper
在 SealTemplate
静态方法中对 FrameworkTemplate
类型的实例调用此方法ResourceDictionary
在构造函数中为 DispatcherObject
类型的 DummyInheritanceContext
属性调用此方法而 Application
类型不是以上任何一个类型的子类(Application
类的直接基类是 DispatcherObject
),因此 Application
类中的 Dispatcher
属性不可能因为 DetachFromDispatcher
方法的调用而被赋值为 null
。
接下来看看 MakeSentinel
方法,此方法的作用不如上面方法那样直观,实际上它的作用仅仅为了验证某个方法调用时所在的线程是否是符合预期的(给 VerifyAccess
和 CheckAccess
使用)。
使用此方法的仅有 1 处:
ItemsControl
所用的 ItemInfo
类的静态构造函数internal static readonly DependencyObject SentinelContainer = new DependencyObject();
internal static readonly DependencyObject UnresolvedContainer = new DependencyObject();
internal static readonly DependencyObject KeyContainer = new DependencyObject();
internal static readonly DependencyObject RemovedContainer = new DependencyObject();
static ItemInfo()
{
// mark the special DOs as sentinels. This helps catch bugs involving
// using them accidentally for anything besides equality comparison.
SentinelContainer.MakeSentinel();
UnresolvedContainer.MakeSentinel();
KeyContainer.MakeSentinel();
RemovedContainer.MakeSentinel();
}
所有这些使用都与 Application
无关。
总结以上所有的分析:
Application
类型的实例在初始化之前,Dispatcher
属性就已经被赋值且不为 null
;_dispatcher
属性的常规方法均与 Application
类型无关;因此,所有常规手段均不会让 Application
类的 Dispatcher
属性拿到 null
值。如果你还说拿到了 null
,那就检查是否有逗比程序员通过反射或其他手段将 _dispatcher
字段改为了 null
吧……
Application.Current
静态属性关于 Application.Current
是否可能为 null
的分析,由于比较长,请参见我的另一篇博客:
参考资料
在微软的官方文档中,说 SetParent
可以在进程内设置,也可以跨进程设置。当使用跨进程设置窗口的父子关系时,你需要注意本文提到的一些问题,避免踩坑。
SetParent
关于 SetParent
函数设置窗口父子关系的文档可以看这个:
在这篇文章的 DPI 感知一段中明确写明了在进程内以及跨进程设置父子关系时的一些行为。虽然没有明确说明支持跨进程设置父子窗口,不过这段文字就几乎说明 Windows 系统对于跨进程设置窗口父子关系还是支持的。
但 Raymond Chen 在 Is it legal to have a cross-process parent/child or owner/owned window relationship? 一文中有另一段文字:
If I remember correctly, the documentation for
SetParent
used to contain a stern warning that it is not supported, but that remark does not appear to be present any more. I have a customer who is reparenting windows between processes, and their application is experiencing intermittent instability.
如果我没记错的话,SetParent
的文档曾经包含一个严厉的警告表明它不受支持,但现在这段备注似乎已经不存在了。我就遇到过一个客户跨进程设置窗口之间的父子关系,然后他们的应用程序间歇性不稳定。
这里表明了 Raymond Chen 对于跨进程设置父子窗口的一些担忧,但从文档趋势来看,还是支持的。只是这种担忧几乎说明跨进程设置 SetParent
存在一些坑。
那么本文就说说跨进程设置父子窗口的一些坑。
我们会感觉到 Windows 中某个窗口有响应(比如鼠标点击有反应),是因为这个窗口在处理 Windows 消息。窗口进行消息循环不断地处理消息使得各种各样的用户输入可以被处理,并正确地在界面上显示。
一个典型的消息循环大概像这样:
while(GetMessage(ref msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
对于显示了窗口的某个线程调用了 GetMessage
获取了消息,Windows 系统就会认为这个线程有响应。相反,如果长时间不调用 GetMessage
,Windows 就会认为这个线程无响应。TranslateMessage
则是翻译一些消息(比如从按键消息翻译成字符消息)。真正处理 GetMessage
中的内容则是后面的调度消息 DispatchMessage
,是这个函数的调用使得我们 UI 界面上的内容可以有可见的反映。
一般来说,每个创建了窗口的线程都有自己独立的消息循环,且不会互相影响。然而一旦这些窗口之间建立了父子关系之后就会变得麻烦起来。
Windows 会让具有父子关系的所有窗口的消息循环强制同步。具体指的是,所有具有父子关系的窗口消息循环,其消息循环会串联成一个队列(这样才可以避免消息循环的并发)。
也就是说,如果你有 A、B、C、D 四个窗口,分属不同进程,A 是 B、C、D 窗口的父窗口,那么当 A 在处理消息的时候,B、C、D 的消息循环就会卡在 GetMessage
的调用。同样,无论是 B、C 还是 D 在处理消息的时候,其他窗口也会同样卡在 GetMessage
的调用。这样,所有进程的 UI 线程实际上会互相等待,所有通过消息循环执行的代码都不会同时执行。然而实际上 Windows GUI 应用程序的开发中基本上 UI 代码都是通过消息循环来执行的,所以这几乎等同于所有进程的 UI 线程强制同步成类似一个 UI 线程的效果了。
带来的副作用也就相当明显,任何一个进程卡了 UI,其他进程的 UI 将完全无响应。当然,不依赖消息循环的代码不会受此影响,比如 WPF 应用程序的动画和渲染。
对于 SetParent
造成的这些问题,实际上没有官方的解决方案,你需要针对你不同的业务采用不同的解决办法。
正如 Raymond Chen 所说:
(It’s one of those “if you don’t already know what the consequences are, then you are not smart enough to do it correctly” things. You must first become the master of the rules before you can start breaking them.)
正如有些人说的“如果你不知道后果,那么你也不足以正确地完成某件事情”。在开始破坏规则之前,您必须先成为规则的主人。
你必须清楚跨进程设置父子窗口带来的各种副作用,然后针对性地给出解决方案:
参考资料
[Is it legal to have a cross-process parent/child or owner/owned window relationship? | The Old New Thing](https://devblogs.microsoft.com/oldnewthing/?p=4683) |
当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现错误:“BuildWindowCore 无法返回寄宿的子窗口句柄。
”。
这是很典型的 Win32 错误,本文介绍如何修复此错误。
我们在 MainWindow
中嵌入一个其他的窗口来承载新的 WPF 控件。一般情况下我们当然不会这么去做,但是如果我们要跨越进程边界来完成 WPF 渲染内容的融合的时候,就需要嵌入一个新的窗口了。
WPF 中可以使用 HwndSource
来包装一个 WPF 控件到 Win32 窗口,使用自定义的继承自 HwndHost
的类可以把 Win32 窗口包装成 WPF 控件。由于窗口句柄是可以跨越进程边界传递的,所以这样的方式可以完成跨进程的 WPF 控件显示。
你有可能在调试嵌入窗口代码的时候遇到错误:
System.InvalidOperationException:“BuildWindowCore 无法返回寄宿的子窗口句柄。”
英文是:
BuildWindowCore failed to return the hosted child window handle.
此异常的原因非常简单,是 HwndSource
的 BuildWindowCore
的返回值有问题。具体来说,就是子窗口的句柄返回了 0。
也就是下面这段代码中 return new HandleRef(this, IntPtr.Zero)
这句,第二个参数是 0。
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
const int WS_CHILD = 1073741824;
const int WS_CLIPCHILDREN = 33554432;
var parameters = new HwndSourceParameters("demo")
{
ParentWindow = hwndParent.Handle,
WindowStyle = (int)(WS_CHILD | WS_CLIPCHILDREN),
TreatAncestorsAsNonClientArea = true,
};
var source = new HwndSource(parameters);
source.RootVisual = new Button();
return new HandleRef(this, _handle);
}
要解决,就需要传入正确的句柄值。当然上面的代码为了示例,故意传了一个不知道哪里的 _handle
,实际上应该传入 source.Handle
才是正确的。
当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现错误:“System.InvalidOperationException:“寄宿 HWND 必须是子窗口。”
”。
这是很典型的 Win32 错误,本文介绍如何修复此错误。
我们在 MainWindow
中嵌入一个其他的窗口来承载新的 WPF 控件。一般情况下我们当然不会这么去做,但是如果我们要跨越进程边界来完成 WPF 渲染内容的融合的时候,就需要嵌入一个新的窗口了。
WPF 中可以使用 HwndSource
来包装一个 WPF 控件到 Win32 窗口,使用自定义的继承自 HwndHost
的类可以把 Win32 窗口包装成 WPF 控件。由于窗口句柄是可以跨越进程边界传递的,所以这样的方式可以完成跨进程的 WPF 控件显示。
下面是最简单的一个例子,为了简单,没有跨进程传递 Win32 窗口句柄,而是直接创建出来。
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace Walterlv.Demo.HwndWrapping
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Content = new HwndWrapper();
}
}
public class HwndWrapper : HwndHost
{
private HwndSource _source;
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
var parameters = new HwndSourceParameters("walterlv");
_source = new HwndSource(parameters);
// 这里的 ChildPage 是一个继承自 UseControl 的 WPF 控件,你可以自己创建自己的 WPF 控件。
_source.RootVisual = new ChildPage();
return new HandleRef(this, _source.Handle);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
_source?.Dispose();
}
}
}
当运行此代码的时候,会提示错误:
System.InvalidOperationException:“寄宿 HWND 必须是子窗口。”
或者英文版:
System.InvalidOperationException:”Hosted HWND must be a child window.”
这是一个 Win32 错误,因为我们试图将一个普通的窗口嵌入到另一个窗口中,而实际上要完成嵌入需要子窗口才行。
那么如何设置一个 Win32 窗口为子窗口呢?使用 SetWindowLong
来设置 Win32 窗口的样式是可以的。不过我们因为使用了 HwndSource
,所以可以通过 HwndSourceParameters
来更方便地设置窗口样式。
我们需要将 HwndSourceParameters
那一行改成这样:
++ const int WS_CHILD = 0x40000000;
-- var parameters = new HwndSourceParameters("walterlv");
++ var parameters = new HwndSourceParameters("walterlv")
++ {
++ ParentWindow = hwndParent.Handle,
++ WindowStyle = WS_CHILD,
++ };
最关键的是两点:
WindowStyle
为 WS_CHILD
;ParentWindow
为 hwndParent.Handle
(我们使用参数中传入的 hwndParent
作为父窗口)。现在再运行,即可正常显示此嵌套窗口:
另外,WindowStyle
属性最好加上 WS_CLIPCHILDREN
,详情请阅读:
参考资料
当试图在 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
设置一次子窗口。
我们做的公共库可能通过 nuget.org 发布,也可能是自己搭建 NuGet 服务器。但是,如果某个包正在开发中,需要快速验证其是否解决掉一些诡异的 bug 的话,除了单元测试这种间接的测试方法,还可以在本地安装未发布的 NuGet 包的方法来快速调试。
本文介绍如何本地打包发布 NuGet 包,然后通过 mklink 收集所有的本地包达到快速调试的目的。
我有另一篇博客介绍如何将本地文件夹设置称为 NuGet 包源:
在 Visual Studio 中打开 工具
-> 选项
-> NuGet 包管理器
-> 包源
可以直接将一个本地文件夹设置称为 NuGet 包源。
其他设置方法可以去那篇博客当中阅读。
如下图,是我通过 mklink 将散落在各处的 NuGet 包的调试输出目录收集了起来:
比如,点开其中的 Walterlv.Packages
可以看到 Walterlv.Packages
仓库中输出的 NuGet 包:
由于我的每一个文件夹都是指向的 Visual Studio 编译后的输出目录,所以,只需要使用 Visual Studio 重新编译一下项目,文件夹中的 NuGet 包即会更新。
于是,这相当于我在一个文件夹中,包含了我整个计算机上所有库项目的 NuGet 包,只需要将这个文件夹设置称为 NuGet 包源,即可直接调试本地任何一个公共组件库打出来的 NuGet 包。
如下图,是我将那个收集所有 NuGet 文件夹的目录设置成为了 NuGet 源:
于是,我可以在 Visual Studio 的包管理器中看到所有还没有发布的,依然处于调试状态的各种库:
基于此,我们可以在包还没有编写完的时候调试,验证速度非常快。
你可以使用 dynamic
来定义一个变量或者字段,随后你可以像弱类型语言一样调用这个实例的各种方法,就像你一开始就知道这个类型的所有属性和方法一样。
但是,使用不当又会遇到各种问题,本文收集使用过程中可能会遇到的各种问题,帮助你解决掉它们。
dynamic
可以这么用:
dynamic foo = GetSomeInstance();
foo.Run("欢迎访问吕毅(lvyi)的博客:blog.walterlv.com");
object GetSomeInstance()
{
return 诡异的东西;
}
我们的 GetSomeInstance
明明返回的是 object
,我们却可以调用真实类中的方法。
接下来讲述使用 dynamic
过程中可能会遇到的问题和解决方法。
你初次在你的项目中引入 dynamic
关键字后,会出现编译错误,提示 “缺少编译器要求的成员”。
error CS0656: 缺少编译器要求的成员“Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create”
需要为你的项目安装以下两个 NuGet 包:
于是你的项目里面会多出两个引用:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net48</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
++ <PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
++ <PackageReference Include="System.Dynamic.Runtime" Version="4.3.0" />
</ItemGroup>
</Project>
你需要引用 Microsoft.CSharp
:
于是你的项目里面会多出一项引用:
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
<ItemGroup>
++ <Reference Include="Microsoft.CSharp" />
</ItemGroup>
</Project>
{0}
是类型名称,而 {1}
是使用 dynamic
访问的属性或者方法的名称。
比如,我试图从某个 Attribute
中访问到 Key
属性的时候会抛出以下异常:
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:““System.Attribute”未包含“Key”的定义”
出现此异常的原因是:
dynamic
所引用的对象里面,没有签名相同的 public
的属性或者方法于是,如果你确认你的类型里面是有这个属性或者方法的话,那么就需要注意需要将此成员改成 public
才可以访问。
参考资料
我们有弱引用 WeakReference<T>
可以用来保存可被垃圾回收的对象,也有可以保存键值对的 ConditionalWeakTable
。
我们经常会考虑制作缓存池。虽然一般不推荐这么设计,但是你可以使用本文所述的方法和代码作为按垃圾回收缓存的缓存池的设计。
既然现有 WeakReference<T>
和 ConditionalWeakTable
可以帮助我们实现弱引用,那么我们可以考虑封装这两个类中的任何一个或者两个来帮助我们完成弱引用集合。
ConditionalWeakTable
类型仅仅在 internal
级别可以访问到集合中的所有的元素,对外开放的接口当中是无法拿到集合中的所有元素的,仅仅能根据 Key 来找到 Value 而已。所以如果要根据 ConditionalWeakTable
来实现弱引用集合那么需要自己记录集合中的所有的 Key,而这样的话我们依然需要自己实现一个用来记录所有 Key 的弱引用集合,相当于鸡生蛋蛋生鸡的问题。
所以我们考虑直接使用 WeakReference<T>
来实现弱引用集合。
自己维护一个列表 List<WeakReference<T>>
,对外开放的 API 只能访问到其中未被垃圾回收到的对象。
在设计此类型的时候,有一个非常大的需要考虑的因素,就是此类型中的元素个数是不确定的,如果设计不当,那么此类型的使用者可能写出的每一行代码都是 Bug。
你可以参考我的另一篇博客了解设计这种不确定类型的 API 的时候的一些指导:
总结起来就是:
那么这个原则怎么体现在此弱引用集合的类型设计上呢?
IList<T>
我们来看看 IList<T>
接口是否可行:
public class WeakCollection<T> : IList<T> where T : class
{
public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public int Count => throw new NotImplementedException();
public bool IsReadOnly => throw new NotImplementedException();
public void Add(T item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public bool Contains(T item) => throw new NotImplementedException();
public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
public int IndexOf(T item) => throw new NotImplementedException();
public void Insert(int index, T item) => throw new NotImplementedException();
public bool Remove(T item) => throw new NotImplementedException();
public void RemoveAt(int index) => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}
this[]
、Count
、IsReadOnly
、Contains
、CopyTo
、IndexOf
、GetEnumerator
这些都是在获取状态,Add
、Clear
、Remove
是在修改状态,而 Insert
、RemoveAt
会在修改状态的同时读取状态。
这么多的获取和修改状态的方法,如果提供出去,还指望使用者能够正常使用,简直是做梦!违背以上两个原则。
ICollection<T>
那我们看看 IList<T>
的底层集合 ICollection<T>
,实际上并没有解决问题,所以依然排除不能用!
public class WeakCollection<T> : ICollection<T> where T : class
{
-- public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public int Count => throw new NotImplementedException();
public bool IsReadOnly => throw new NotImplementedException();
public void Add(T item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public bool Contains(T item) => throw new NotImplementedException();
public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
-- public int IndexOf(T item) => throw new NotImplementedException();
-- public void Insert(int index, T item) => throw new NotImplementedException();
public bool Remove(T item) => throw new NotImplementedException();
-- public void RemoveAt(int index) => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}
不过,Add
和 Remove
方法可能我们会考虑留下来,但这就不能是继承自 ICollection<T>
了。
IEnumerable<T>
IEnumerable<T>
里面只有两个方法,看起来少多了,那么我们能用吗?
public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
这个方法仅供 foreach
使用,本来如果只是如此的话,问题还不是很大,但针对 IEnumerator<T>
有一大堆的 Linq 扩展方法,于是这相当于给此弱引用集合提供了大量可以用来读取状态的方法。
这依然非常危险!
使用者随时可能使用其中一个扩展方法得到了其中一个状态,随后使用另一个扩展方法得知其第二个状态,例如:
// 判断集合中是否存在 IFoo 类型以及是否存在 IBar 类型。
var hasFoo = weakList.OfType<IFoo>().Any();
var hasBar = weakList.OfType<IBar>().Any();
对具有并发开发经验的你来说,以上方法第一眼就能识别出这是不正确的写法。然而类型既然已经开放出去给大家使用了,那么这就非常危险。关键是这不是一个并发场景,于是开发者可能更难感受到在同一个上下文中调用两个方法将得到不确定的结果。对于并发可以使用锁,但对于弱引用,没有可以使用的相关方法来快速解决问题。
因此,IEnumerable<T>
也是不能继承的。
object
看来,我们只能继承自单纯的 object
基类了。此类型没有对托管来说可见的状态,于是谁也不会多次读取状态造成状态不确定了。
因此,我们需要自行实现所有场景下的 API。
弱引用集合我们需要这些使用场景:
此场景下仅仅修改集合而不需要读取任何状态。
既然可以在参数中传入元素,说明此元素一定没有会垃圾回收;因此只要集合中还存在此元素,一定可以确定地移除,不会出现不确定的状态。
一旦满足要求,必须得到完全确定的结果,且在此结果保存的过程中一直生效。
可选考虑下面这些场景:
通常是为了复用某个缓存池的实例。
一定不能实现下面这些方法:
因为判断是否存在通常不是单独的操作,通常会使用此集合继续下一个操作,因此一定不能直接提供。
于是,我们的 API 设计将是这样的:
public class WeakCollection<T> where T : class
{
public void Add(T item) => throw new NotImplementedException();
public bool Remove(T item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public T[] TryGetItems(Func<T, bool> filter) => throw new NotImplementedException();
}
此类型已经以源代码包的形式发布到了 NuGet 上,你可以安装以下 NuGet 包阅读和使用其源代码:
安装后,你可以在你的项目中使用其源代码,并且可以直接使用 Ctrl + 鼠标点击的方式打开类型的源代码,而不需要进行反编译。
.NET 中提供了一些线程安全的类型,如 ConcurrentDictionary<TKey, TValue>
,它们的 API 设计与常规设计差异很大。如果你对此觉得奇怪,那么正好阅读本文。本文介绍为这些非常不确定的行为设计 API 时应该考虑的原则,了解这些原则之后你会体会到为什么会有这些 API 设计上的差异,然后指导你设计新的类型。
像并发集合一样,如 ConcurrentDictionary<TKey, TValue>
、ConcurrentQueue<T>
,其设计为线程安全,于是它的每一个对外公开的方法调用都不会导致其内部状态错误。但是,你在调用其任何一个方法的时候,虽然调用的方法本身能够保证其线程安全,能够保证此方法涉及到的状态是确定的,但是一旦完成此方法的调用,其状态都将再次不确定。你只能依靠其方法的返回值来使用刚刚调用那一刻确定的状态。
我们来看几段代码:
var isRunning = Interlocked.CompareExchange(ref _isRunning, 1, 0);
if (isRunning is 1)
{
// 当前已经在执行队列,因此无需继续执行。
}
private ConcurrentDictionary<string, object> KeyValues { get; }
= new ConcurrentDictionary<string, object>();
object Get(string key)
{
var value = KeyValues.TryGetValue(key, out var v) ? v : null;
return value;
}
这两段代码都使用到了可能涉及线程安全的一些代码。前者使用 Interlocked
做原则操作,而后者使用并发字典。
无论写上面哪一段代码,都面临着问题:
比如前者的 Interlocked.CompareExchange(ref _isRunning, 1, 0)
我们得到一个返回值 isRunning
,然后判断这个返回值。但是我们绝对不能够判断 _isRunning
这个字段,因为这个字段非常易变,在你的任何一个代码上下文中都可能变成你不希望看到的值。Interlocked
是原子操作,所以才确保安全。
而后者,此时访问得到的字典数据,和下一时刻访问得到的字典数据将可能完全不匹配,两次的数据不能通用。
如果你正在为一个易变的状态设计 API,或者说你需要编写的类型带有很强的不确定性(类型状态的变化可能发生在任何一行代码上),那么你需要遵循一些设计原则才能确保安全。
比如要为缓存设计一个获取可用实例的方法,可以使用:
private ConcurrentDictionary<string, object> KeyValues { get; }
= new ConcurrentDictionary<string, object>();
void Get(string key)
{
// CreateCachedInstance 是一个工厂方法,所有 GetOrAdd 的地方都是用此工厂方法创建。
var value = KeyValues.GetOrAdd(key, CreateCachedInstance);
return value;
}
但是绝对不能使用:
if(!KeyValues.TryGetValue(key, out var v))
{
KeyValues.TryAdd(key, CreateCachedInstance(key));
}
这一段代码就是对并发的状态 KeyValues
做了两次访问。
ConcurrentDictionary
也正是考虑到了这种设计场景,于是才提供了 API GetOrAdd
方法。让你在获取对象实例的时候可以通过工厂方法去创建实例。
如果你需要设计这种状态极易变的 API,那么需要针对一些典型的设计场景提供一次调用就能获取此时此刻所有状态的方法。就像上文的 GetOrAdd
一样。
另一个例子,WeakReference<T>
弱引用对象的管理也是在一个方法里面可以获取到一个绝对确定的状态,而避免使用方进行两次判断:
if (weak.TryGetTarget(out var value))
{
// 一旦这里拿到了对象,这个对象一定会存在且可用。
}
一定不能提供两个方法调用来完成这样的事情(比如先判断是否存在再获取对象的实例,就像 .NET Framework 4.0 和早期版本弱引用的 API 设计一样)。
比如以下方法,是试图一个接一个地依次执行 _queue
中的所有任务。
虽然我们使用 Interlocked.CompareExchange
原子操作,但因为后面依然涉及到了多次状态的获取,导致不得不加锁才能确保安全。我们依然使用原则操作是为了避免单纯 lock
带来的性能损耗。
private volatile int _isRunning;
private readonly object _locker = new object();
private readonly ConcurrentQueue<TaskWrapper> _queue = new ConcurrentQueue<TaskWrapper>();
private async void Run()
{
var isRunning = Interlocked.CompareExchange(ref _isRunning, 1, 0);
if (isRunning is 1)
{
lock (_locker)
{
if (_isRunning is 1)
{
// 当前已经在执行队列,因此无需继续执行。
return;
}
}
}
var hasTask = true;
while (hasTask)
{
// 当前还没有任何队列开始执行,因此需要开始执行队列。
while (_queue.TryDequeue(out var wrapper))
{
// 内部已包含异常处理,因此外面可以无需捕获或者清理。
await wrapper.RunAsync().ConfigureAwait(false);
}
lock (_locker)
{
hasTask = _queue.TryPeek(out _);
if (!hasTask)
{
_isRunning = 0;
}
}
}
}
这段代码的完全解读:
Run
方法的时候,先判断当前是否已经在跑其他的任务:
isRunning
为 0
表示当前一定没有在跑其他任务,我们使用原则操作立刻将其修改为 1
;isRunning
为 1
表示当前不确定是否在跑其他任务;isRunning
为 1
的时候状态不确定,于是我们加锁来判断其是否真的有任务在跑:
lock
环境中确认 _isRunning
字段而非变量为 1
则说明真的有任务在跑,此时等待任务完成即可,这里就可以退出了;lock
环境中发现 _isRunning
字段而非变量为 0
则说明实际上是没有任务在跑的(刚刚判断为 1
只是因为这两次判断之间,并发的任务刚刚在结束的过程中),于是需要跟一开始判断为 0
一样,进入到后面的循环中;while
循环第一次是一定能进去的,于是我们暂且不谈;while
内循环中,我们依次检查并发队列 _queue
中是否还有任务要执行,如果有要执行的,就执行:
queue
中的所有任务执行完毕,我们将进入一个 lock
区间:
lock
区间里面我们再次确认任务是否已经完成,如果没有完成,我们靠最外层的 while
循环重新回到内层 while
循环中继续任务;lock
区间里面我们发现任务已经完成了,就设置 _isRunning
为 0
,表示任务真的已经完成,随后退出 while
循环;你可以注意到我们的 lock
是用来确认一开始 isRunning
为 1
时的那个不确定的状态的。因为我们需要多次访问这个状态,所以必须加锁来确认状态是同步的。
在了解了上面的用法指导后,API 设计指导也呼之欲出了:
对于多线程并发导致的不确定性,使用方虽然可以通过 lock
来规避以上第二条问题,但设计方最好在设计之初就避免问题,以便让 API 更好使用。
关于通用 API 设计指导,你可以阅读我的另一篇双语博客:
WPF 框架自己实现了一套触摸机制,但同一窗口只能支持一套触摸机制,于是这会禁用系统的触摸消息(WM_TOUCH
)。这能够很大程度提升 WPF 程序的触摸响应速度,但是很多时候又会产生一些 Bug。
如果你有需要,可以考虑禁用 WPF 的内置的实时触摸(RealTimeStylus)。本文介绍禁用方法,使用 AppSwitch,而不是网上广为流传的反射方法。
在你的应用程序的 app.config 文件中加入 Switch.System.Windows.Input.Stylus.DisableStylusAndTouchSupport=true
开关,即可关闭 WPF 内置的实时触摸,而改用 Windows 触摸消息(WM_TOUCH
)。
<configuration>
<runtime>
<AppContextSwitchOverrides value="Switch.System.Windows.Input.Stylus.DisableStylusAndTouchSupport=true" />
</runtime>
</configuration>
如果你的解决方案中没有找到 app.config 文件,可以创建一个:
然后,把上面的代码拷贝进去即可。
微软的官方文档也有提到使用放射禁用的方法,但一般不推荐这种调用内部 API 的方式,比较容易在 .NET 的版本更新中出现问题:
参考资料
本文介绍在使用 Visual Studio 2019 或者命令行执行 MSBuild
dotnet build
命令时,决定是否使用 .NET Core SDK 预览版的全局配置文件。
指定是否使用 .NET Core 预览版 SDK 的全局配置文件在:
%LocalAppData%\Microsoft\VisualStudio\16.0_xxxxxxxx\sdk.txt
其中 %LocalAppData%
是 Windows 系统中自带的环境变量,16.0_xxxxxxxx
在不同的 Visual Studio 版本下不同。
比如,我的路径就是 C:\Users\lvyi\AppData\Local\Microsoft\VisualStudio\16.0_0b1a4ea6\sdk.txt
。
这个文件的内容非常简单,只有一行:
UsePreviews=True
你一定觉得奇怪,我们在 Visual Studio 2019 中设置了使用 .NET Core SDK 预览版之后,这个配置是全局生效的,即便在命令行中运行 MSBuild
或者 dotnet build
也是会因此而使用预览版或者正式版的。但是这个路径明显看起来是 Visual Studio 的私有路径。
虽然这很诡异,但确实如此,不信,可以看我是如何确认这个文件就是 .NET Core SDK 预览版的全局配置的:
另外,如果你想知道如何在 Visual Studio 2019 中指定使用 .NET Core SDK 的预览版,可以参考我的另外一篇博客:
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.
做库的时候,需要一定程度上保持 API 的兼容性
首先打开你的库项目,或者如果你希望从零开始也可以直接新建一个项目。这里为了博客阅读的简单,我创建一个全新的项目来演示。
然后,为主要的库项目安装 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>
在你的项目内创建两个文件:
这就是两个普通的文本文件。创建纯文本文件的方法是在项目上右键 -> 添加
-> 新建项...
,然后在打开的模板中选择 文本文件
,使用上面指定的名称即可(要创建两个)。
然后,编辑项目文件,我们需要将这两个文件加入到项目中来。
如果你看不到上图中的“编辑项目文件”选项,则需要升级项目文件到 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>
如果你把这两个文件放到了其他的路径,那么上面也需要改成对应的路径。
这时,这两个文件内容还是空的。
这个时候,你会看到库中的 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
现在,我们将 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>
前面我们都是在 PublicAPI.Unshipped.txt
文件中追踪 API。但是如果我们的库需要发布一个版本的时候,我们就需要跟上一个版本比较 API 的差异。
上一个发布版本的 API 就记录在 PublicAPI.Shipped.txt
文件中,这两个文件的差异即是这两个版本的 API 差异。在一个新的版本发布后,就需要将 API 归档到 PublicAPI.Shipped.txt
文件中。
参考资料
你是否好奇 Visual Studio 2019 中的 .NET Core SDK 预览版开关是全局生效的,那个全局的配置在哪里呢?
本文将和你一起探索找到这个全局的配置文件。
Process Monitor 是微软极品工具箱的一部分,你可以在此页面下载:
当你一开始打开 Process Monitor 的时候,列表中会立刻刷出大量的进程的操作记录。这么多的记录会让我们找到目标进程操作的文件有些吃力,于是我们需要设置规则。
Process Monitor 的工具栏按钮并不多,而且我们这一次的目标只会用到其中的两个:
在工具栏上点击“设置过滤器”,然后,添加我们感兴趣的两个进程名称:
devenv.exe
MSBuild.exe
前者是 Visual Studio 的进程名,后者是 MSBuild.exe 的进程名。我们使用这两个进程名称分别找到 Visual Studio 2019 是如何设置全局 .NET Core 预览配置的,并且在命令行中运行 MSBuild.exe 来验证确实是这个全局配置。
然后排除除了文件意外的所有事件类型,最终是如下过滤器:
现在,我们打开 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 自己的设置配置。
于是必须通过其他途径来确认这是否就是真实的全局配置。
现在,我们清除一下 Process Monitor 中的已经记录的数据,然后,我们在命令行中对一个项目敲下 msbuild
命令。
> msbuild
然后在 Process Monitor 里面观察事件。这次发现事件相当多,于是换个方式。
因为我们主要是验证 sdk.txt
文件,但同时希望看看是否还有其他文件。于是我们将 sdk.txt
文件相关的事件高亮。
点击 Filter
-> Highlight...
,然后选择 Path
contains
sdk.txt
时则 Include
。
这时,再看捕获到的事件,可以发现编译期间确实读取了这个文件。
此举虽不能成为此文件是全局配置的铁证,但至少说明这个文件与全局配置非常相关。
另外,继续在记录中翻找,还可以发现与此配置可能相关的两个 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;
}
我这里使用 Visual Studio 2019 能好好编译的一个项目,发现在另一个小伙伴那里却编译不通过,是在 NuGet 还原那里报告了错误:
调用的目标发生了异常。Error parsing the nested project section in solution file.
本文介绍如何解决这样的问题。
此问题的原因可能有多种:
Project
和 EndProject
不成对,导致某个项目没有被识别出来Project
部分发现对应的项目Project
和 EndProject
不成对Project
和 EndProject
不成对通常是合并分支时,自动解冲突解错了导致的,例如像下面这样:
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
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
可能是 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。
参考资料
使用 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:
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin
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
的前面,出现了编译错误。
参考资料
一般情况下我们并不需要关心 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 文件:
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("{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 来对项目做一些编译上的解决方案级别的配置。
Project
和 EndProject
的内部还可以放 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),Debug
和 Release
,平台都是 Any CPU
。同时也为每个项目指定了单独的配置种类,可供选择,每一行都是 项目的配置 = 解决方案的配置
表示此项目的此种配置在解决方案的某个全局配置之下。
如果我们将这两个项目放到文件夹中,那么我们可以额外看到一个新的全局配置 NestedProjects
字面意思是说 {DC0B1D44-5DF4-4590-BBFE-072183677A78}
和 {98FF9756-B95A-4FDB-9858-5106F486FBF3}
两个项目在 {20B61509-640C-492B-8B33-FB472CCF1391}
项目中嵌套,实际意义代表 Walterlv.Demo
和 Walterlv.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
当 A 项目引用 B 项目,那么使用 Visual Studio 或者 MSBuild 编译 A 项目之前就会确保 B 项目已经编译完毕。通常我们指定这种引用是因为 A 项目确实在运行期间需要 B 项目生成的程序集。
但是,现在 B 项目可能仅仅只是一个工具项目,或者说 A 项目编译之后的程序集并不需要 B,仅仅只是将 B 打到一个包中,那么我们其实需要的仅仅是 B 项目先编译而已。
本文介绍如何影响项目的编译顺序,而不带来项目实际引用。
依然在项目中使用往常习惯的方法设置项目引用:
但是,在项目引用设置完成之后,需要打开项目的项目文件(.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>