ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WinForms 两个模板怎么选:Framework 与 .NET 8 差异

WinForms 两个模板怎么选:Framework 与 .NET 8 差异 在“创建新项目”的搜索框里敲下“Windows 窗体”几个字列表里通常会跳出两条长得几乎一模一样的条目一条叫“Windows 窗体应用 (.NET Framework)”另一条就叫“Windows 窗体应用”。很多刚上手 Visual Studio 的人在这里随手点一个然后就拿到了一份完全看不懂的工程有人发现自己新建的项目里莫名其妙多了一个 Properties 文件夹和一堆 AssemblyInfo.cs有人发现 csproj 文件短得只剩几行、连一个Compile Include都找不到还有人把老项目里的ConfigurationManager.AppSettings拷过来直接报红。这两个模板的差别不是“新旧皮肤”那种无关痛痒的差别它决定的是目标机器上要装什么运行时、发布出来的东西长什么样、能用哪些第三方控件、设计器什么时候会罢工。我做过不少“接手别人半路留下来的 WinForms 项目”的活也帮人做过从 .NET Framework 到 .NET 8 的迁移评估这篇就把这两个模板从工程文件、运行时模型、编码写法、设计器行为到选型决策完整拆一遍新手可以当成选型指南老手可以直接拿去对照排错。1. 名字只差一个括号工程结构差一个世代1.1 在新建项目对话框里怎么一眼把它们分开最可靠的辨认方式就是看后缀。带.NET Framework后缀的那一条是继承自 .NET Framework 4.x 时代的老模板不带后缀的那一条走的是 .NET也就是大家习惯叫的 .NET Core 之后那一套技术栈。除了名字还有几个地方可以交叉确认语言下拉框里两个模板都提供 C# 和 Visual Basic但在“框架”下拉框里带后缀的那条只会让你在4.6.2 到 4.8.1之间挑不带后缀的那条则是.NET 6 / 7 / 8 / 9这样的选项。列表右侧的标签也不一样老模板一般挂着 Windows 和 .NET Framework新模板挂的是 Windows、Desktop 和 .NET。还有一个容易踩的细节控件库模板也是成对出现的“Windows 窗体控件库”和“Windows 窗体控件库 (.NET Framework)”。这两个的差异和主模板完全一致所以选控件库的时候同样要看清后缀别一边用着新模板建主程序、一边用老模板建控件库混在一起引用会带来框架兼容性上的麻烦。另外顺手把一件事说清楚这里的 VS 指的是 Visual Studio不是 VS Code。VS Code 里没有 WinForms 的项目模板也没有可视化窗体设计器装了 C# 扩展之后你能做的只有编辑代码、跑dotnet build和dotnet run。用它来写 WinForms 不是不行但拖控件、调属性、看设计器预览这些事它做不了初学者很容易在这里绕冤枉路。1.2 模板生成出来的工程骨架长什么样新建完对比一下目录树差异一目了然。老模板生成的工程大概是这个结构MyApp/ ├─ MyApp.sln └─ MyApp/ ├─ Program.cs ├─ Form1.cs ├─ Form1.Designer.cs ├─ Form1.resx ├─ App.config ├─ packages.config 用了旧式 NuGet 才会有 ├─ Properties/ │ ├─ AssemblyInfo.cs │ ├─ Resources.resx │ ├─ Resources.Designer.cs │ ├─ Settings.settings │ └─ Settings.Designer.cs └─ MyApp.csproj新模板生成的工程则干净得多MyApp/ ├─ MyApp.sln └─ MyApp/ ├─ Program.cs ├─ Form1.cs ├─ Form1.Designer.cs ├─ Form1.resx └─ MyApp.csproj新工程里没有 Properties 文件夹没有 AssemblyInfo.cs没有 App.config也没有 Settings.settings。这不是模板偷懒而是这些东西在新工程系统里被“隐式生成”或者“用别的方式替代”了程序集版本、公司名、版权这些信息现在直接写在 csproj 的属性里编译时由 SDK 自动生成等价于原来 AssemblyInfo.cs 的内容配置数据则推荐走 appsettings.json 加配置类而不是.config文件。注意正因为新工程是按文件名约定自动配对资源文件的Form1.cs/Form1.Designer.cs/Form1.resx这三个文件的名字必须保持一致的前缀。你要是随手把 Designer.cs 重命名成Form1.design.cs或者换个大写设计器很可能就认不出这是同一个窗体的分部类打开之后变成一片空白。1.3 Visual Studio 2022 能选到的老框架版本已经不全了很多十年前的老教程会让你把目标框架设成 .NET Framework 4.0 或者 4.5然后你在 VS 2022 里翻半天也找不到这些选项这不是环境装坏了。VS 2022 只支持以4.6.2 及以上作为编译目标更低的版本要么得回去用旧版 IDE要么就得先升级目标框架。相对的这些老框架版本在客户机上往往还是能跑的因为 .NET Framework 的 4.x 系列是就地升级关系一台机器上装了 4.8它并不是“同时拥有 4.0、4.5、4.8 三套运行时”而是只有一套 4.8早先针对 4.5 编译的程序会被 4.8 的运行时接管运行。这也解释了两个很常见的疑问为什么装了 4.8.1 的机器再去安装 4.8 的离线包安装程序会直接告诉你“这台计算机中已经安装了 .NET Framework 4.8 或版本更高的更新”然后退出以及为什么有些只针对 .NET Framework 3.5 写的祖传程序在新系统上要先按需启用那个老组件才能跑——3.5 和 4.x 不是同一条产品线彼此不覆盖。2. csproj 文件对比SDK 风格与老式工程文件到底差在哪2.1 老式工程文件把每件事都写死了老模板的 csproj 是一个完整的 MSBuild 文件根节点带ToolsVersion和xmlns开头要先导入公共属性然后是一堆PropertyGroup和ItemGroup。它最显著的特征是所有参与编译的文件都要一条条列出来Project ToolsVersion15.0 xmlnshttp://schemas.microsoft.com/developer/msbuild/2003 Import Project$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props ConditionExists($(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props) / PropertyGroup OutputTypeWinExe/OutputType RootNamespaceMyApp/RootNamespace AssemblyNameMyApp/AssemblyName TargetFrameworkVersionv4.8/TargetFrameworkVersion AutoGenerateBindingRedirectstrue/AutoGenerateBindingRedirects /PropertyGroup ItemGroup Compile IncludeForm1.csSubTypeForm/SubType/Compile Compile IncludeForm1.Designer.csDependentUponForm1.cs/DependentUpon/Compile Compile IncludeProgram.cs / Compile IncludeProperties\AssemblyInfo.cs / /ItemGroup /Project这段结构里有几个值得记住的点。SubTypeForm/SubType和DependentUpon元数据是老工程里用来告诉 IDE“这是一个窗体、这个 Designer 文件属于哪个窗体”的缺了它设计器就打不开对应的可视化视图。AutoGenerateBindingRedirects是老 NuGet 时代解决程序集版本冲突的常规操作原因就是 .NET Framework 的程序集大量来自全局程序集缓存GAC多个包依赖同一个程序集的不同版本时会打架。而TargetFrameworkVersion用的是v4.8这种带 v 前缀的写法和后面要讲的新写法完全不是一个格式。还有一个隐藏行为老模板默认的 AnyCPU 可执行文件是带着Prefer32Bittrue的也就是说在 64 位系统上它默认以 32 位进程跑。这个默认值曾经救过无数依赖 32 位原生 DLL 的老程序但也是后来迁移时最容易被忘掉的一环。2.2 SDK 风格工程文件为什么可以这么短新模板的 csproj 短得让人怀疑人生Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeWinExe/OutputType TargetFrameworknet8.0-windows/TargetFramework Nullableenable/Nullable ImplicitUsingsenable/ImplicitUsings UseWindowsFormstrue/UseWindowsForms ApplicationHighDpiModePerMonitorV2/ApplicationHighDpiMode /PropertyGroup /Project短短十来行之所以够用是因为 SDK 属性包做了三件事文件通配符自动包含**/*.cs自动进编译列表**/*.resx自动进资源列表不需要一条条写、隐式框架引用UseWindowsFormstrue加上net8.0-windows这个 TFMSDK 就知道自动引用 WinForms 相关程序集不用手工添加引用、属性代替文件程序集元数据写在 csproj 里编译时生成。程序集依赖也不再靠 GAC 和绑定重定向而是靠deps.json描述运行时按需加载本地目录里的 DLL。TFM 的写法是另一个必须记住的细节net8.0-windows里的-windows后缀不是装饰它意味着这个项目只能在 Windows 上运行。WinForms 在 .NET 6 之后依然没有跨平台计划这一点和“.NET 跨平台”这个印象是两回事——跨平台的是 .NET 运行时和类库桌面 UI 框架各有各的平台限制。2.3 属性对照表与“中间态”工程把两边的关键差异列成表迁移时照着改会省很多事关注点老式工程SDK 风格工程目标框架TargetFrameworkVersion v4.8TargetFramework net8.0-windowsUI 框架声明手工引用程序集UseWindowsForms true源码包含方式显式Compile Include通配符自动包含程序集元数据Properties/AssemblyInfo.cscsproj 属性编译时生成配置文件App.config 编译成 xxx.exe.config无常规配置文件推荐 appsettings.json包管理packages.config 或 PackageReferencePackageReference绑定重定向需要AutoGenerateBindingRedirects不需要AnyCPU 行为默认 32 位Prefer32Bit默认 64 位输出主程序MyApp.exe 里就是你的 IL代码在 MyApp.dllexe 只是启动器表里最后一行是新手最容易懵的地方后面第 3 节会展开。这里还有一个很多人不知道的中间态做法你可以用 SDK 风格的工程文件去编译 .NET Framework 4.8 的 WinForms 项目也就是把TargetFramework写成net48同时保留UseWindowsFormstrue。这样做的价值在于先把工程系统现代化文件通配、PackageReference、属性化元数据但运行时仍然是 4.8客户机不用装任何新东西回归测试的压力也小。等这一步稳定了再改 TFM 去试新运行时出问题的时候能快速判断是“工程系统改动”引起的还是“运行时替换”引起的。这个分两步走的方法比一次性全改完再去大海捞针定位问题要轻松得多。3. 运行时与部署模型客户机上到底装了什么3.1 “装了 4.8”和“装了 .NET 8”是两码事.NET Framework 从 4.x 开始就是系统级组件随机安装、就地升级、全机器共享程序集装在 Windows 目录下一部分在 GAC 里。对开发者来说这意味着部署极其省心Win10 1903 之后的版本和 Win11 都自带 4.8 或 4.8.1你编出来的 exe 拷过去就能双击运行这也是大量企业内部工具至今还留在 .NET Framework 上的最直接原因。想确认客户机上的版本最快的办法是查注册表。PowerShell 一行就够(Get-ItemProperty HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full).Release常见的 Release 值对照大致是461808对应 4.7.2528040对应 4.8533320对应 4.8.1。如果键不存在那这台机器根本没装 4.x 运行时程序双击会直接弹“需要安装 .NET Framework”的对话框。.NET现代那条线走的是完全不同的路子并排安装、按版本共存。不同大版本安装在C:\Program Files\dotnet\shared\下面各占一个目录互不干扰各自独立更新和卸载。好处是升级不用怕动到别人的程序代价就是客户机上不一定有你得在部署时把它装上或者干脆把运行时打包进发布目录。开发机上检查运行时和 SDK 的命令是dotnet --list-runtimes dotnet --list-sdks输出的清单里桌面应用真正依赖的是Microsoft.WindowsDesktop.App这一行。这一点在打包机器上做 CI 的时候特别重要只装了 SDK 没装对应桌面运行时的容器里dotnet publish可能能过但dotnet run会直接报找不到框架。3.2 输出目录里的文件清单说明了一切打开两个项目的bin\Debug\目录对比一下很多问题不用解释就明白了。老项目输出大致是MyApp.exe MyApp.exe.config MyApp.pdb新项目输出大致是MyApp.exe apphost 启动器通常只有一百多 KB MyApp.dll 你的真实代码在这里 MyApp.deps.json 依赖清单 MyApp.runtimeconfig.json运行时配置TFM、是否自包含、回滚策略 MyApp.pdb我见过不止一个人在新项目发布之后对着几十 KB 的 exe 发愁以为发布失败或者代码被清了。事实是 exe 只是个壳负责找到并加载对应版本的运行时然后调用 dll 里的入口点。另外runtimeconfig.json里还藏着滚动升级策略如果你需要锁定某个具体的补丁版本可以在 csproj 里通过RollForward之类的属性控制这在交付给客户现场、对方 IT 部门会不定期更新运行时的场景里很有用。还有一个容易被忽略的差别老项目里如果把某个框架程序集的“复制到本地”设成 truebin 目录会瞬间膨胀出一堆System.*.dll共用同一个目录的两个程序还可能互相覆盖新项目默认所有框架程序集都由共享运行时提供bin 目录里基本只有你自己的东西和第三方包干净很多。3.3 单文件、自包含和体积取舍在部署方式上两个模板的能力差异非常实际。老项目这边可选择的主要是 ClickOnce 或者自己写安装包运行时依赖系统提供新项目除了 ClickOnce还多了两条路框架依赖发布体积小客户机必须有对应运行时和自包含发布体积大但什么都不用装。# 框架依赖 单文件体积小客户机要装 .NET 8 桌面运行时 dotnet publish -c Release -r win-x64 -p:PublishSingleFiletrue --self-contained false # 自包含 单文件客户机零依赖发布目录通常几十到上百 MB dotnet publish -c Release -r win-x64 -p:PublishSingleFiletrue --self-contained true这里有个坑要提前说WinForms 项目不要开裁剪PublishTrimmed。WinForms 大量依赖反射和设计器生成的代码裁剪器看不到这些动态引用很容易把运行时需要的东西剪掉程序在开发机上好好的到客户机上一点按钮就崩报的还是让人摸不着头脑的类型加载异常。如果嫌启动慢用 ReadyToRun 更稳妥它只是预编译成机器码不会改变程序集内容。4. 写代码时真正会撞上的差异4.1 入口代码的写法已经完全不一样了老模板生成的Program.cs是每个 WinForms 开发者都背下来的那几行using System; using System.Windows.Forms; namespace MyApp { static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }新模板生成的入口短了一截而且多了一个看起来“不存在”的方法namespace MyApp; static class Program { [STAThread] static void Main() { ApplicationConfiguration.Initialize(); Application.Run(new Form1()); } }ApplicationConfiguration.Initialize()既不是框架方法也不在你的源码里它是WinForms 源生成器根据 csproj 里的属性自动生成的一个静态类。你可以在obj\Debug\net8.0-windows\下面找到生成出来的文件内容其实就是把原来的三行调用包了一层并且把高 DPI 模式也从属性里读了出来。知道这一点很关键当你想改高 DPI 行为、默认字体、是否启用视觉样式的时候正确做法是改 csproj 属性而不是像以前那样手写Application.SetHighDpiMode(...)——手写虽然也能生效但会和生成代码打架顺序问题会导致设置被覆盖。顺带说新模板默认打开了可空引用类型和隐式 using所以你会发现using System;这类语句凭空消失了string和string?也开始被编译器严格区分。对老项目迁移来说可空引用类型要么一次开、一次改完要么就明确关掉写到一半开开关关警告会多到没法看。4.2 配置和设置System.Configuration 不再白送老项目里读配置最顺手的是ConfigurationManager.AppSettings[key]System.Configuration是框架自带的。到了新项目这个命名空间默认不在引用列表里你得单独装System.Configuration.ConfigurationManager这个 NuGet 包才能用而且它读的还是 app.config 风格的文件和我们平时说的“新式配置”是两套东西。真正推荐的做法是用配置绑定// 需要 Microsoft.Extensions.Configuration.Json 等包 var config new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile(appsettings.json, optional: true, reloadOnChange: true) .Build(); var db config.GetSection(Database).GetDatabaseOptions();Properties/Settings.settings这个设计器支持的东西也值得单独提一句。它在老项目里是靠ApplicationSettingsBase加生成代码实现的新项目早期版本里这个设计器不可用后来逐步补齐了但底层依赖的仍然是System.Configuration.ConfigurationManager包行为和老框架内置的那套并不完全等价尤其在部署路径、用户级设置存储位置上容易出意外。我个人的做法是新项目里不要再用.settings这套东西配置统一走 JSON 加一个强类型配置类用户级偏好存到%AppData%下的自定义文件逻辑清晰、可控、好排查。4.3 语言特性和运行时 API 的边界很多人以为换成新模板就什么新语法都能用了方向对但不够准确C# 语言版本主要由编译器决定但有一部分特性需要运行时配合这部分在老框架上就是不可用的。整理一下实际会遇到的边界SpanT、MemoryT、Index、Rangearr[1..3]这种写法在老框架上需要额外装System.Memory之类的兼容包勉强能用但会带来包依赖IAsyncEnumerableT异步流在老框架上需要Microsoft.Bcl.AsyncInterfaces记录类型record和init访问器需要IsExternalInit垫片网上有现成写法属于能凑合默认接口实现DIM需要运行时支持老框架上基本没有可行方案这是硬边界。反过来说性能相关的 API 差距也不小。HTTP 客户端、字符串处理、JSON 序列化、异步 I/O 这些在新运行时上都有明显的性能改进和内存占用优化尤其是高 DPI 下的 WinForms 渲染新运行时的绘制路径优化是能肉眼感觉出来的。如果你的程序是数据量大、界面刷新频繁的类型这个差距值得计入选型考虑。4.4 平台目标32 位依赖是最硬的一道墙这是最容易被忽视、也最容易在迁移时翻车的一条。老项目默认 AnyCPU 加 Prefer32Bit在 64 位系统上以 32 位运行新项目没有 Prefer32Bit 这个概念AnyCPU 就是 64 位。如果你的程序要调用某台仪器、某台打印机、某个读卡器厂商提供的 32 位原生 DLL或者引用了只能在 32 位下注册的 COM 组件、ActiveX 控件迁移后就会在运行时抛BadImageFormatException报错信息通常只说试图加载格式不正确的程序不会告诉你真正的原因是位数不匹配。解决方式是把平台目标显式锁到 x86PropertyGroup TargetFrameworknet8.0-windows/TargetFramework UseWindowsFormstrue/UseWindowsForms PlatformTargetx86/PlatformTarget /PropertyGroup发布的时候也要带上对应的 RID比如-r win-x86。要注意锁定 x86 之后新运行时的部分内存和性能优势会打折扣同时新的进程外设计器对 32 位项目的支持也有限制可能影响可视化编辑体验。所以遇到这种依赖先评估有没有 64 位版本的原生库或替代方案实在没有就老实评估是继续留在 .NET Framework老框架对 32 位原生互操作的支持最成熟还是接受新模板加 x86 的各种不便。这个判断没有标准答案取决于那套原生依赖有多难替换。5. 设计器行为差异为什么你的窗体打不开了5.1 进程内设计器和进程外设计器老框架项目的窗体设计器是在 IDE 进程内运行的它加载你编译出来的程序集在同一个进程里渲染设计视图。新项目的设计器是进程外的Visual Studio 会启动一个独立的设计器宿主进程来承载你在任务管理器里能看到它。这个架构差异带来几个实际后果设计器启动慢一点设计器崩溃不会连累整个 IDE但最重要的是——设计器依赖项目能成功编译并加载编译不过、运行时缺失、目标 TFM 对应的运行时没装设计器就是打不开。5.2 设计器打不开的排查顺序我在实际项目里总结出一套固定的排查顺序基本能覆盖八成以上的设计器空白/报错先编译整个项目。很多人一边写着一半的代码一边去开设计器编译不过的时候设计器必然失败这一步能省掉一半的无效排查。确认目标运行时和 SDK 装齐了。新项目的 TFM 是net8.0-windows机器上就必须有对应的 .NET 8 SDK 和桌面运行时。升级 IDE 之后把目标框架改到更新版本忘了装新 SDK也会出现这个现象。老项目这边则要确认对应的 **.NET Framework 开发包Developer Pack**在装只装运行时不具备编译和设计器所需的引用程序集。检查平台目标是否和设计器兼容。项目锁了 x86 或依赖 32 位原生控件时设计器加载控件类型可能失败。清缓存重来。关闭 VS删掉解决方案目录下的.vs隐藏文件夹再清理 IDE 自身的组件模型缓存目录%LocalAppData%\Microsoft\VisualStudio\下对应版本号目录里的ComponentModelCache然后重新打开工程。逐个窗体排除。如果一个项目里有十几个窗体只有一个打不开通常问题就出在那个窗体的构造函数或者某个自定义控件上而不是环境问题。提示如果某个自定义控件在构造函数里访问了数据库、读配置文件或者调用外部服务在进程外设计器里这些操作可能失败表现就是设计器加载失败。正确做法是在构造函数里判断当前是否处于设计时环境。5.3 DesignMode 陷阱与高 DPI 配置的两种写法DesignMode这个属性是个经典坑在构造函数和InitializeComponent执行期间DesignMode往往返回 false于是你以为写对了的判空逻辑根本没生效设计器一加载就抛异常。稳妥的写法是判断许可证使用模式if (LicenseManager.UsageMode LicenseUsageMode.Designtime) return;或者用System.ComponentModel.DesignerProperties.GetIsInDesignMode这类方式。这个坑在老框架和新项目里都存在只是新项目的进程外设计器在某些场景下表现和以前不同所以别指望换个模板就自动好了。高 DPI 的配置写法也是两种模板差异的典型代表。老框架 4.7 之后是在 app.config 里加配置节configuration System.Windows.Forms.ApplicationConfigurationSection add keyDpiAwareness valuePerMonitorV2 / /System.Windows.Forms.ApplicationConfigurationSection /configuration新项目是在 csproj 里写属性由源生成器读出来ApplicationHighDpiModePerMonitorV2/ApplicationHighDpiMode这里有个很容易漏的点新模板生成的ApplicationConfiguration.Initialize()里默认给的是SystemAware也就是不额外声明的话多显示器拖拽时界面缩放会糊。想拿到逐显示器适配的效果必须显式声明PerMonitorV2。声明完之后建议去obj目录里翻一下生成的那个文件亲眼确认最终生成的调用是哪个模式——配置文件里写了不等于运行时真的按这个模式跑了中间被覆盖的情况我遇到过不止一次。6. 到底该选哪个把决策依据摆清楚6.1 一张对照表看清全部差异维度.NET Framework 模板新模板.NET 8 等客户机预装情况Win10 1903/Win11 自带 4.8/4.8.1需要单独安装运行时或自包含发布老系统支持支持到 Windows 7 SP1 等较老系统较新版本已不支持过老的系统运行时部署模型系统级、就地升级、全机器共享并排安装、按版本共存第三方控件兼容老控件库基本都支持看厂商支持矩阵停更的库通常没有32 位原生/COM 依赖最省事默认就是 32 位需显式锁 x86部分控件加载受限单文件/自包含发布不支持支持输出目录exe 里就是你的 IL代码在 dllexe 是启动器语言特性支持部分需要垫片部分不可用全量可用长期维护4.8.1 是最后版本只做安全修复每年发新版有 LTS 轨道设计器成熟度最稳进程内进程外近几年已经很顺手个别控件有坑6.2 应该继续留在 .NET Framework 的真实场景我不想给新项目必须上新运行时这种一刀切的建议因为以下几种情况留在老框架上是完全理性的选择第一客户现场完全不可控。给制造业车间、医院科室、政务窗口这类环境做工具机器上装什么不由你决定IT 部门的软件分发流程又长又慢能利用系统自带运行时是巨大的优势这时候新建项目直接选 .NET Framework 4.8交付成本最低。第二依赖停更的控件库或 32 位原生 SDK。老版本的报表控件、图表控件、条形码控件如果厂商早就不维护了强行上新运行时的成本可能比重新写一个功能模块还高。第三需要和老系统长期共存。企业内部往往有一整套基于 .NET Framework 构建的公共库和基础设施新项目插进去走完全不同的包管理和运行时模型会在引用关系和版本冲突上持续产生摩擦收益不明显而维护成本明显上升。6.3 真要迁移的话按这个顺序走如果评估下来值得迁移我建议按下面的顺序推进每一步都留有退路先把业务逻辑和 UI 解耦。把计算、数据访问、协议解析这些抽到一个独立的类库目标框架选netstandard2.0这样它同时能被老框架和新运行时引用。这一步做完后面切换 UI 层的时候基本不用重写业务代码。把老工程换成 SDK 风格但保留 net48。文件通配、PackageReference、属性化元数据都在这一步完成运行时不变测试压力最小。装上升级辅助工具跑一遍扫描。它会标出不兼容的 API比如AppDomain相关调用、远程处理、部分配置 API、老式加密实现等生成一份清单。重点看这份清单里有多少是你真正在用的。逐个第三方依赖确认新版本。控件库、数据库驱动、日志库、序列化库挨个查支持矩阵。这一步最容易卡住也是决定迁移可行性的关键。把 TFM 换到 net8.0-windows编译修错回归测试。别指望一次过重点测试文件路径、编码、并发、打印和报表导出这些老框架和新运行时行为有细微差别的地方。顺便说一句关于把 WinForms 工程挪到 Linux 上编译的想法。新运行时的 SDK 本身是跨平台的理论上可以用EnableWindowsTargeting属性在 Linux 上编译net8.0-windows的项目适合放在 CI 里做编译验证。但这只解决了编译WinForms 程序在 Linux 上没有运行时支持跑不起来设计器也没有。真想让业务逻辑跨平台出路是把逻辑层抽成普通的类库去跨平台而不是指望整个窗体程序跨过去。7. 那些实际踩过之后才记住的坑7.1 找不到类型或命名空间从老项目往新项目拷代码大概率第一批报错就是ConfigurationManager、Registry、ManagementObjectSearcher找不到。前两个是命名空间没被隐式引用装对应的 NuGet 包或者在文件顶部补 using 就行。这里要注意System.Management这类包在新运行时的行为和老框架并不完全一致涉及硬件信息读取、打印机枚举这类功能的代码迁移后务必在真实环境里验证一遍不要只在开发机上跑通就发布。7.2 引用路径写死的后果老项目如果还在用 packages.config 管理包csproj 里的引用会带HintPath写的是..\packages\Xxx.1.0.0\lib\net45\Xxx.dll这种相对路径。把工程发给同事、或者换个目录层级引用就找不到编译报错但看不出所以然。解决办法就是在换 SDK 风格工程的同时切到 PackageReference让包还原走统一的位置这个改动本身风险很低收益却很高。7.3 其余零散但高频的坑现象真正原因处理方式新项目发布后 exe 只有一百多 KBexe 是启动器代码在 dll 里发布整个目录别只拷 exe运行时抛 BadImageFormatException位数不匹配缺少 Prefer32Bit 的行为显式设置 PlatformTarget 为 x86设计器打开一片空白项目编译失败或目标运行时未安装先编译再确认运行时和开发包配置文件读不到新项目里 App.config 不再是默认机制改用 appsettings.json 加配置绑定高 DPI 下界面仍然模糊生成代码里默认是 SystemAware设置 ApplicationHighDpiMode 为 PerMonitorV2程序到客户机上启动就崩缺少对应版本的桌面运行时改为自包含发布或让客户机装运行时界面里某些老控件在设计器里放不进去进程外设计器不支持该类控件改在代码里创建或换等价控件最后分享一个我自己的习惯在项目根目录放一个README或者一个简单的说明文件写清楚这个工程用的是哪个模板、目标框架是什么、客户机上需要预装什么、发布命令是哪一条。看着不起眼但等半年后你或者同事再打开这个项目或者要交接给别人的时候这几行字能省下一整天的猜测。另外一个很实用的动作是在 CI 脚本里加一步检查目标框架和已安装运行时的匹配关系把本机跑得好好的、到客户机就崩这类问题尽量拦在发布之前而不是等售后电话打过来才发现。
返回列表