在 .NET Framework 时代里面,有一组有趣的概念,那就是 SDK 和 Runtime 这两个概念。开发模式十分有趣,在开发者设备上,可以指定 .NET Framework 的 SDK 版本,例如指定 .NET Framework 4.5 版本。开发完成之后,分发给到用户,用户的电脑上所安装的 .NET Framework 基本都是 Runtime 版本。应用程序要求运行的 Runtime 版本一定要大于等于 SDK 的指定版本号

这就有一个非常有趣的问题了,我开发环境使用的 SDK 是低版本,例如 .NET Framework 4.5 版本。但用户的电脑上所安装的 .NET Framework 的 Runtime 版本是高版本,例如是 .NET Framework 4.7 版本,中间距离过了几年的版本。那行为如何保证相同?如果行为不能保证相同,那将会出现行为差异,在我的开发设备上跑得好好的,在一些用户电脑上,将会因为运行时基础库的版本差异从而变更行为,应用程序就不能符合预期跑起来了。事实上,咱没有碰到过这个问题,这是因为在 .NET Framework 层做了很多兼容处理逻辑,其中就包括本文要和大家聊的 WPF 框架的兼容行为

本文所聊的仅仅只是 .NET Framework 的老故事,本文对 .NET Core 3.0 及以上版本基本无效,因为其开发模式变更了

在 .NET Framework 的开发模式里面,开发者采用的是 .NET Framework SDK 进行开发,可以指定版本号。指定版本号后,生成的应用程序要求在运行的设备上,一定要安装大于或等于指定版本的运行时才可以

在 .NET Framework 下,无法让 .NET Framework 的运行时跟随应用进行分发,应用只能采用系统里安装的版本。存在的问题就是应用在开发阶段是无法预知实际运行在用户端,用户的设备上所安装的 .NET Framework 版本的

如果对于相同的一套代码,在不同的 .NET Framework 版本下,有行为上的差异,那这是一个非常可怕的事情,甚至开发端啥都做不了。想想如以下的例子,假如我是 .NET Framework 版本的 WPF 框架的开发者,我写了一个叫 Foo 的方法,这个方法我是在 .NET Framework 4.5 定义的,用于支持某个关键逻辑。但是我在 .NET Framework 4.6 版本,我觉得这个方法应该抛出异常,于是就让这个 Foo 方法被调用时一定抛出异常

这个时候,预计开发者将会寄刀片给我。因为开发者将会发现他的应用能在一些用户的设备上,符合预期工作。在一些用户的设备上,一定会炸掉。其原因就是我在高版本上变更了行为。根本原因就是开发者无法独立分发 .NET Framework 导致应用程序的运行受实际的环境影响

当然了,这个问题在 .NET Core 或 .NET 下已经被彻底解决,那就是允许开发者自己独立发布,不依赖系统的 .NET 环境。如此的开发模式,既可以降低基础运行时框架开发者的压力,也能让上层应用业务端的开发者能更好的控制应用运行,减少奇怪的环境问题

作为考古的博客,继续回到主题

在实际的开发里面,几乎没有遇到上面说的由于 .NET Framework 的 Runtime 版本不同,而导致的行为不同从而导致的问题

这是因为在 WPF 里面做了很多的兼容的处理逻辑。例如大家都知道的,在 .NET Framework 4.0 到 .NET Framework 4.5 是存在极大的变动的。在 WPF 里面,为了编写代码方便,就添加了一个叫 FrameworkCompatibilityPreferences 的类型,里面有一个属性如下

    public static class FrameworkCompatibilityPreferences
{
static FrameworkCompatibilityPreferences()
{
#if NETFX && !NETCOREAPP
_targetsDesktop_V4_0 = BinaryCompatibility.AppWasBuiltForFramework == TargetFrameworkId.NetFramework
&& !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5;
#elif NETCOREAPP
// When building for NETCOREAPP, set this to false
// to indicate that quirks should be treated as if they are running on
// .NET 4.5+
_targetsDesktop_V4_0 = false;
#else
_targetsDesktop_V4_0 = false;
#endif
} // CLR's BinaryCompatibility class doesn't expose a convenient way to determine
// if the app targets 4.0 exactly. We use that a lot, so encapsulate it here
static bool _targetsDesktop_V4_0; internal static bool TargetsDesktop_V4_0
{
get { return _targetsDesktop_V4_0; }
}
}

在各处代码里面对此属性的判断,从而加上兼容的逻辑,例如绑定的代码

    public class Binding : BindingBase
{
public PropertyPath Path
{
get { return _ppath; }
set
{
_ppath = value; if (_ppath != null && _ppath.StartsWithStaticProperty)
{
if (_sourceInUse == SourceProperties.None || _sourceInUse == SourceProperties.StaticSource
// 在 .NET Framework 4.5 和以上的版本更改了行为,需要加上兼容逻辑
|| FrameworkCompatibilityPreferences.TargetsDesktop_V4_0)
{
// net 4.5 breaks static bindings - this is for compat
SourceReference = StaticSourceRef;
}
else
throw new InvalidOperationException(SR.Get(SRID.BindingConflict, SourceProperties.StaticSource, _sourceInUse));
}
}
}
}

毫无疑问,通过一个属性的方式确实简单明了。缺点也很明显,代码里面将会充斥版本判断逻辑,这个方式是走不远的。这个也算是兼容的包袱

作为一群对代码有追求的开发者,肯定是要做一些设计。于是大家可以看到 WPF 开源仓库里面,实际上现在主流的还有一层兼容逻辑,那就是功能开关

依托 System.AppContext 提供的组件之间建立松耦合的协定,从而将代码里面原本对版本强依赖的逻辑变更为对功能的支持

这是一个很好的思路,让功能的开关与具体的 .NET Framework 版本关联,让实际的功能逻辑和功能开关关联。如此即可进行逻辑控制且减少在代码里面判断版本的逻辑,也不会在后续随着版本越来越多,维护越来越难

另外,功能开关本身和具体的 .NET Framework 版本只是一个弱相关性而已,仅仅只是在应用程序运行的时候,在应用程序配置里面声明这个应用程序所期望运行的 .NET Framework 版本,再根据此声明使用的版本,决定开关的开启和关闭

例如在 AppContextDefaultValues 里的代码

    internal static partial class AppContextDefaultValues
{
static partial void PopulateDefaultValuesPartial(string platformIdentifier, string profile, int targetFrameworkVersion)
{
switch (platformIdentifier)
{
case ".NETFramework":
{
if (targetFrameworkVersion <= 40601)
{
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotScaleForDpiChangesSwitchName, true);
} if (targetFrameworkVersion <= 40602)
{
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.OverrideExceptionWithNullReferenceExceptionName, true);
} if (targetFrameworkVersion <= 40702)
{
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotUsePresentationDpiCapabilityTier2OrGreaterSwitchName, true);
} break;
}
case ".NETCoreApp":
{
InitializeNetFxSwitchDefaultsForNetCoreRuntime();
}
break;
}
} private static void InitializeNetFxSwitchDefaultsForNetCoreRuntime()
{
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotScaleForDpiChangesSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.OverrideExceptionWithNullReferenceExceptionName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotUsePresentationDpiCapabilityTier2OrGreaterSwitchName, false); LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DisableStylusAndTouchSupportSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.EnablePointerSupportSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DisableDiagnosticsSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.AllowChangesDuringVisualTreeChangedSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DisableImplicitTouchKeyboardInvocationSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.ShouldRenderEvenWhenNoDisplayDevicesAreAvailableSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.ShouldNotRenderInNonInteractiveWindowStationSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotUsePresentationDpiCapabilityTier3OrGreaterSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.AllowExternalProcessToBlockAccessToTemporaryFilesSwitchName, false);
}
}

当然,功能开关的作用可不只是用来做兼容逻辑,还可以用来实现特殊的配置,从而打开某些功能或者禁用某些功能,更方便开发者进行定制。更多的功能开关请看 WPF Application Compatibility switches list

这就是 WPF 框架对 .NET Framework 运行时版本不同进行的适配逻辑,通过在代码里面添加大量的判断条件,从而让开发者指定的运行版本和实际运行所采用的逻辑尽可能相同。这个兼容逻辑是 WPF 参考里面写代码实现的,也就是靠 WPF 框架的开发者保证的,这也就是本文开头我用了 几乎 而不是确定词来说不会在这里踩坑

dotnet 读 WPF 源代码笔记 WPF 是如何做到一套代码兼容多个 .NET Framework 版本的更多相关文章

  1. dotnet 读 WPF 源代码笔记 布局时 Arrange 如何影响元素渲染坐标

    大家是否好奇,在 WPF 里面,对 UIElement 重写 OnRender 方法进行渲染的内容,是如何受到上层容器控件的布局而进行坐标偏移.如有两个放入到 StackPanel 的自定义 UIEl ...

  2. dotnet 读 WPF 源代码笔记 渲染收集是如何触发

    在 WPF 里面,渲染可以从架构上划分为两层.上层是 WPF 框架的 OnRender 之类的函数,作用是收集应用程序渲染的命令.上层将收集到的应用程序绘制渲染的命令传给下层,下层是 WPF 的 GF ...

  3. 【WPF学习笔记】之如何通过后台C#代码添加(增/删/改按钮)实现对SQLServer数据库数据的更改

    首先,需要连接SQLServer数据库的服务器名称server.数据库名database.数据库用户名uid以及密码pwd,如下图: 然后需要以下数据库SQL代码段,还有一个myHelper.cs代码 ...

  4. 《深入浅出WPF》笔记——绘画与动画

    <深入浅出WPF>笔记——绘画与动画   本篇将记录一下如何在WPF中绘画和设计动画,这方面一直都不是VS的强项,然而它有一套利器Blend:这方面也不是我的优势,幸好我有博客园,能记录一 ...

  5. 《深入浅出WPF》笔记——资源篇

    原文:<深入浅出WPF>笔记--资源篇 前面的记录有的地方已经用到了资源,本文就来详细的记录一下WPF中的资源.我们平时的“资源”一词是指“资财之源”,是创造人类社会财富的源泉.在计算机程 ...

  6. WPF自学笔记

    WPF使用哪几种元素作为顶级元素: 1. Window元素 2. Page元素(与Window元素类似,用于可导航的应用程序) 3. Application元素(定义应用程序资源和启动设置) PS:在 ...

  7. [WPF源代码]QQ空间相册下载工具

    放一个WPF源代码,源代码地址 http://download.csdn.net/detail/witch_soya/6195987 代码没多少技术含量,就是用WPF做的一个QQ空间相册下载工具,效果 ...

  8. WPF学习笔记-用Expression Design制作矢量图然后导出为XAML

    WPF学习笔记-用Expression Design制作矢量图然后导出为XAML 第一次用Windows live writer写东西,感觉不错,哈哈~~ 1.在白纸上完全凭感觉,想象来画图难度很大, ...

  9. WPF 学习笔记-在WPF下创建托盘图标

    原文:WPF 学习笔记-在WPF下创建托盘图标 首先需要在项目中引用System.Windows.Forms,System.Drawing; using System; using System.Co ...

  10. WPF 学习笔记-设置属性使窗口不可改变大小

    原文:WPF 学习笔记-设置属性使窗口不可改变大小 调整Windows下的ResizeMode属性: ResizeMode = NoResize Resize属性是控制Windows是否可以改变大小, ...

随机推荐

  1. IValueConverter的基础用法

    1.我们在做工控项目的时候通常设置配方的上下限 这个时候要求OK数在上下限范围之内,否则NG 首先我们绑定一个简单的List用来展示数据,我这里用学生Age来展示 <ListView Items ...

  2. 记录--通过Promise实现分批处理接口请求

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 如何通过 Promise 实现百条接口请求? 实际项目中遇到需要批量发起上百条接口请求怎么办? 最新案例代码在此!点击看看 前言 不知你项 ...

  3. 记录--一行代码修复100vh bug

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 你知道奇怪的移动视口错误(也称为100vh bug)吗?或者如何以正确的方式创建全屏块? 一.100vh bug 什么是移动视口错误? 你 ...

  4. 计算机网络-Keep Alive

    问题背景 介绍两个经典的网络问题, 问题1: 访问位于Azure Application Gateway之后的nodejs server, 偶尔会触发502 问题2: 请求一个Azure App Se ...

  5. 脚本推荐-verilog自动对齐

    verilog自动对齐脚本 简介 vscode中的verilog-utils可以自动例化 但是没有保留注释 导致注释丢失比较严重 这个为后期的维护带来了巨大的工作量 基于此,使用TCL脚本完成同样的工 ...

  6. Kingbase ES 游标跨事务及其案例一则

    前言 本文介绍游标在PLSQL和SQL层跨事务使用案例,及其案例一则. 本文验证版本: db0=# select version(); db0-# / version ---------------- ...

  7. 线上问题分析之java dump文件生成

    一.查看java进程 jps or ps aux | grep java 二.生成dump文件 jmap -dump:live,format=b,file=xxxx.bin 进程ID 三.查看dump ...

  8. HttpOnly的笔记

    HttpOnly是什么 简单的说: HttpOnly是Cookie的属性: Web服务器通过为Cookie设置这个属性,告诉浏览器应当避免页面端的脚本访问对应Cookie的值,保证被Cookie承载的 ...

  9. 小师妹学JavaIO之:文件读取那些事

    目录 简介 字符和字节 按字符读取的方式 按字节读取的方式 寻找出错的行数 总结 简介 小师妹最新对java IO中的reader和stream产生了一点点困惑,不知道到底该用哪一个才对,怎么读取文件 ...

  10. 大咖齐聚!OpenHarmony技术峰会豪华嘉宾阵容揭晓

      第一届开放原子开源基金会OpenHarmony技术峰会即将来袭 重量级嘉宾和行业大咖高能集结 展示OpenHarmony操作系统技术革新 1场主论坛.6场分论坛干货拉满 2月25日,一起解锁更多精 ...