2019 年 5 月

第 34 卷,第 5 期

[.NET Core 3.0]

使用 .NET Core 3.0 中的 WinForms 创建集中式拉取请求中心

作者:Eric Fleming | 2019 年 5 月

Windows 窗体(或简称 WinForms),多年来被用于开发具有丰富和交互式界面的基于 Windows 的强大应用程序。各类企业对这些桌面应用程序的投入量非常巨大,每月有大约 240 万开发人员使用 Visual Studio 创建桌面式应用。利用和扩展现有 WinForms 代码资产的好处无疑极具吸引力,但还有其他好处。WinForms 拖放式设计器体验使用户能够构建功能齐全的 UI,而不需要任何特殊知识或培训。WinForms 应用程序易于部署和更新,可独立于 Internet 连接工作,并且可以在不向 Internet 公开配置的本地计算机上运行,提高了安全性。一直到最近,WinForms 应用程序都还依然只能使用完整的 .NET Framework 进行构建,但 .NET Core 3.0 预览版的发布改变了这一现状。

.NET Core 的新功能和优点不再局限于 Web 开发。通过 .NET Core 3.0,WinForms 增加了一些功能,比如更易于部署、更高的性能、对 .NET Core 特有的 NuGet 包的支持、.NET Core 命令行接口 (CLI) 等等。本文将介绍使用这些功能的诸多好处、它们的重要性以及如何在 WinForms 应用程序中使用它们。

让我们直接开始构建第一个 .NET Core 3.0 WinForms 应用程序。在本文中,我将构建一个应用程序,用于检索并显示托管在 GitHub 上的其中一个开源 Microsoft 存储库的开放式拉取请求。第一步是安装最新版本的 Visual Studio 2019 和 .NET Core 3.0 SDK,之后便可使用 .NET Core CLI 命令来创建新的 WinForms 应用程序。在添加 .NET Core 支持之前,WinForms 应用程序无法实现这一点。

即将发布的是一个新的 Visual Studio 模板,用于创建针对 .NET Core 3.0 的 WinForms 项目。由于模板目前尚未发布,因此现在让我们通过运行以下命令生成一个名为 PullRequestHub 的新 WinForms 项目:

dotnet new winforms -o PullRequestHub

为了确保项目成功创建,请导航到 dotnet new 命令创建的新目录,使用 CLI 构建并运行项目,如下所示:

cd .\PullRequestHub\

由于可以访问 .NET Core CLI,因此也可以访问要还原、运行和构建的命令。在运行之前,请尝试还原和构建命令,如下所示:

dotnet restore
dotnet build

这些命令的工作方式与在 .NET Core Web 应用程序的命令行中运行时的工作方式相同。请注意,在执行 dotnet run 命令时,它实际上会在执行应用之前执行还原和构建命令 (bit.ly/2UCkEaN)。现在让我们运行项目,通过在命令行输入 dotnet run 对其进行测试。

成功!你刚刚创建了第一个 .NET Core WinForms 应用程序。运行时,将看到屏幕上出现一个带有“Hello .NET Core!”文本的窗体。

在进一步向应用程序添加逻辑之前,让我们花一点时间来讨论 Visual Studio 中 WinForms 设计器视图的当前状态。

设置 .NET Core WinForms 应用的设计器

在 Visual Studio 中打开 CLI 生成的项目时,可能会注意到缺少某些功能。最值得注意的是,目前没有为 .NET Core WinForms 应用程序提供设计器视图。虽然有计划提供此功能,但尚未完成。

幸运的是,有一个解决方案可让你至少在添加本机支持之前访问设计器。现在,可创建包含 UI 文件的 .NET Framework 项目。通过这种方式就可以使用设计器编辑 UI 文件,然后 .NET Core 项目将引用 .NET Framework 项目中的 UI 文件。这使你能够利用 UI 功能,同时仍然在 .NET Core 中构建应用程序。以下是我为项目执行操作的方法。

除了所创建的 PullRequestHub 项目之外,还需要添加一个在 .NET Full-Framework 版本上运行的新 WinForms 项目。将此项目命名为 PullRequestHub.Designer。创建新项目后,从 .NET Core 项目中删除 Form1 文件,只保留 Program.cs 类。

导航到 PullRequestHub.Designer 并将窗体文件重命名为 PullRequestForm。现在将编辑 .NET Core 项目文件,并添加以下代码,将两个项目中的文件关联起来。这还将负责处理将来创建的任何其他窗体或资源:

<ItemGroup>
  <Compile Include=”..\PullRequestHub.Designer\**\*.cs” />
</ItemGroup>

保存项目文件后,将看到 PullRequestForm 文件出现在解决方案资源管理器中,你将能够与它们进行交互。如果要使用 UI 编辑器,需要确保从 .NET Core 项目中关闭 PullRequestForm 文件,并从 .NET Framework 项目中打开 PullRequestForm 文件。更改将在两者中进行,但仅 .NET Framework 项目提供编辑器。

构建应用程序

让我们开始向应用程序中添加一些代码。为了从 GitHub 检索开放式拉取请求,我需要创建一个 HttpClient。这就是 .NET Core 3.0 的用武之地,因为它提供了对新 HttpClientFactory 的访问权限。全框架版本中的 HttpClient 存在一些问题,包括使用 using 语句创建客户端的问题。HttpClient 对象将被释放,但底层套接字在一段时间内不会被释放,默认情况下为 240 秒。如果套接字连接保持打开状态 240 秒,并且系统中的吞吐量很高,则系统可能会使所有空闲套接字达到饱和。发生这种情况时,新请求必须等待套接字释放,这可能对性能产生一些非常严重的影响。

HttpClientFactory 有助于缓解这些问题。首先,它提供了一种在更中心位置预先配置客户端实现的更简单方法。它还为你管理 HttpClients 的生命周期,因此你不会遇到前面提到的问题。我们来了解如何在 WinForms 应用程序中执行此操作。

使用此新功能的最佳和最简单的方法之一是通过依赖项注入。依赖项注入,或更普遍的控制反转,是一种将依赖项传递到类中的技术。它也是减少类耦合和简化单元测试的绝佳方法。例如,你将看到如何在程序启动时创建 IHttpClientFactory 的实例,以便稍后在窗体中使用该对象。在以前的 .NET 版本中,这在 WinForms 中不太容易实现,这就是使用 .NET Core 的另一个优势。

在 Program.cs 中,将创建一个名为 ConfigureServices 的方法。在本方法中,创建新的 ServiceCollection,以便可通过依赖项注入来使用服务。需要先安装最新的这两个 NuGet 包:

  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Http

然后添加图 1 中所示的代码。这将创建一个要在窗体中使用的新 IHttpClientFactory。结果会得到一个客户端,可以显式地使用它来处理涉及 GitHub API 的请求。

图 1 创建新的 IHttpClientFactory

private static void ConfigureServices()
{
  var services = new ServiceCollection();
  services.AddHttpClient();
  services.AddHttpClient(“github”, c =>
  {
    c.BaseAddress = new Uri(“https://api.github.com/”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/vnd.github.v3+json”);
    c.DefaultRequestHeaders.Add(“User-Agent”, “HttpClientFactory-Sample”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/json”);
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
  });
}

接下来,需要将实际的窗体类 PullRequestForm 注册为单一实例。在本方法的末尾处,添加以下行:

services.AddSingleton<PullRequestForm>();

然后,需要创建 ServiceProvider 的实例。在 Program.cs 类的顶部,创建以下属性:

private static IServiceProvider ServiceProvider { get; set; }

现在有了 ServiceProvider 属性,请在 ConfigureServices 方法的末尾处添加一行来构建 ServiceProvider,如下所示:

ServiceProvider = services.BuildServiceProvider();

最后,完整的 ConfigureServices 方法应类似于图 2 ****中的代码。

图 2 ConfigureServices 方法

private static void ConfigureServices()
{
  var services = new ServiceCollection();
  services.AddHttpClient();
  services.AddHttpClient(“github”, c =>
  {
    c.BaseAddress = new Uri(“https://api.github.com/”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/vnd.github.v3+json”);
    c.DefaultRequestHeaders.Add(“User-Agent”, “HttpClientFactory-Sample”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/json”);
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
  });
  services.AddSingleton<PullRequestForm>();
  ServiceProvider = services.BuildServiceProvider();
}

现在,需要在启动时将窗体与容器连接起来。应用程序运行时,这将调用 PullRequestForm 并提供可用的必要服务。将 Main 方法更改为以下代码:

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  ConfigureServices();
  Application.Run((PullRequestForm)ServiceProvider.GetService(typeof(PullRequestForm)));
}

感觉很棒!现在已完成所有连接。在 PullRequestForm 构造函数中,注入刚连接好的 IHttpClientFactory 并将其分配给本地变量,如以下代码所示:

private static HttpClient _httpClient;
public PullRequestForm(IHttpClientFactory httpClientFactory)
{
  InitializeComponent();
  _httpClient = httpClientFactory.CreateClient(“github”);
}

现在有了 HttpClient,可使用它来调用 GitHub 以检索拉取请求、问题等。这也使得后续几个步骤略微棘手。来自 HttpClient 的调用将为异步请求,如果你一直在使用 WinForms,你就知道接下来该怎么做。接下来必须处理线程,并将调度更新发送到 UI 线程。

为了开始检索所有拉取请求,需在视图中添加一个按钮。通过这种方式,可在将来添加更多存储库或更多存储库组进行检查。使用连接的设计器,将按钮拖到窗体上,并将文本重命名为“Microsoft”。 在此过程中,给按钮取一个更具含义的名称,如 RetrieveData_Button。需要绑定到 RetrieveData_Button_Click 事件,但需要使用此代码将其设置为异步:

private async void RetrieveData_Button_Click(object sender, EventArgs e)
{
}

在此处,需要调用检索开放式 GitHub 拉取请求的方法。但首先,由于现在正在处理异步调用,因此必须连接 SynchronizationContext。可通过添加新属性并使用以下代码更新构造函数来完成此操作:

private static HttpClient _httpClient;
private readonly SynchronizationContext synchronizationContext;
public PullRequestForm(IHttpClientFactory httpClientFactory)
{
  InitializeComponent();
  synchronizationContext = SynchronizationContext.Current;
  _httpClient = httpClientFactory.CreateClient(“github”);
}

接下来,创建一个模型并将其命名为 PullRequestData,以便可以轻松地反序列化请求。相关代码如下:

public class PullRequestData
{
  public string Url { get; set; }
  public string Title { get; set; }
}

最后,创建一个名为 GetPullRequestData 方法。在本方法中,将向 GitHub API 发出请求并检索所有开放式拉取请求。将反序列化 JSON 请求,因此请将最新版本的 Newtonsoft.Json 包添加到项目中。代码如下:

private async Task<List<PullRequestData>> GetPullRequestData()
{
  var gitHubResponse =
    await _httpClient.GetStringAsync(
    $”repos/dotnet/winforms/pulls?state=open”);
  var gitHubData =
    JsonConvert.DeserializeObject<List<PullRequestData>>(gitHubResponse);
  return gitHubData;
}

现在可使用 RetrieveData_Button_Click 方法调用此方法。获得所需的数据列表后,为每个标题创建标签列表,以便在窗体上显示。获得标签列表后,可在 UpdateUI 方法中将它们添加到 UI 中。图 3 显示了此步骤。

图 3 从 RetrieveData_Button_Click 进行调用

private async void RetrieveData_Button_Click(object sender, EventArgs e)
{
  var pullRequestData = await GetPullRequestData();
  await Task.Run(() =>
  {
    var labelsToAdd = new List<Label>();
    var verticalSpaceBetweenLabels = 20;
    var horizontalSpaceFromLeft = 10;
    for (int i = 0; i < pullRequestData.Count; i++)
    {
      Label label = new Label();
      label.Text = pullRequestData[i].Title;
      label.Left = horizontalSpaceFromLeft;
      label.Size = new Size(100, 10);
      label.AutoSize = true;
      label.Top = (i * verticalSpaceBetweenLabels);
      labelsToAdd.Add(label);
    }
    UpdateUI(labelsToAdd);
  });
}

然后,UpdateUI 方法将使用 synchronizationContext 更新 UI,如下所示:

public void UpdateUI(List<Label> labels)
{
  synchronizationContext.Post(new SendOrPostCallback(o =>
  {
    foreach (var label in labels)
    {
      Controls.Add(label);
    }
  }), labels);
}

如果运行应用程序并单击 Microsoft 按钮,UI 将和 GitHub 上 dotnet/winforms 存储库中的所有开放式拉取请求的窗体一起更新。

现在轮到你了。正如本文的标题所说,为了使它成为一个真正的集中式拉取请求中心,让我们更新此示例,以便从多个 GitHub 存储库中进行读取。这些存储库不需要来自 Microsoft 团队,尽管观察它们的进程十分有趣。例如,微服务体系结构非常常见,在其中你可能拥有许多组成整个系统的存储库。鉴于一般而言,不长时间将分支和拉取请求单独存储是一个好主意,这样的工具可以提高对开放式拉取请求的见解并提高整个系统的质量。

可设置一个 Web 应用,但又得担心部署、运行、身份验证等问题。使用 . NET Core 中的 WinForms 应用程序,你无需担心任何此类问题。现在让我们来看看使用 .NET Core 构建 WinForms 应用的最大优势之一。

打包应用程序

过去,部署新的或更新的 WinForms 应用程序可能会导致与主机上安装的 .NET Framework 版本相关的问题。通过 .NET Core 则可以独立部署应用并从单个文件夹运行应用,而不依赖于计算机上安装的 .NET Framework 版本。这意味着用户无需安装任何内容;他们可以仅运行应用程序。通过 .NET Core 还可以一次更新和部署一个应用,因为其包版本不会相互影响。

对于本文中的示例应用,需要为其独立打包。请注意,独立应用程序会更大,因为它们包含 .NET Core 库。如果要部署到安装了 .NET Core 最新版本的计算机中,则无需独立部署应用。相反,可通过利用已安装的 .NET Core 版本来减小已部署应用的大小。当不希望应用程序依赖于它将运行的环境时,可使用独立选项。

若要在本地打包应用程序,需要确保在设置中启用了开发人员模式。尝试运行打包项目时,Visual Studio 将进行提示并提供设置的链接,但若要直接启用它,请转到 Windows 设置,按 Windows 徽标键并搜索“设置”。在搜索框中键入“面向开发人员的设置”并选择它。将看到启用开发人员模式的选项。选择并启用此选项。

大多数情况下,如果以前打包过 WinForms 应用程序,那么创建独立包的步骤则看起来比较熟悉。首先,创建一个新的 Windows 应用程序打包项目。将新项目命名为 PullRequestHubPackaging。当系统提示选择目标和最低平台版本时,请使用默认值并单击“确定”。右键单击应用程序并为 PullRequestHub 项目添加引用。

添加引用后,需要将 PullRequestHub 项目设置为入口点。完成后,在下次构建时很可能会看到以下错误:“如果 SelfContained 为 true,则项目 PullRequestHub 必须在项目文件中指定 RuntimeIdentifiers。”

若要修复此错误,请编辑 PullRequestHub.csproj 文件。在打开本项目文件时,你会注意到使用 .NET Core 的另一个优点,这是因为该项目文件现在使用的是新的轻量级格式。在基于 .NET Framework 的 WinForms 项目中,项目文件将更详细地包含显式默认值和引用,并将 NuGet 引用拆分为 packages.config 文件。新的项目文件格式会将包引用引入到项目文件中,从而可以在一个位置管理所有依赖项。

在本文件的第一个 PropertyGroup 节点中,添加以下行:

<RuntimeIdentifiers>win-x86</RuntimeIdentifiers>

运行时标识符用于标识运行应用程序的目标平台,并由 .NET 包用来表示 NuGet 包中特定于平台的资产。添加该标识符后,构建就应该成功了,可将 PullRequestHubPackaging 项目设置为 Visual Studio 中的启动项目。

指示项目是独立的设置是 PullRequestHubPackaging.wapproj 文件中需要注意的一点。文件中需要注意的代码部分如下:

<ItemGroup>
  <ProjectReference Include=”..\PullRequestHub\PullRequestHub.csproj”>
    <DesktopBridgeSelfContained>True</DesktopBridgeSelfContained>
    <DesktopBridgeIdentifier>$(DesktopBridgeRuntimeIdentifier)
    </DesktopBridgeIdentifier>
      <Properties>SelfContained=%(DesktopBridgeSelfContained);
        RuntimeIdentifier=%(DesktopBridgeIdentifier)
      </Properties>
    <SkipGetTargetFrameworkProperties>True
    </SkipGetTargetFrameworkProperties>
  </ProjectReference>
</ItemGroup>

在此处,可看到 DesktopBridgeSelfContained 选项已设置为 true,这使得 WinForms 应用程序可以与 .NET Core 二进制文件一起打包。运行该项目时,它会将文件转储到“win-x86”的文件夹中,该文件夹的路径与此类似:

C:\Your-Path\PullRequestHub\PullRequestHub\bin\x86\Debug\netcoreapp3.0

在 win-x86 文件夹中你会注意到有许多 DLL,这些 DLL 包含独立应用运行所需的所有文件。

你更有可能需要将应用部署为旁载应用程序或将其上载到 Microsoft Store。旁载将使用 appinstaller 文件进行自动更新。从 Visual Studio 2017 Update 15.7 开始支持这些更新。此外,还可以创建支持提交到 Microsoft Store 进行分发的包。Microsoft Store 随后会处理应用的所有代码签名、分发和更新。

除了这些选项之外,还有一些正在进行的工作可以将应用程序打包成单个可执行文件,从而无需使用 DLL 填充输出目录。

其他优点

通过 .NET Core 3.0,还可以利用 C# 8.0 的功能,包括可为 null 的引用类型、接口上的默认实现、使用模式切换语句的改进以及异步数据流。若要启用 C# 8.0,请打开 PullRequestHub.csproj 文件并将以下行添加到第一个 PropertyGroup:

<LangVersion>8.0</LangVersion>

使用 .NET Core 和 WinForms 的另一个优点是两个项目均为开源。这使得你可以访问源代码、提交 bug、共享反馈并成为贡献者。请查看 github.com/dotnet/winforms 上的 WinForms 项目。

.NET Core 3.0 旨在为企业和公司在 WinForms 应用程序中进行的投资注入新的活力,而 WinForms 应用程序会继续保持高效、可靠且易于部署和维护。开发人员可以利用新的 .NET Core 特定类(如 HttpClientFactory)、采用 C# 8.0 功能(如可为 null 的引用类型)和打包独立应用程序。他们还可以访问 .NET Core CLI 以及 .NET Core 附带的所有性能改进。


Eric Fleming**** 是一名高级软件工程师,在使用 Microsoft 工具和技术方面拥有十多年的经验。他在 ericflemingblog.com 上发布博客文章,并共同主持了探索 Azure Functions 的函数接合 YouTube 频道。可以在 Twitter 上关注他:@efleming18

衷心感谢以下技术专家对本文的审阅:Olia Gavrysh (Microsoft)、Simon Timms


在 MSDN 杂志论坛讨论这篇文章