WPF项目从.Net Framework迁移到.Net6

前言

先说结论

目前还不建议桌面端应用做迁移。

原因如下

  • 迁移起来并不容易。

  • 系统默认没有运行时,用户运行的时候需要安装庞大的运行时,程序自带的检测运行时的功能也不完善。

  • 在制作Innosetup制作安装包的时候检测安装.Net6的运行时也是个问题。

所以还是退回.Net Framework了。

先决条件

.NET 升级助手是一个 .NET 工具,可以使用以下命令进行全局安装:

1
dotnet tool install -g upgrade-assistant

运行

1
upgrade-assistant upgrade .\SchoolClient.sln

目前结论

老项目依赖众多,很多依赖并不支持.net6,因此放弃迁移。

新项目可以考虑使用。

问题处理

打印不显示

打印要替换为

1
System.Diagnostics.Trace.WriteLine("WS:用户上线");

依赖不兼容

自动迁移后的包

我们发现自动迁移后有些包是不可用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ItemGroup>
<PackageReference Include="Aspose.Words" Version="19.10.0" />
<PackageReference Include="ICSharpCode.SharpZipLib.dll" Version="0.85.4.369" />
<PackageReference Include="ImageProcessor" Version="2.9.1" />
<PackageReference Include="Imazen.WebP" Version="10.0.1" />
<PackageReference Include="LiveCharts.Wpf" Version="0.9.7" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="Microsoft.Office.Interop.Word" Version="15.0.4797.1004" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="QRCoder" Version="1.3.9" />
<PackageReference Include="SuperWebSocket" Version="0.9.0.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.113.7" />
<PackageReference Include="WpfAnimatedGif" Version="2.0.0" />
<PackageReference Include="System.ServiceModel.Federation" Version="4.8.1" />
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.336902">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.Compatibility" Version="6.0.0" />
</ItemGroup>

发现很多库不支持.net core

要找对应支持的版本进行替换

更换后的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ItemGroup>
<PackageReference Include="Aspose.Words" Version="19.10.0" />
<PackageReference Include="ImageProcessor.Core" Version="1.1.0" />
<PackageReference Include="LiveCharts.Wpf.Core" Version="0.9.8" />
<PackageReference Include="log4net" Version="2.0.15" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Office.Interop.Word" Version="15.0.4797.1004" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="QRCoder" Version="1.3.9" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.113.7" />
<PackageReference Include="TouchSocket" Version="0.5.2" />
<PackageReference Include="WpfAnimatedGif" Version="2.0.0" />
<PackageReference Include="System.ServiceModel.Federation" Version="4.8.1" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="6.0.0" />
</ItemGroup>

其中有的库要找Core版本的,如

  • ImageProcessor =>ImageProcessor.Core
  • LiveCharts.Wpf => LiveCharts.Wpf.Core`.

有的库就要换成新的了

  • SuperWebSocket => TouchSocket
  • Imazen.WebP => https://github.com/psvmc/Imazen.WebP

预生成事件保持不变

1
2
xcopy /Y /i /e $(ProjectDir)\wwwroot $(TargetDir)\wwwroot
xcopy /Y /d $(ProjectDir)\DLL\libwebp.dll $(TargetDir)

程序不包含适合于入口点

报错

程序不包含适合于入口点的静态 “Main” 方法

解决方式

生成操作选择 应用程序定义

自定义工具输入 MSBuild:Compile

image-20220902180537990

配置文件读写

.net framework的读写方式

以前配置在App.config中的appSettings

之前的读写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/// <summary>
/// 配置文件读取
/// </summary>
/// <param name="key">配置文件中key字符串</param>
/// <returns></returns>
public static string GetConfigValue(string key)
{
try
{
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
//获取AppSettings的节点
AppSettingsSection appsection = (AppSettingsSection)config.GetSection("appSettings");
return appsection.Settings[key].Value;
}
catch
{
return "";
}
}


/// <summary>
/// 配置文件写入
/// </summary>
/// <param name="key">配置文件中key字符串</param>
/// <param name="value">配置文件中value字符串</param>
/// <returns></returns>
public static bool SetConfigValue(string key, string value)
{
try
{
//打开配置文件
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
//获取AppSettings的节点
AppSettingsSection appsection = (AppSettingsSection)config.GetSection("appSettings");
appsection.Settings[key].Value = value;
config.Save();

return true;
}
catch
{
return false;
}
}

迁移后就不能用了,原来的配置文件变成了

相应的读写方法也变了。

.net6读取appsettings.json

Nuget 安装 Microsoft.Extensions.Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using Microsoft.Extensions.Configuration;

using System.Linq;

namespace SchoolClient.Utils
{
public class ZConfigCoreUtil
{
private static IConfiguration _config;

public static void init()
{
_config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
}

/// <summary>
/// 读取指定节点的字符串
/// </summary>
/// <param name="sessions"></param>
/// <returns></returns>
public static string GetVaule(params string[] sessions)
{
if (_config == null)
{
init();
}
try
{
if (sessions.Any())
{
return _config[string.Join(":", sessions)];
}
}
catch
{
return "";
}
return "";
}

public static void test()
{
System.Diagnostics.Trace.WriteLine("IsDebug:" + GetVaule("IsDebug"));
}
}
}

但是只支持读取配置了,不支持写入,所以要把项目运行时读写的字段换一种方式。

其实这也是合理的,我也推荐项目本身的配置和项目运行的配置分开保存,项目的配置只能读取,运行中的配置则可以读写。

下面两种方式任取其一即可。

推荐使用JSON方式。

XML方式读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using System;
using System.Configuration;

namespace SchoolClient.Utils
{
public static class ZConfigUtil
{
///<summary>
///返回app.config文件中appSettings配置节的value项
///</summary>
///<param name="strKey"></param>
///<returns></returns>
public static string GetVaule(string strKey)
{
return GetVaule(strKey, "app.config");
}

public static string GetVaule(string strKey, string filepath)
{
ExeConfigurationFileMap file = new ExeConfigurationFileMap();
file.ExeConfigFilename = filepath;
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(file, ConfigurationUserLevel.None);
foreach (string key in config.AppSettings.Settings.AllKeys)
{
if (key == strKey)
{
return config.AppSettings.Settings[strKey].Value.ToString();
}
}
return null;
}

///<summary>
///在app.config文件中appSettings配置节增加一对键值对
///</summary>
///<param name="newKey"></param>
///<param name="newValue"></param>
public static void SetVaule(string newKey, string newValue)
{
SetVaule(newKey, newValue, "app.config");
}

public static void SetVaule(string newKey, string newValue, string filepath)
{
ExeConfigurationFileMap file = new ExeConfigurationFileMap();
file.ExeConfigFilename = filepath;
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(file, ConfigurationUserLevel.None);
bool exist = false;
foreach (string key in config.AppSettings.Settings.AllKeys)
{
if (key == newKey)
{
exist = true;
}
}
if (exist)
{
config.AppSettings.Settings.Remove(newKey);
}
config.AppSettings.Settings.Add(newKey, newValue);
config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");
}

public static void test()
{
SetVaule("ServerIp", "127.0.0.1");
SetVaule("ServerPort", "8899");
Console.WriteLine("ServerIp:" + GetVaule("ServerIp"));
Console.WriteLine("ServerXXX:" + (GetVaule("ServerXXX") == null));
}
}
}

JSON方式读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
using Newtonsoft.Json;

using System;
using System.IO;

namespace SchoolClient.Utils
{
public class ZConfigJsonUtil
{
private static string readtxt(string filepath)
{
string config_txt = "";
using (FileStream fs = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
using (StreamReader sr = new StreamReader(fs))
{
config_txt = sr.ReadToEnd();
}
}

return config_txt;
}

private static void writetxt(string filepath, string txt)
{
using (FileStream fs = new FileStream(filepath, FileMode.Truncate, FileAccess.ReadWrite))
{
using (StreamWriter sw = new StreamWriter(fs))
{
sw.Write(txt);
}
}
}

private static string getconfigpath()
{
string docpath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string configfolderpath = Path.Combine(docpath, "_XHCLASS");
if (!Directory.Exists(configfolderpath))
{
Directory.CreateDirectory(configfolderpath);
}

string configpath = Path.Combine(configfolderpath, "config.json");
return configpath;
}

/// <summary>
/// 初始化配置
/// </summary>
public static void init()
{
string configpath = getconfigpath();
string txt = readtxt(configpath);
if (string.IsNullOrEmpty(txt))
{
txt = "{}";
ConfigModel jsonobj = JsonConvert.DeserializeObject<ConfigModel>(txt);
string docpath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string configfolderpath = Path.Combine(docpath, "_XHCLASS");
jsonobj.savepath = configfolderpath;
writetxt(configpath, JsonConvert.SerializeObject(jsonobj));
}
}

/// <summary>
/// 获取配置文件
/// </summary>
/// <returns></returns>
public static ConfigModel configGet()
{
string configpath = getconfigpath();
string txt = readtxt(configpath);
if (string.IsNullOrEmpty(txt))
{
txt = "{}";
}
try
{
return JsonConvert.DeserializeObject<ConfigModel>(txt); ;
}
catch (Exception)
{
return new ConfigModel();
}
}

/// <summary>
/// 保存配置文件
/// </summary>
/// <param name="config"></param>
public static void configSet(ConfigModel config)
{
string configpath = getconfigpath();
writetxt(configpath, JsonConvert.SerializeObject(config));
}

/// <summary>
/// 设置临时文件存储位置
/// </summary>
/// <param name="value"></param>
public static void setSavepath(string value)
{
ConfigModel config = configGet();
config.savepath = value;
configSet(config);
}

/// <summary>
/// 获取临时文件存储位置
/// </summary>
/// <returns></returns>
public static string getSavepath()
{
var config = configGet();
if (string.IsNullOrEmpty(config.savepath))
{
string docpath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string configfolderpath = Path.Combine(docpath, "_XHCLASS");
return configfolderpath;
}
else
{
return config.savepath;
}
}
}

public class ConfigModel
{
public string savepath { get; set; }
}
}

输出精简

多运行时精简

输出目录中创建了一个名为runtime的文件夹,里面有很多与平台相关的dll。

解决方法

在csproj文件中的PropertyGroup中,将SelfContained属性设置为false并指定一个RuntimeIdentifier

如下所示:

1
2
3
4
<PropertyGroup>
<SelfContained>false</SelfContained>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
</PropertyGroup>

64位的

1
2
3
4
<PropertyGroup>
<SelfContained>false</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>

多语言精简

我们没有为项目指定语言, 所以会列出多种语言。

解决方法:

在csproj文件中的PropertyGroup中,

1
2
3
<PropertyGroup>
<SatelliteResourceLanguages>zh-Hans</SatelliteResourceLanguages>
</PropertyGroup>