WPF UI 多目标框架(Multi-Targeting)架构决策:从 .NET Framework 4.6.2 到 .NET 10 的兼容性工程实践
2026/9/15 12:04:38 网站建设 项目流程

WPF UI 多目标框架(Multi-Targeting)架构决策:从 .NET Framework 4.6.2 到 .NET 10 的兼容性工程实践

【免费下载链接】wpfuiWPF UI provides the Fluent experience in your known and loved WPF framework. Intuitive design, themes, navigation and new immersive controls. All natively and effortlessly.项目地址: https://gitcode.com/GitHub_Trending/wp/wpfui

WPF UI(Wpf.Ui)作为一套提供 Fluent 设计体验的 WPF 控件库,其架构核心决策之一(ADR-001,状态:Accepted)是同时面向现代 .NET 与遗留 .NET Framework 构建多个目标框架(TFM)。本文以该 ADR 为骨架,结合仓库中Directory.Build.propsDirectory.Packages.propsDirectory.Build.targets及各.csproj的真实配置,完整还原 WPF UI 的多目标化工程方案,帮助读者理解如何在控件库中落地 CPM(中央包管理)、条件编译、PolySharp 源码生成器与统一 C# 语言版本,从而在最大化消费端兼容性的同时保持现代开发体验。

一、决策背景:为什么要同时支持 8 个以上框架

WPF UI 的定位决定了它必须兼容"新旧两代"消费者:既有 .NET Framework 时代遗留的企业级 WPF 应用,也有跑在 .NET 8/9/10 上的现代桌面项目。ADR-001 将支持面划分为三个梯队:

梯队目标框架定位
现代 .NET.NET 10、9、8可调用 Windows 专属 API 的现代实现
遗留框架.NET Framework 4.8.1、4.7.2、4.6.2覆盖存量企业应用
抽象层.NET Standard 2.0、2.1仅用于无 WPF 依赖的抽象库,最大化兼容面

这一决策背后有三条核心动因,在 ADR-001 中均有明确说明:

  1. -windows后缀是硬性要求:面向 .NET 5+ 的 WPF 项目必须使用netX.0-windows形式的 TFM 后缀,这是 .NET 5 引入操作系统特定 TFM 机制后的强制约定;
  2. 遗留兼容不是可选项:大量企业应用仍停留在 .NET Framework,控件库要进入这类项目必须直接产出对应程序集;
  3. 多版本并提供升级通道:同时面向多个 .NET 主版本,让消费者在升级运行时期间无需更换库的版本。

二、三大库的分层目标框架设计

2.1 核心库 Wpf.Ui:WPF 专属,六目标并行

仓库中 src/Wpf.Ui/Wpf.Ui.csproj 的真实配置为:

<TargetFrameworks>net10.0-windows;net9.0-windows;net8.0-windows;net481;net472;net462</TargetFrameworks>

注意三个细节:

  • netX.0-windows后缀:凡是UseWPF=true的项目(Wpf.Ui、Wpf.Ui.Tray、Wpf.Ui.Gallery 等)一律采用带-windows的 TFM,而不是裸net10.0
  • EnableWindowsTargeting=true:允许在非 Windows 的构建主机(如 CI 的 Linux 容器)上编译 Windows 目标,这是多目标工程能在 GitHub Actions 等平台跑通的关键;
  • .NET Framework三兄弟net481;net472;net462依次对应 .NET Framework 4.8.1 / 4.7.2 / 4.6.2。

2.2 抽象层 Wpf.Ui.Abstractions:无 WPF 依赖,兼容面最广

src/Wpf.Ui.Abstractions/Wpf.Ui.Abstractions.csproj 的目标为:

<TargetFrameworks>net10.0;net9.0;net8.0;net481;net472;net462;netstandard2.1;netstandard2.0</TargetFrameworks>

与 ADR 原文相比,当前仓库将net481;net472;net462一并纳入了抽象层目标,并与核心库保持同一组 .NET Framework 目标,体现了"同一 TFM 组内所有项目必须同步"的执行准则(见下文"强制规则")。该层只承载INavigationViewPageProviderINavigationAware等接口与基类,不引用任何 WPF 程序集,因此可以打进 .NET Standard 2.0——这使得它可以在 .NET Framework 与 .NET Core/5+ 之间共享的类库中直接使用。

2.3 DI 集成层 Wpf.Ui.DependencyInjection:同一目标集,低版本依赖

src/Wpf.Ui.DependencyInjection/Wpf.Ui.DependencyInjection.csproj 采用与 Abstractions 完全相同的 TFM 集合,且只依赖一个包:

<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" VersionOverride="3.1.0" />

ADR 原文提到"版本 3.1.0 以获取广泛兼容性",仓库中的落地方式不是Version属性,而是 CPM 体系下的VersionOverride="3.1.0"——这是对该 ADR 决策更精确的实现细节。由于无 WPF 依赖,该层甚至可以被后台服务(background services)等非 UI 上下文复用。

三、Central Package Management:版本唯一真源

ADR 要求"所有包版本只允许出现在Directory.Packages.props"。仓库根目录的 Directory.Packages.props 正是这一决策的执行者:

<Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled> <NuGetAudit>true</NuGetAudit> <NuGetAuditLevel>moderate</NuGetAuditLevel> </PropertyGroup> <ItemGroup> <PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.275" /> <PackageVersion Include="System.Memory" Version="4.6.3" /> <PackageVersion Include="PolySharp" Version="1.15.0" /> <!-- 其余 ~30 个包版本集中在此 --> </ItemGroup> </Project>

使用 CPM 后,各项目引用包时不再写版本号,例如 src/Wpf.Ui/Wpf.Ui.csproj:

<PackageReference Include="Microsoft.Windows.CsWin32"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="System.Memory" />

ADR 文档阐述的三项收益在仓库中得到印证:

  • 单一真源:所有PackageVersion集中在 Directory.Packages.props 一处,升级版本只需改一个文件;
  • 防止版本冲突:整个解决方案(含Wpf.Ui.Gallery及所有测试项目)共享同一版本表;
  • 简化依赖更新:配合CentralPackageTransitivePinningEnabled=falseNuGetAudit(审计级别 moderate),版本治理可以集中管控。

当前仓库的版本号与 ADR 撰写时略有差异(如 CsWin32 由 0.3.242 升至 0.3.275),这正说明 CPM 的价值:版本演进只发生在单一文件内。

四、条件编译:用#if而非运行时判断

4.1 框架检测指令的规范写法

ADR 给出了一套标准模式,仓库源码中随处可见其落地:

#if NET5_0_OR_GREATER // 现代 .NET API(如 Environment.OSVersion) #else // .NET Framework 回退方案(如注册表) #endif #if NET6_0_OR_GREATER // 例如 CancellationTokenRegistration 的 DisposeAsync #endif #if NET8_0_OR_GREATER // 使用 .NET 8+ 的新特性 #endif

实际代码印证:

  • src/Wpf.Ui/Win32/Utilities.cs 用#if NET5_0_OR_GREATER区分现代 .NET 与 .NET Framework 的 API 路径(第 154 行是#if !NET5_0_OR_GREATER的对称回退分支);
  • src/Wpf.Ui/Controls/MessageBox/MessageBox.cs 在#if NET8_0_OR_GREATERusing System.Runtime.CompilerServices;,并分别在 L256、L333、L375-L382 多处按NET8_0_OR_GREATER/NET6_0_OR_GREATER分支实现;
  • src/Wpf.Ui/Controls/NavigationView/NavigationView.Navigation.cs 在NET6_0_OR_GREATER下启用较新的导航 API;
  • 其余项目同样遵守该约定,如 src/Wpf.Ui.Tray/RoutedNotifyIconEvent.cs。

ADR 的核心纪律是:编译期能力差异一律用#if指令处理,绝不用运行时版本检查。因为运行时判断会拖慢路径且无法消除编译器对缺失 API 的报错,而#if能让每个 TFM 的编译在构建期就暴露 API 缺失问题。

4.2 框架专属依赖的条件引用

src/Wpf.Ui/Wpf.Ui.csproj 只对net462引入System.ValueTuple

<ItemGroup Condition="'$(TargetFramework)' == 'net462'"> <PackageReference Include="System.ValueTuple" /> </ItemGroup>

同理,src/Wpf.Ui.Tray/Wpf.Ui.Tray.csproj 反向使用条件——除net462外引入System.Drawing.Common(托盘图标Hicon.cs依赖其生成 HICON,见 src/Wpf.Ui.Tray/Hicon.cs 的 TODO 注释),而对net462只引System.ValueTuple

<ItemGroup Condition="'$(TargetFramework)' != 'net462'"> <PackageReference Include="System.Drawing.Common" /> </ItemGroup> <ItemGroup Condition="'$(TargetFramework)' == 'net462'"> <PackageReference Include="System.ValueTuple" /> </ItemGroup>

这种"按 TFM 条件裁剪依赖"的做法,避免了在 .NET 6+ 上引入不受支持的System.Drawing.Common(该包自 .NET 6 起仅支持 Windows),是多目标工程避免运行时陷阱的典型手法。

五、PolySharp:编译期生成 polyfill,现代语法贯通老框架

5.1 工作原理与仓库配置

PolySharp 是一个构建期源码生成器:它不会引入运行时依赖,而是在编译时按需生成 C# 语言特性所需的 polyfill 类型。ADR 文档中给出的配置示例(CompilerVisibleProperty+PolySharpExcludeGeneratedTypes)在仓库中落地为 src/Wpf.Ui/Wpf.Ui.csproj 的一行属性:

<PolySharpExcludeGeneratedTypes>System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute;System.Diagnostics.CodeAnalysis.UnscopedRefAttribute</PolySharpExcludeGeneratedTypes>

同时在 Directory.Build.targets 中,PolySharp 被按目标框架条件引入——只有旧框架(netstandard2.0netstandard2.1net481net472net462)才需要 polyfill,现代 .NET 无需加载:

<PackageReference Include="PolySharp" Condition="'$(TargetFramework)' == 'net462'"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>build; analyzers</IncludeAssets> </PackageReference> <!-- netstandard2.0 / netstandard2.1 / net472 / net481 同理 -->

5.2 能力清单

ADR 明确 PolySharp 带来的核心收益,这些在现代 .NET 上是 BCL 内置能力、在旧框架上由生成器补齐:

  • init-only 属性IsExternalInit):旧框架上让{ get; init; }可用;
  • required 成员:支持 C# 11 的required关键字;
  • CallerArgumentExpression:让断言/校验库在旧框架也能拿到调用点表达式;
  • C# 11 / C# 12+ 语法在旧框架上的落地:配合下文的语言版本设置,实现"一套语法、处处编译"。

5.3 纪律:禁止手写 polyfill

ADR 强调"绝不为语言特性手写 polyfill 类"。原因在于 PolySharp 的生成器在构建期自动产出符号,若开发者手动声明IsExternalInit等同名类型,会触发重复符号编译错误——机制本身即是防手写的最强校验。

六、统一语言版本:C# 14 与 preview 例外

所有项目共享 Directory.Build.props 中的统一配置:

<LangVersion>14.0</LangVersion>

唯一的例外是核心库 src/Wpf.Ui/Wpf.Ui.csproj:

<LangVersion>preview</LangVersion>

这是 ADR 中明确记录的唯一 override,用于在核心库中启用 C# 预览特性。C# 14 + PolySharp 的组合意味着:编写代码时可以使用最新 C# 语法,PolySharp 在编译期为旧 TFM 生成对应 polyfill,全程零运行时依赖。

使用提示:当 .NET 工具链与LangVersion=14.0不匹配时,需要保证 SDK 版本支持 C# 14(对应 .NET 10 SDK);若仅想稳定编译,可将LangVersion调低,但会失去该仓库对全部 TFM 的统一语法基线。

七、构建全局属性与框架探测机制

7.1 Directory.Build.props:一次定义,处处生效

Directory.Build.props 集中管理所有项目的公共构建属性,除了上文提到的LangVersion,还包括:

<Version>4.3.0</Version> <LangVersion>14.0</LangVersion> <Nullable>enable</Nullable> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <GenerateDocumentationFile>true</GenerateDocumentationFile>

(注意:当前仓库实际版本为4.3.0,ADR 文档示例中的4.2.0是撰写时的快照。)

7.2 条件属性组:如何定义NET8_0_OR_GREATER

ADR 展示了用MSBuild::IsTargetFrameworkCompatible定义自定义常量的模式。仓库中 Directory.Build.props 采用等价的"白名单"实现:

<PropertyGroup> <IsBelowNet8 Condition="'$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'netstandard2.1' Or '$(TargetFramework)' == 'net462' Or '$(TargetFramework)' == 'net472' Or '$(TargetFramework)' == 'net481' Or '$(TargetFramework)' == 'net5.0' ... Or '$(TargetFramework)' == 'net10.0'" >True</IsBelowNet8> </PropertyGroup> <PropertyGroup Condition="'$(IsBelowNet8)' == 'false'"> <DefineConstants>$(DefineConstants);NET8_0_OR_GREATER</DefineConstants> </PropertyGroup>

这套机制的价值在于:仓库在正式 SDK 预定义符号之外,显式注入NET8_0_OR_GREATER常量,让 src/Wpf.Ui/Controls/MessageBox/MessageBox.cs 等文件中#if NET8_0_OR_GREATER的语义完全由中央构建属性驱动,任何新增 TFM 只需维护一处列表。

7.3 Directory.Build.targets:打包、分析与裁剪的统一收口

Directory.Build.targets 补充了打包与分析阶段的能力:

  • 统一打包元数据GenerateLibraryLayoutsnupkg符号包、DeterministicSourcePaths,并在打包时附带 ThirdPartyNotices.txt 与 LICENSE.md;
  • 分析器全家桶Microsoft.CodeAnalysis.BannedApiAnalyzers(配合根目录 BannedSymbols.txt)、AsyncFixerIDisposableAnalyzersStyleCop.Analyzers全部按PrivateAssets=all引入;
  • 裁剪/AOT 分析器:对net6.0及以上的 TFM 开启IsTrimmableEnableTrimAnalyzerEnableAotAnalyzerEnableSingleFileAnalyzer(Directory.Build.targets)——这与 ADR 中"Abstractions 库 AOT 兼容"的目标相呼应;
  • SourceLink 特判WpfSourceLinkWorkaround目标针对UseWPF=true的项目绕过 WPF 与 SourceLink 的已知冲突(Directory.Build.targets)。

八、执行纪律:MUST / MUST NOT 清单与自校验机制

ADR 用强约束条款把多目标工程的可维护性固定下来:

必须遵循(MUST):

  1. 所有包版本只允许写在Directory.Packages.props(CPM 唯一真源);
  2. 有 WPF 依赖的项目必须使用-windows后缀 TFM(如net10.0-windows,而不是net10.0);
  3. 无 WPF/Windows 依赖的抽象包使用netstandard2.0/netstandard2.1
  4. 框架专属代码一律用#if NET{X}_0_OR_GREATER守护,禁用运行时版本检查;
  5. 语言特性 polyfill 一律交给 PolySharp,禁止手写IsExternalInitCallerArgumentExpression等类型;
  6. LangVersionDirectory.Build.props统一设置,仅在有充分理由时在单个.csproj覆盖(当前仅Wpf.Ui.csproj使用preview)。

禁止事项(MUST NOT):

  1. 严禁在.csprojPackageReference上写Version属性(版本只能在Directory.Packages.props)——ManagePackageVersionsCentrally=true会让违规写法直接触发构建错误;
  2. 严禁只给部分项目新增 TFM——同一 TFM 组内所有项目必须保持同步(对照上文,Abstractions 与 DI 层在仓库中确实共享同一 TFM 集合);
  3. 严禁使用带补丁号的#if(如NET8_0_10),只允许_OR_GREATER后缀符号;
  4. 严禁在未进行主版本升级时移除已发布包中的 TFM——移除 TFM 对仍停留在该框架的消费者是破坏性变更。

三层自校验机制:

  • CPM 强制ManagePackageVersionsCentrally=true使任何带Version的引用直接编译失败;
  • TFM 验证:每次dotnet build都会编译全部 TFM,缺失 API 立即以编译错误形式暴露——这是条件编译与多目标配合的天然安全网;
  • PolySharp 覆盖:生成器自动产出 polyfill,手写同类符号会触发重复定义错误。

九、决策后果:收益与代价

正面收益(ADR 原文 + 仓库印证):

  • 广泛兼容:从 .NET Framework 4.6.2 到 .NET 10 的应用都能直接引用,且 Wpf.Ui.Abstractions 通过netstandard2.0覆盖到无法升级运行时的存量类库;
  • 现代开发体验:C# 14(核心库 preview)语法配合 PolySharp 贯通全部 TFM,Directory.Build.props 一处设置全局生效;
  • 维护简化:CPM 把版本治理收敛到 Directory.Packages.props 单文件;
  • 清晰升级路径:消费者升级 .NET 版本时无需更换库版本;
  • AOT/裁剪就绪:Directory.Build.targets 对 .NET 6+ 开启裁剪与 AOT 分析器,抽象层无 WPF 依赖天然适合 AOT 场景。

负面代价:

  • 构建复杂度上升:每次提交都要编译 6+ 个框架变体(以 src/Wpf.Ui/Wpf.Ui.csproj 的六目标为例);
  • 包体积增大:多目标 NuGet 包内含全部框架的程序集;
  • 条件编译负担:框架差异代码必须接受#if指令约束;
  • 测试成本:新特性应尽量在多个框架版本上验证——仓库在 tests 目录同时维护了 Wpf.Ui.UnitTests 与基于 FlaUI 的 Wpf.Ui.Gallery.IntegrationTests,正是对这一负担的工程回应。

十、实践要点速查

在多目标控件库(或任何需要兼容新旧框架的 .NET 库)中复刻这套方案时,按以下顺序落地:

  1. 根目录创建Directory.Packages.props:设ManagePackageVersionsCentrally=true,把所有包版本集中管理;
  2. 根目录创建Directory.Build.props:统一LangVersionNullableAllowUnsafeBlocks,并按 TFM 白名单注入NET8_0_OR_GREATER等自定义常量;
  3. 根目录创建Directory.Build.targets:按旧框架条件引入 PolySharp,对 .NET 6+ 开启裁剪/AOT 分析器;
  4. 按分层设定 TFM:WPF 层用net10.0-windows;...;net462并加EnableWindowsTargeting,抽象层追加netstandard2.1;netstandard2.0
  5. 源码中用#if NET{X}_0_OR_GREATER守护框架差异,依赖用Condition="'$(TargetFramework)' == ..."裁剪;
  6. dotnet build全量验证:所有 TFM 在同一命令内编译,任何 API 缺失都会立即报错——这正是 ADR 选择"构建期验证"而非运行时探测的根本原因。

十一、相关文档延伸

  • ADR 决策原文:docs/architecture/decisions/ADR-001-multi-target-framework.md
  • 架构总览与推荐实践:docs/architecture/README.md、docs/architecture/RECOMMENDATIONS.md
  • 控制库分层架构:docs/architecture/decisions/ADR-002-control-library-architecture.md
  • Win32 互操作选型(依赖 CsWin32 版本管理):docs/architecture/decisions/ADR-003-win32-interop-via-cswin32.md
  • 主题静态管理器(同样受多目标约束):docs/architecture/decisions/ADR-004-static-managers-for-theming.md
  • 核心实现参考:src/Wpf.Ui/Wpf.Ui.csproj、src/Wpf.Ui.Abstractions/Wpf.Ui.Abstractions.csproj、src/Wpf.Ui.DependencyInjection/Wpf.Ui.DependencyInjection.csproj
  • 工程化配置:Directory.Build.props、Directory.Packages.props、Directory.Build.targets

【免费下载链接】wpfuiWPF UI provides the Fluent experience in your known and loved WPF framework. Intuitive design, themes, navigation and new immersive controls. All natively and effortlessly.项目地址: https://gitcode.com/GitHub_Trending/wp/wpfui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询