dotnet / dotnet/winforms

Add WindowsFormsLifetime extensions to the .NET Generic Host

Open
#11,415 40 comments 21 reactions 1 assignee Claimed by @KlausLoeffelmann View on GitHub
api-suggestion tenet-modernization waiting-on-team
Dominant language
C#
Stars
4.9k
Forks
1.1k
Avg merge
20h 23m
Merged PRs (30d)
103

Description

### Background and motivation

The .NET Generic Host is the standard for developing .NET applications that leverage common .NET libraries such as Dependency Injection, Configuration and Logging. Currently, .NET does not provide a built in way for Windows Forms developers to use the Generic Host. I believe most Windows Forms developers use the standard template that comes with Visual Studio when creating Windows Forms applications. This template uses old .NET patterns and isn't up to date with the new Minimal API pattern added to .NET.

While working in the Enterprise world, I worked on a lot of internal Windows Forms applications that we were modernizing and upgrading to .NET Core at the time. There was no easy way to use **dependency injection**, **configuration**, **logging**, and background task support. This need drove me to write the [WindowsFormsLifetime](https://github.com/alex-oswald/WindowsFormsLifetime) library that provides extensions for the Generic Host to support Windows Forms and its lifetime. I believe there is a want and need for this functionality world wide, and building it into .NET would allow Windows Forms developers everywhere the chance to use modern patterns and libraries they use in other places.

#### High Level

Windows Forms Lifetime works by registering a custom implementation of `IHostLifetime` for the generic host. It also registers an `IHostedService` that creates and manages the GUI thread that Windows Forms works on top of, and hooks into Windows Forms `Application` context to shut down the host when the application exits.

#### Going Deep

A high level explanation of how Windows Forms Lifetime was described above. This section will go into detail of the inner workings. Lets hope I understand this the same as when I wrote the library since it's been 4 years since my initial proof of concept.

To understand how this works, you need to have a beginners understanding of how Windows Forms works internally. WinForms uses an `Application` class with a bunch of static methods that manages the application and the UI thread. The `Application` class helps manage an `ApplicationContext`. This context controls the application by listening to the main forms close event and then exits the GUI threads message loop that is reference by the `ThreadContext`. When you write a Windows Forms application, you invoke `Application.Run()` to start the GUI thread and show the main form. When you exit the main form, the `ApplicationContext` is notified and exits the GUI thread. Using this information, we can modify how `Application` works and use our own GUI thread. So lets get into it.

The Generic Host implements a singleton of `IHostLifetime`. In the standard Generic Host implementation, `IHostLifetime` is never messed with, but since we want to control the lifetime of the application, we first create a custom implementation of the interface called `WindowsFormsLifetime`. Injecting our own implementation of `IHostLifetime` overrides the default implementation and lets us control when the Generic Host shuts down.

We also add the `WindowsFormsHostedService` `IHostedService` implementation. This is really the core of the library. The Generic Host starts up every `IHostedService` registered and they can effectively run for the entirety of the application. When `WindowsFormsHostedService` is started, it creates a thread for the GUI and passes in the `StartUiThread` method. The `StartUiThread` method also registers a callback with `Application.ApplicationExit` that signals the Generic Host to shutdown when the WinForms application exits, i.e. the main form is closed. The `StartUiThread` also captures the `WindowsFormsSynchronizationContext` for the GUI thread and saves it to the singleton `WindowsFormsSynchronizationContextProvider` for easy retrieval later using dependency injection. Lastly, the `StartUiThread` method gets the `ApplicationContext` registered with the service provider and passes it to the `Application.Run()` invocation.

As mentioned above, an instance of `WindowsFormsSynchronizationContextProvider` is registered with the service provider for easy retrieval in various parts of the application. This class also inherits the `IGuiContext` interface that exposes methods to invoke `Action`'s and `Func`'s with the `WindowsFormsSynchronizationContext` to ensure they are invoked on the GUI thread.

An implementation of `IFormProvider` is also registered with the service provider. This implementation provides an easy way to fetch a new instance of a form from the service provider and ensures it is created on the GUI thread. It exposes a few methods such as, `GetFormAsync()` and `GetFormAsync(IServiceScope scope)` allowing the creation of scoped forms. The advantage here is that you can create a form with the same scope as a `DbContext` instance. This allows the `DbContext` instance to be disposed of when the form is closed.

### API Proposal

The main API proposal adds extension methods to the Generic Host. A few `IHostApplicationBuilder` extensions would be created in the `Microsoft.Extensions.Hosting` namespace.

```csharp
namespace Microsoft.Extensions.Hosting;

public static class WindowsFormsLifetimeHostApplicationBuilderExtensionsa
{
public static IHostApplicationBuilder UseWindowsFormsLifetime(
this IHostApplicationBuilder hostAppBuilder,
Action configure = null)
where TStartForm : Form

public static IHostApplicationBuilder UseWindowsFormsLifetime(
this IHostApplicationBuilder hostAppBuilder,
Func applicationContextFactory = null,
Action configure = null)
where TAppContext : ApplicationContext

public static IHostApplicationBuilder UseWindowsFormsLifetime(
this IHostApplicationBuilder hostAppBuilder,
Func applicationContextFactory,
Action configure = null)
where TAppContext : ApplicationContext
where TStartForm : Form
}
```

A few `IServiceCollection` extensions would be created in the `Microsoft.Extensions.DependencyInjection` namespace.

```csharp
namespace Microsoft.Extensions.DependencyInjection;

public static class WindowsFormsLifetimeServiceCollectionExtensions
{
public static IServiceCollection AddWindowsFormsLifetime(
this IServiceCollection services,
Action configure,
Action preApplicationRunAction = null)

public static IServiceCollection AddWindowsFormsLifetime(
this IServiceCollection services,
Action configure = null,
Action preApplicationRunAction = null)
where TStartForm : Form

public static IServiceCollection AddWindowsFormsLifetime(
this IServiceCollection services,
Func applicationContextFactory = null,
Action configure = null,
Action preApplicationRunAction = null)
where TAppContext : ApplicationContext

public static IServiceCollection AddWindowsFormsLifetime(
this IServiceCollection services,
Func applicationContextFactory,
Action configure = null,
Action preApplicationRunAction = null)
where TAppContext : ApplicationContext
where TStartForm : Form
}
```

#### Registered Services

`IOptions`

```csharp
public HighDpiMode HighDpiMode { get; set; } = HighDpiMode.SystemAware;
public bool EnableVisualStyles { get; set; } = true;
public bool CompatibleTextRenderingDefault { get; set; }
public bool SuppressStatusMessages { get; set; }
public bool EnableConsoleShutdown { get; set; }
```

`IHostApplicationLifetime` via `WindowsFormsLifetime`

Replaces the hosts singleton lifetime implementation.

`IWindowsFormsSynchronizationContextProvider` via `WindowsFormsSynchronizationContextProvider`

Holds a reference to the `WindowsFormsSynchronizationContext` for the Windows Forms thread.

```csharp
WindowsFormsSynchronizationContext SynchronizationContext { get; }
```

`IWindowsFormsThreadContext` via `WindowsFormsSynchronizationContextProvider`

Provides methods to marshal calls to the `WindowsFormsSynchronizationContext`, i.e. the Windows Forms thread.

```csharp
void Invoke(Action action);
TResult Invoke(Func func);
Task InvokeAsync(Func func);
Task InvokeAsync(Func func, TInput input);
```

`IFormProvider` via `FormProvider`

```csharp
Task GetFormAsync() where T : Form;
Task GetFormAsync(IServiceScope scope) where T : Form;
Task GetMainFormAsync();
T GetForm() where T : Form;
T GetForm(IServiceScope scope) where T : Form;
```

### API Usage

Currently, when you create a Windows Forms project in C# using the template, you get the following `Program.cs` file:

```csharp
namespace WinFormsApp1
{
internal static class Program
{
///
/// The main entry point for the application.
///
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
}
}
```

Using the new API, `Program.cs` would look like the following:

```csharp
using Microsoft.Extensions.Hosting;
using WinFormsApp1;

var builder = Host.CreateApplicationBuilder(args);
builder.UseWindowsFormsLifetime();
var app = builder.Build();
app.Run();
```

This allows the use of dependency injection. Here is an example of a forms code.

https://github.com/alex-oswald/WindowsFormsLifetime/blob/main/samples/SampleApp/Form1.cs

```csharp
using Microsoft.Extensions.Logging;
using WindowsFormsLifetime;

namespace SampleApp;

public partial class Form1 : Form
{
private readonly ILogger _logger;
private readonly IFormProvider _formProvider;

public Form1(ILogger logger, IFormProvider formProvider)
{
InitializeComponent();
_logger = logger;
_formProvider = formProvider;

ThreadLabel.Text = $"{Thread.CurrentThread.ManagedThreadId} {Thread.CurrentThread.Name}";
}

private async void button1_Click(object sender, EventArgs e)
{
_logger.LogInformation("Show");
var form = await _formProvider.GetFormAsync();
form.Show();
}

private void button2_Click(object sender, EventArgs e)
{
_logger.LogInformation("Close");
this.Close();
}
}
```

#### Use Cases

Windows Forms Blazor Hybrid

```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWindowsFormsLifetime();
builder.Services.AddWindowsFormsBlazorWebView();

var app = builder.Build();
app.Run();
```

An app with hosted services that run background tasks.

```csharp
var builder = Host.CreateApplicationBuilder(args);
builder.UseWindowsFormsLifetime();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
builder.Services.AddTransient();

var app = builder.Build();
app.Run();
```

Using Entity Framework with a Sqlite database.

```csharp
var builder = Host.CreateApplicationBuilder(args);
builder.Host.UseWindowsFormsLifetime();

builder.Services.AddScoped, EntityFrameworkRepository>();
builder.Services.AddDbContext(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

// Create the database
var db = app.Services.GetService();
db?.Database.EnsureCreated();

app.Run();
```

### Alternative Designs

- `IHostApplicationBuilder` extension methods like `UseWindowsFormsLifetime` could be simply named `UseWindowForms`.
- Change name of `IGuiContext` to `IWindowsFormsThreadContext`.

### Risks

Low, this should just enable easier modern development. Possible Windows Forms thread issues, though enough testing will mitigate this.

### Will this feature affect UI controls?

No, this will not affect UI controls.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.