May 2019

Volume 34 Number 5

[.NET Core 3.0]

Create a Centralized Pull Request Hub with WinForms in .NET Core 3.0

By Eric Fleming | May 2019

Windows Forms, or simply WinForms, has been used for years to develop powerful Windows-based applications with rich and interactive interfaces. The investments in these desktop applications across businesses of every type are profound, with some 2.4 million developers using Visual Studio to create desktop-­style apps every month. The benefits of leveraging and extending existing WinForms code assets are compelling, but there are others, as well. The WinForms drag-and-drop designer experience empowers users to build fully functional UIs with no special knowledge or training. WinForms applications are easy to deploy and update, can work independent of Internet connectivity, and can offer improved security by running on a local machine that doesn’t expose configurations to the Internet. Until recently, WinForms applications could only be built using the full .NET Framework, but the release of the .NET Core 3.0 preview changes all that.

The new features and benefits of .NET Core go beyond Web development. With .NET Core 3.0, WinForms adds capabilities like easier deployment, improved performance, support for .NET Core-specific NuGet packages, the .NET Core command-line interface (CLI) and more. Throughout this article, I’ll touch on many of these benefits, why they matter and how to use them in WinForms applications.

Let’s jump right into building our first .NET Core 3.0 WinForms application. For this article, I’m going to build an application that retrieves and displays open pull requests for one of the open source Microsoft repositories hosted on GitHub. The first step is to install the latest versions of Visual Studio 2019 and the .NET Core 3.0 SDK, after which you’ll have access to the .NET Core CLI commands to create a new WinForms application. This wasn’t possible for WinForms applications before the addition of .NET Core support.

Coming soon is a new Visual Studio template that lets you create a WinForms project targeting .NET Core 3.0. It’s not available just yet, so for now let’s generate a new WinForms project named PullRequestHub by running the following command:

dotnet new winforms -o PullRequestHub

In order to ensure that the project was created successfully, navigate into the new directory created by the dotnet new command and use the CLI to build and run the project, like so:

cd .\PullRequestHub\

Because you have access to the .NET Core CLI, you also have access to the commands to restore, run and build. Before running, try out the restore and build commands, as follows:

dotnet restore
dotnet build

These commands work just as they would when run in the command line for a .NET Core Web application. And note that when you execute the dotnet run command, it actually performs both a restore and a build before executing the app (bit.ly/2UCkEaN). Now let’s run the project to test it out by entering dotnet run at the command line.

Success! You’ve just created your first .NET Core WinForms application. When you run, you’ll see a form appear on your screen with the text, “Hello .NET Core!”.

Before I go further with adding logic to our application, let’s take a moment to talk about the current state of the WinForms Designer view in Visual Studio.

Setting up the Designer for .NET Core WinForms Apps

When you open the CLI-generated project in Visual Studio, you may notice that some functionality is missing. Most notably, there’s currently no designer view provided for .NET Core WinForms applications. While there are plans to make this functionality available, they’ve yet to be completed.

Fortunately, there’s a workaround that can give you access to a designer, at least until native support is added. For now, you can create a .NET Framework project that contains your UI files. This way you can edit the UI files using the designer, and the .NET Core project will then reference the UI files from the .NET Framework project. This enables you to leverage the UI capabilities while still building the application in .NET Core. Here’s how I do this for my project.

In addition to the PullRequestHub project you created, you’ll want to add a new WinForms project running on a version of .NET Full-Framework. Name this project PullRequestHub.Designer. After the new project is created, remove the Form1 files from the .NET Core project, leaving only the Program.cs class.

Navigate into the PullRequestHub.Designer and rename the form files to PullRequestForm. Now you’ll edit the .NET Core project file and add the following code to link the files together in both projects. This will take care of any additional forms or resources you create in the future, as well:

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

Once you save the project file, you’ll see the PullRequestForm files appear in the solution explorer and you’ll be able to interact with them. When you want to use the UI editor, you’ll need to make sure to close the PullRequestForm file from the .NET Core project and open the PullRequestForm file from the .NET Framework project. The changes will take place in both, but the editor is only available from the .NET Framework project.

Building the Application

Let’s start adding some code to the application. In order to retrieve open pull requests from GitHub, I need to create an HttpClient. This is where .NET Core 3.0 comes in, because it provides access to the new HttpClientFactory. The HttpClient in the full-framework version had some issues, including one with creating the client with a using statement. The HttpClient object would be disposed of, but the underlying socket wouldn’t be released for some time, which by default is 240 seconds. If the socket connection remains open for 240 seconds and you have a high throughput in your system, there’s a chance your system saturates all the free sockets. When this happens, new requests must wait for a socket to free up, which can produce some rather drastic performance impacts.

The HttpClientFactory helps mitigate these issues. For one, it gives you an easier way to pre-configure client implementations in a more central location. It also manages the lifetime of the HttpClients for you, so you don’t run into the previously mentioned issues. Let’s look at how you can do this now in a WinForms application.

One of the best and easiest ways to use this new feature is through dependency injection. Dependency injection, or more generally inversion of control, is a technique for passing dependencies into classes. It’s also a fantastic way to reduce the coupling of classes and to ease unit testing. For example, you’ll see how you can create an instance of the IHttpClientFactory while the program is starting up, with that object able to be leveraged later in the form. This was not something very easily accomplished in WinForms on previous versions of .NET, and it’s another advantage of using .NET Core.

In the Program.cs you’re going to create a method named ConfigureServices. In this method, create a new ServiceCollection to make services available to you via dependency injection. You’ll need to install the latest of these two NuGet packages first:

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

Then add the code shown in Figure 1. This creates a new IHttpClientFactory to be used in your forms. The result is a client that you can explicitly use for requests involving the GitHub API.

Figure 1 Create a New 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;
  });
}

Next, you need to register the actual form class, PullRequestForm, as a singleton. To the end of this method, add the following line:

services.AddSingleton<PullRequestForm>();

Then you need to create an instance of the ServiceProvider. At the top of the Program.cs class, create the following property:

private static IServiceProvider ServiceProvider { get; set; }

Now that you have a property for your ServiceProvider, at the end of the ConfigureServices method, add a line to build the ServiceProvider, like so:

ServiceProvider = services.BuildServiceProvider();

At the end of all of this, the full ConfigureServices method should look like the code in Figure 2.

Figure 2 The ConfigureServices Method

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();
}

Now you need to wire the form up with the container when it starts. When the application runs, this will invoke the PullRequestForm with the necessary services available to it. Change the Main method to be the following code:

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

Great! Now you’re all wired up. In the PullRequestForm constructor, you’re going to inject the IHttpClientFactory you just wired up and assign it to a local variable, as shown in the code here:

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

You now have an HttpClient you can use to make calls out to GitHub to retrieve pull requests, issues and the like. This also makes the next few steps a little tricky. The calls from the HttpClient are going to be async requests, and if you’ve been using WinForms, you know what’s coming. You’re going to have to handle the threading, and send dispatch updates to the UI thread.

In order to kick off retrieving all of the pull requests, let’s add a button to the view. This way you can, in the future, add more repos or more groups of repos to check. Using the designer that you wired up, drag the button on to the form and rename the text to read “Microsoft.” While you’re at it, give your button a more meaningful name like RetrieveData_Button. You’ll need to tie in to the RetrieveData_Button_Click event, but you need it to be async, using this code:

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

Here’s where you’ll want to call a method that retrieves open GitHub pull requests. But first, because you’re dealing with async calls now, you must wire up the SynchronizationContext. You do this by adding a new property and updating the constructor with the following code:

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

Next, create a model named PullRequestData, so you can easily deserialize your request. Here’s the code for that:

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

Finally, create a method named GetPullRequestData. In this method, you’re going to make your request to the GitHub API and retrieve all the open pull requests. You’ll be deserializing a JSON request, so add the latest version of Newtonsoft.Json package to the project. Here’s the code:

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;
}

This can now be invoked from the RetrieveData_Button_Click method. Once you have a list of the data you want, create a list of labels for each Title so you can display it on the form. After you have the list of labels, you can add them to the UI in the UpdateUI method. Figure 3 shows this.

Figure 3 Invoking from 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);
  });
}

Your UpdateUI method will then use the synchronizationContext to update the UI, like so:

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

If you run the application and click the Microsoft button, the UI will be updated with the titles of all the open pull requests from the dotnet/winforms repository on GitHub.

Now it’s your turn. To truly make this a centralized pull request hub, as the title of this article promises, let’s update this example to read from multiple GitHub repositories. These repositories don’t need to be from the Microsoft team, though it’s fun to watch their progress. For example, microservices architectures are very common, and in them you may have numerous repositories that make up your system as a whole. Given that it’s usually a good idea to not leave branches and pull requests out there, unmerged, for too long, a tool like this could boost your insight into open pull requests and improve the quality of your entire system.

You could certainly set up a Web app, but then you’d have to worry about deployments, where it’s going to run, authentication and the like. With a WinForms application in .NET Core, you don’t have to worry about any of this. Let’s now take a look at one of the biggest advantages of building a WinForms app using .NET Core.

Packaging the Application

In the past, deploying a new or updated WinForms application could cause problems related to the version of the .NET Framework installed on the host machines. With .NET Core, apps can be deployed self-contained and run from a single folder, with no dependencies to the version of .NET Framework installed on the machine. This means the user doesn’t need to install anything; they can simply run the application. It also enables you to update and deploy apps one at a time, because the packaged versions of .NET Core will not affect each other.

For the sample app in this article, you’ll want to package it up as self-contained. Keep in mind that self-contained applications will be larger because they include the .NET Core libraries with them. If you’re deploying to machines with the latest versions of .NET Core installed, you don’t need to make the app self-contained. Instead, you can reduce the size of the deployed app by leveraging the installed version of .NET Core. Self-contained options are for when you don’t want your application to be dependent on the environment in which it will be running.

To package the application locally, you need to make sure Developer Mode is enabled in your settings. Visual Studio will prompt you and give you a link to the settings when you try to run the packaging project, but to enable it directly go to your Windows settings, press the Windows key and search for Settings. In the search box type “For developers settings” and select it. You’ll see an option to enable Developer Mode. Select and enable this option.

For the most part, the steps to create a self-contained package will seem familiar if you’ve previously packaged a WinForms application. First, start by creating a new Windows Application Packaging Project. Name the new project PullRequestHubPackaging. When prompted to select the target and minimum platform versions, use the defaults and click OK. Right-click on Applications and add a reference to the PullRequestHub project.

After the reference has been added, you need to set the PullRequestHub project as the Entry Point. Once that’s done, the next time you build you’ll very likely see the following error: “Project PullRequestHub must specify ‘RuntimeIdentifiers’ in the project file when ‘SelfContained’ is true.”

To fix this error, edit the PullRequestHub.csproj file. When you open this project file, you’ll notice yet another advantage of using .NET Core, because the project file is now using the new, lightweight format. In .NET Framework-based WinForms projects, the project file is much more verbose with explicit defaults and references, as well as NuGet references split out into a packages.config file. The new project file format brings package references into the project file, making it possible to manage all your dependencies in one place.

In this file, in the first PropertyGroup node, add the following line:

<RuntimeIdentifiers>win-x86</RuntimeIdentifiers>

A Runtime Identifier is used to identify target platforms where the application runs, and is used by .NET packages to represent platform-specific assets in NuGet packages. Once this is added, the build should succeed, and you can set the PullRequestHubPackaging project as the startup project in Visual Studio.

One thing to note in the PullRequestHubPackaging.wapproj file is the setting to indicate that the project is self-contained. The section of code in the file to pay attention to is the following:

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

Here you can see that the DesktopBridgeSelfContained option is set to true, which enables the WinForms application to be packaged with the .NET Core binaries. When you run the project, it dumps the files out to a folder named “win-x86” found in a path similar to this:

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

Inside of the win-x86 folder you’ll notice many DLLs, which include everything that the self-contained app needs to run.

More likely, you’ll want to deploy the app as a side-loaded appli­cation or upload it to the Microsoft Store. Side-loading will make automatic updates possible using an appinstaller file. These updates are supported starting with Visual Studio 2017, Update 15.7. You can also create packages that support submission to the Microsoft Store for distribution. The Microsoft Store then handles all the code signing, distribution and updating of the app.

In addition to these options, there’s ongoing work to make it possible for applications to be packaged up into a single executable, eliminating the need to populate an output directory with DLLs.

Additional Advantages

With .NET Core 3.0, you’re also able to leverage features of C# 8.0, including nullable reference types, default implementations on interfaces, improvements to switch statements using patterns, and asynchronous streams. To enable C# 8.0, open the PullRequestHub.csproj file and add the following line to the first PropertyGroup:

<LangVersion>8.0</LangVersion>

Another advantage to using .NET Core and WinForms is that both projects are open source. This gives you access to the source code, and lets you file bugs, share feedback and become a contributor. Check out the WinForms project at github.com/dotnet/winforms.

.NET Core 3.0 promises to breathe new life into the investments that enterprises and businesses have made into WinForms applications, which continue to be productive, reliable, and easy to deploy and maintain. Developers can leverage new .NET Core-specific classes like HttpClientFactory, employ C# 8.0 features like nullable reference types, and package up self-contained applications. They also gain access to the .NET Core CLI and all the performance improvements that come with .NET Core.


Eric Fleming is a senior software engineer with more than a decade of experience working with Microsoft tools and technologies. He blogs at ericflemingblog.com and co-hosts the Function Junction YouTube channel that explores Azure Functions. Follow him on Twitter: @efleming18.

Thanks to the following technical experts who reviewed this article: Olia Gavrysh (Microsoft), Simon Timms


Discuss this article in the MSDN Magazine forum