From 9885dd6255e6f248ad908b82d793b48e62d7f5a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 28 Mar 2024 23:55:22 +0100 Subject: [PATCH] feat!: Finished implementation, added tests, added description and README (#4) * feat: Finished implementation, added tests, added description and README * docs: Updated exception infos * fix: Registered `IFormularProvider` * chore: Added Integration Tests * chore: Removed unused classes and activated sonarqube * chore: Hide TestForm --- .editorconfig | 2 +- .github/workflows/cicd.yml | 3 + Extensions.Hosting.WinForms.sln | 25 +- README.md | 69 +++- .../IFormularProvider.cs | 54 ++- .../IHostBuilderExtensions.cs | 122 +----- ...dowsFormsSynchronizationContextProvider.cs | 60 ++- .../Internals/FormularProvider.cs | 88 ++-- .../Internals/IServiceCollectionExtensions.cs | 2 + .../Internals/WindowsFormsLifetime.cs | 2 + ...dowsFormsSynchronizationContextProvider.cs | 63 +-- .../README.md | 59 ++- .../IHostBuilderExtensionsTests.cs | 291 +++++++++++++ .../IHostBuilderExtensionsTests.cs | 17 +- .../Internals/FormularProviderTests.cs | 326 +++++++++++++++ .../IServiceCollectionExtensionsTests.cs | 4 +- .../Internals/TestForm.cs | 7 - ...ormsSynchronizationContextProviderTests.cs | 383 ++++++++++++++++++ xample/Xample.Simple/Form1.Designer.cs | 45 ++ xample/Xample.Simple/Form1.cs | 19 + .../Xample.Simple/Form1.resx | 50 +-- xample/Xample.Simple/Program.cs | 13 + xample/Xample.Simple/Xample.Simple.csproj | 15 + 23 files changed, 1476 insertions(+), 243 deletions(-) create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration/IHostBuilderExtensionsTests.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/FormularProviderTests.cs delete mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/WindowsFormsSynchronizationContextProviderTests.cs create mode 100644 xample/Xample.Simple/Form1.Designer.cs create mode 100644 xample/Xample.Simple/Form1.cs rename tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.resx => xample/Xample.Simple/Form1.resx (93%) create mode 100644 xample/Xample.Simple/Program.cs create mode 100644 xample/Xample.Simple/Xample.Simple.csproj diff --git a/.editorconfig b/.editorconfig index e0f6063..5dd3656 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ insert_final_newline = true indent_style = space trim_trailing_whitespace = true charset = utf-8 -end_of_line = lf +end_of_line = crlf # Code files [*.{cs,csx,vb,vbx}] diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 9f563a8..2feab99 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -23,9 +23,12 @@ jobs: name: Build & Tests uses: dailydevops/pipelines/.github/workflows/cicd-dotnet.yml@main with: + enableSonarQube: true dotnet-logging: ${{ inputs.dotnet-logging }} dotnet-version: | + 6.x 7.x + 8.x solution: ./Extensions.Hosting.WinForms.sln runs-on-build: windows-latest runs-on-tests: windows-latest diff --git a/Extensions.Hosting.WinForms.sln b/Extensions.Hosting.WinForms.sln index 05fd95a..c176862 100644 --- a/Extensions.Hosting.WinForms.sln +++ b/Extensions.Hosting.WinForms.sln @@ -23,24 +23,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{63A3C280-DDDF-4BC1-9B8F-AC8D9F949B3E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.Extensions.Hosting.WinForms", "src\NetEvolve.Extensions.Hosting.WinForms\NetEvolve.Extensions.Hosting.WinForms.csproj", "{5501B170-5917-4568-9EAC-8853D14238A1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.Extensions.Hosting.WinForms", "src\NetEvolve.Extensions.Hosting.WinForms\NetEvolve.Extensions.Hosting.WinForms.csproj", "{5501B170-5917-4568-9EAC-8853D14238A1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8A14AF25-8CF0-494E-9D50-2FAE3CC9A50A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.Extensions.Hosting.WinForms.Tests.Unit", "tests\NetEvolve.Extensions.Hosting.WinForms.Tests.Unit\NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.csproj", "{F7BA85AD-CBD1-4F99-BC28-42DF1253BAE8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.Extensions.Hosting.WinForms.Tests.Unit", "tests\NetEvolve.Extensions.Hosting.WinForms.Tests.Unit\NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.csproj", "{F7BA85AD-CBD1-4F99-BC28-42DF1253BAE8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.Extensions.Hosting.WinForms.Tests.Integration", "tests\NetEvolve.Extensions.Hosting.WinForms.Tests.Integration\NetEvolve.Extensions.Hosting.WinForms.Tests.Integration.csproj", "{22F991BF-06C3-4B35-BCA2-DD61959C187F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.Extensions.Hosting.WinForms.Tests.Integration", "tests\NetEvolve.Extensions.Hosting.WinForms.Tests.Integration\NetEvolve.Extensions.Hosting.WinForms.Tests.Integration.csproj", "{22F991BF-06C3-4B35-BCA2-DD61959C187F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture", "tests\NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture\NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture.csproj", "{DF47FD5F-2F7F-470B-98A2-9C5266AA0FBD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture", "tests\NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture\NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture.csproj", "{DF47FD5F-2F7F-470B-98A2-9C5266AA0FBD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "xample", "xample", "{B241FEA0-8C20-49E2-BD16-36ACBD4A00C9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xample.Simple", "xample\Xample.Simple\Xample.Simple.csproj", "{F8CFBE08-1E6F-4CC6-BF56-CE2E72B19A16}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5501B170-5917-4568-9EAC-8853D14238A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5501B170-5917-4568-9EAC-8853D14238A1}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -58,11 +59,21 @@ Global {DF47FD5F-2F7F-470B-98A2-9C5266AA0FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU {DF47FD5F-2F7F-470B-98A2-9C5266AA0FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF47FD5F-2F7F-470B-98A2-9C5266AA0FBD}.Release|Any CPU.Build.0 = Release|Any CPU + {F8CFBE08-1E6F-4CC6-BF56-CE2E72B19A16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8CFBE08-1E6F-4CC6-BF56-CE2E72B19A16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8CFBE08-1E6F-4CC6-BF56-CE2E72B19A16}.Release|Any CPU.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {5501B170-5917-4568-9EAC-8853D14238A1} = {63A3C280-DDDF-4BC1-9B8F-AC8D9F949B3E} {F7BA85AD-CBD1-4F99-BC28-42DF1253BAE8} = {8A14AF25-8CF0-494E-9D50-2FAE3CC9A50A} {22F991BF-06C3-4B35-BCA2-DD61959C187F} = {8A14AF25-8CF0-494E-9D50-2FAE3CC9A50A} {DF47FD5F-2F7F-470B-98A2-9C5266AA0FBD} = {8A14AF25-8CF0-494E-9D50-2FAE3CC9A50A} + {F8CFBE08-1E6F-4CC6-BF56-CE2E72B19A16} = {B241FEA0-8C20-49E2-BD16-36ACBD4A00C9} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {89AAE175-2ADC-48FB-8667-A87EA55FD851} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 0e844c8..0073e1f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,67 @@ -# template-dotnet -.NET template for repositories +# NetEvolve.Extensions.Hosting.WinForms + +[![Nuget](https://img.shields.io/nuget/v/NetEvolve.Extensions.Hosting.WinForms?logo=nuget)](https://www.nuget.org/packages/NetEvolve.Extensions.Hosting.WinForms/) +[![Nuget](https://img.shields.io/nuget/dt/NetEvolve.Extensions.Hosting.WinForms?logo=nuget)](https://www.nuget.org/packages/NetEvolve.Extensions.Hosting.WinForms/) + +The main purpose of this package is to provide a way to use the `Microsoft.Extensions.Hosting` for WinForms applications, allowing the use of dependency injection, configuration, logging, and other features provided by the `Microsoft.Extensions` libraries. + +:bulb: This package is available for .NET 6.0 and later. + +## Contribution + +If you have any suggestions, bug reports, or any other form of feedback, please feel free to open an issue or a pull request. Any contributions are welcome! + +## Why not .NET Standard? +With the .NET Standard Microsoft created a specification for APIs that are intended to be available on all .NET implementations. This was a great idea, but it also has some drawbacks. The main drawback is that the .NET Standard is a specification and not an implementation. This means that the real work is done by .NET implementations, such as .NET 5.0 and later versions. Which is why we decided us against the .NET Standard and for the concrete .NET implementations. + +See [The future of .NET Standard](https://devblogs.microsoft.com/dotnet/the-future-of-net-standard/) for more details. + +## Installation +To use this package, you need to add the package to your project. You can do this by using the NuGet package manager or by using the dotnet CLI. +```powershell +dotnet add package NetEvolve.Extensions.Hosting.WinForms +``` + +## Usage +To use the `Microsoft.Extensions.Hosting` in a WinForms application, you just need to create a new `HostBuilder` and configure it as you would do in a console application. + +```csharp +namespace WinForms; + +using Microsoft.Extensions.Hosting; +using NetEvolve.Extensions.Hosting.WinForms; + +internal static class Program +{ + internal static async Task Main() => + await CreateHostBuilder().Build().RunAsync().ConfigureAwait(false); + + public static IHostBuilder CreateHostBuilder() => + Host.CreateDefaultBuilder().UseWindowsForms(); +} +``` + +Therefore, you can use for example the `Microsoft.Extensions.DependencyInjection` to register services and inject them into your forms. + +```csharp +namespace WinForms; + +using Microsoft.Extensions.DependencyInjection; +using System.Windows.Forms; + +public partial class Form1 : Form +{ + private readonly ILogger _logger; + + public Form1(ILogger logger) + { + _logger = logger; + InitializeComponent(); + } + + private void Form1_Load(object sender, EventArgs e) + { + _logger.LogInformation("Form loaded."); + } +} +``` \ No newline at end of file diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/IFormularProvider.cs b/src/NetEvolve.Extensions.Hosting.WinForms/IFormularProvider.cs index 5c418aa..f27dd38 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/IFormularProvider.cs +++ b/src/NetEvolve.Extensions.Hosting.WinForms/IFormularProvider.cs @@ -1,30 +1,76 @@ namespace NetEvolve.Extensions.Hosting.WinForms; +using System; using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.Extensions.DependencyInjection; +/// +/// Unified access to provide windows forms, which can be used to create and manage forms. +/// public interface IFormularProvider { + /// + /// Gets the formular of the specified type. + /// + /// The specified forms. + /// The requested form. T GetFormular() where T : Form; + /// + /// Gets the formular of the specified type asynchronously. + /// + /// The specified forms. + /// The requested form. ValueTask GetFormularAsync() where T : Form; + /// + /// Gets the main formular. + /// + /// The requested form. Form GetMainFormular(); + /// + /// Gets the main formular asynchronously. + /// + /// The requested form. ValueTask
GetMainFormularAsync(); - ValueTask GetScopedFormAsync() + /// + /// Gets the scoped formular of the specified type. + /// + /// The specified forms. + /// The requested form. + T GetScopedForm() where T : Form; - ValueTask GetScopedFormAsync(IServiceScope scope) + /// + /// Gets the scoped formular of the specified type. + /// + /// The specified forms. + /// The scope. + /// Throws a , if is . + /// The requested form. + T GetScopedForm(IServiceScope scope) where T : Form; - T GetScopedForm() + /// + /// Gets the scoped formular of the specified type asynchronously. + /// + /// The specified forms. + /// The requested form. + ValueTask GetScopedFormAsync() where T : Form; - T GetScopedForm(IServiceScope scope) + /// + /// Gets the scoped formular of the specified type asynchronously. + /// + /// The specified forms. + /// The scope. + /// Throws a , if is . + /// The requested form. + ValueTask GetScopedFormAsync(IServiceScope scope) where T : Form; } diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/IHostBuilderExtensions.cs b/src/NetEvolve.Extensions.Hosting.WinForms/IHostBuilderExtensions.cs index 68ec520..15e7ae6 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/IHostBuilderExtensions.cs +++ b/src/NetEvolve.Extensions.Hosting.WinForms/IHostBuilderExtensions.cs @@ -6,11 +6,7 @@ using Microsoft.Extensions.Hosting; using NetEvolve.Extensions.Hosting.WinForms.Internals; -#if NET8_0_OR_GREATER -/// -/// Extension methods for or to configure Windows Forms Lifetime. -/// -#elif NET7_0 +#if NET7_0_OR_GREATER /// /// Extension methods for or to configure Windows Forms Lifetime. /// @@ -68,13 +64,14 @@ public static IHostBuilder UseWindowsForms( return builder.ConfigureServices(services => { services = contextFactory is null - ? services.AddSingleton() - : services.AddSingleton(sp => contextFactory.Invoke(sp)); + ? services.AddSingleton() + : services.AddSingleton(sp => + contextFactory.Invoke(sp) + ); _ = services - .AddSingleton() - // Default WindowsForms services - .AddWindowsFormsLifetime(configure); + // Default WindowsForms services + .AddWindowsFormsLifetime(configure); }); } @@ -115,7 +112,7 @@ public static IHostBuilder UseWindowsForms( ); } -#if NET7_0 +#if NET7_0_OR_GREATER /// /// Enables Windows Forms support, builds and starts the host with the specified , /// then waits for the host to close the before shutting down. @@ -160,13 +157,14 @@ public static HostApplicationBuilder UseWindowsForms( ArgumentNullException.ThrowIfNull(builder); var services = contextFactory is null - ? builder.Services.AddSingleton() - : builder.Services.AddSingleton(sp => contextFactory.Invoke(sp)); + ? builder.Services.AddSingleton() + : builder.Services.AddSingleton(sp => + contextFactory.Invoke(sp) + ); _ = services - .AddSingleton(sp => sp.GetRequiredService()) - // Default WindowsForms services - .AddWindowsFormsLifetime(configure); + // Default WindowsForms services + .AddWindowsFormsLifetime(configure); return builder; } @@ -206,96 +204,4 @@ public static HostApplicationBuilder UseWindowsForms - /// Enables Windows Forms support, builds and starts the host with the specified , - /// then waits for the host to close the before shutting down. - /// - /// Form with which the application is to be started. - /// The to configure. - /// The action to be executed for the configuration of the . - /// with enabled Windows Forms support. - public static IHostApplicationBuilder UseWindowsForms( - this IHostApplicationBuilder builder, - Action? configure = null - ) - where TStartForm : Form - { - ArgumentNullException.ThrowIfNull(builder); - - _ = builder - .Services.AddSingleton() - .AddSingleton(sp => new ApplicationContext(sp.GetRequiredService())) - // Default WindowsForms services - .AddWindowsFormsLifetime(configure); - - return builder; - } - - /// - /// Enables Windows Forms support, builds and starts the host with the specified , - /// then waits for the host to close the before shutting down. - /// - /// - /// The to configure. - /// The factory. - /// The action to be executed for the configuration of the . - /// with enabled Windows Forms support. - public static IHostApplicationBuilder UseWindowsForms( - this IHostApplicationBuilder builder, - Func? contextFactory = null, - Action? configure = null - ) - where TApplicationContext : ApplicationContext - { - ArgumentNullException.ThrowIfNull(builder); - - var services = contextFactory is null - ? builder.Services.AddSingleton() - : builder.Services.AddSingleton(sp => contextFactory(sp)); - - _ = services - .AddSingleton(sp => sp.GetRequiredService()) - // Default WindowsForms services - .AddWindowsFormsLifetime(configure); - - return builder; - } - - /// - /// Enables Windows Forms support, builds and starts the host with the specified , - /// which is created by the function, then waits for the host to close the before shutting down. - /// - /// - /// Form with which the application is to be started. - /// The to configure. - /// The factory. - /// The action to be executed for the configuration of the . - /// with enabled Windows Forms support. - public static IHostApplicationBuilder UseWindowsForms( - this IHostApplicationBuilder builder, - Func contextFactory, - Action? configure = null - ) - where TApplicationContext : ApplicationContext - where TStartForm : Form - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(contextFactory); - - _ = builder - .Services.AddSingleton() - .AddSingleton(sp => - { - var startForm = sp.GetRequiredService(); - return contextFactory(sp, startForm); - }) - .AddSingleton(sp => sp.GetRequiredService()) - // Default WindowsForms services - .AddWindowsFormsLifetime(configure); - - return builder; - } -#endif } diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/IWindowsFormsSynchronizationContextProvider.cs b/src/NetEvolve.Extensions.Hosting.WinForms/IWindowsFormsSynchronizationContextProvider.cs index a49a4df..2470378 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/IWindowsFormsSynchronizationContextProvider.cs +++ b/src/NetEvolve.Extensions.Hosting.WinForms/IWindowsFormsSynchronizationContextProvider.cs @@ -2,24 +2,78 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Threading; using System.Threading.Tasks; +/// +/// Allows to invoke actions on the Windows Forms synchronization context. +/// public interface IWindowsFormsSynchronizationContextProvider { + /// + /// Invokes the specified action on the Windows Forms synchronization context. + /// + /// The action to be performed. + /// If is . void Invoke([NotNull] Action action); + /// + /// Invokes the specified action on the Windows Forms synchronization context. + /// + /// The expected return type. + /// The action to be performed. + /// The result of the specified action. + /// If is . [return: MaybeNull] TResult Invoke([NotNull] Func action); + /// + /// Invokes the specified action on the Windows Forms synchronization context. + /// + /// The expected return type. + /// The input type to be passed, which is to be processed. + /// The action to be performed. + /// The specified input. + /// The result of the specified action. + /// If is . [return: MaybeNull] TResult Invoke([NotNull] Func action, TInput input); - ValueTask InvokeAsync([NotNull] Action action); + /// + /// Invokes the specified asynchronous action on the Windows Forms synchronization context. + /// + /// The action to be performed. + /// The cancellation token to be used. + /// The result of the specified action. + /// If is . + ValueTask InvokeAsync([NotNull] Action action, CancellationToken cancellationToken = default); - ValueTask InvokeAsync([NotNull] Func action); + /// + /// Invokes the specified asynchronous action on the Windows Forms synchronization context. + /// + /// The expected return type. + /// The action to be performed. + /// The cancellation token to be used. + /// The result of the specified action. + /// If is . + ValueTask InvokeAsync( + [NotNull] Func action, + CancellationToken cancellationToken = default + ); + /// + /// Invokes the specified asynchronous action on the Windows Forms synchronization context. + /// + /// The expected return type. + /// The input type to be passed, which is to be processed. + /// The action to be performed. + /// The specified input. + /// The cancellation token to be used. + /// The result of the specified action. + /// If is . ValueTask InvokeAsync( [NotNull] Func action, - TInput input + TInput input, + CancellationToken cancellationToken = default ); } diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/FormularProvider.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/FormularProvider.cs index 00412b0..3f2c083 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/FormularProvider.cs +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/FormularProvider.cs @@ -7,6 +7,7 @@ using System.Windows.Forms; using Microsoft.Extensions.DependencyInjection; +/// internal sealed class FormularProvider( IServiceProvider serviceProvider, IWindowsFormsSynchronizationContextProvider synchronizationContext @@ -41,9 +42,7 @@ public T GetFormular() _semaphore.Wait(); try { - var form = synchronizationContext.Invoke(() => serviceProvider.GetService()); - ArgumentNullException.ThrowIfNull(form); - return form; + return synchronizationContext.Invoke(() => serviceProvider.GetRequiredService())!; } finally { @@ -58,11 +57,9 @@ public async ValueTask GetFormularAsync() await _semaphore.WaitAsync().ConfigureAwait(false); try { - var form = await synchronizationContext - .InvokeAsync(() => serviceProvider.GetService()) + return await synchronizationContext + .InvokeAsync(() => serviceProvider.GetRequiredService())! .ConfigureAwait(false); - ArgumentNullException.ThrowIfNull(form); - return form; } finally { @@ -73,10 +70,12 @@ public async ValueTask GetFormularAsync() /// public Form GetMainFormular() { - var context = serviceProvider.GetService(); + var context = serviceProvider.GetRequiredService(); - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(context.MainForm); + if (context.MainForm is null) + { + throw new InvalidOperationException("The main form is not set."); + } return context.MainForm; } @@ -84,41 +83,54 @@ public Form GetMainFormular() /// public ValueTask GetMainFormularAsync() { - var context = serviceProvider.GetService(); + var context = serviceProvider.GetRequiredService(); - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(context.MainForm); + if (context.MainForm is null) + { + throw new InvalidOperationException("The main form is not set."); + } return new ValueTask(context.MainForm); } /// - public async ValueTask GetScopedFormAsync() + public T GetScopedForm() where T : Form { - await _semaphore.WaitAsync().ConfigureAwait(false); + var factory = serviceProvider.GetService(); + var scope = factory!.CreateScope(); try { - var form = await synchronizationContext - .InvokeAsync(GetScopedForm) - .ConfigureAwait(false); + var form = scope.ServiceProvider.GetRequiredService(); + form.Disposed += (_, _) => scope.Dispose(); return form; } - finally + catch { - _ = _semaphore.Release(); + scope.Dispose(); + throw; } } /// - public async ValueTask GetScopedFormAsync(IServiceScope scope) + public T GetScopedForm([NotNull] IServiceScope scope) + where T : Form + { + ArgumentNullException.ThrowIfNull(scope); + + var form = scope.ServiceProvider.GetRequiredService(); + return form; + } + + /// + public async ValueTask GetScopedFormAsync() where T : Form { await _semaphore.WaitAsync().ConfigureAwait(false); try { var form = await synchronizationContext - .InvokeAsync(GetScopedForm, scope) + .InvokeAsync(GetScopedForm) .ConfigureAwait(false); return form; } @@ -129,37 +141,21 @@ public async ValueTask GetScopedFormAsync(IServiceScope scope) } /// - public T GetScopedForm() + public async ValueTask GetScopedFormAsync(IServiceScope scope) where T : Form { - var factory = serviceProvider.GetService(); - var scope = factory!.CreateScope(); + ArgumentNullException.ThrowIfNull(scope); + await _semaphore.WaitAsync().ConfigureAwait(false); try { - var form = scope.ServiceProvider.GetService(); - - ArgumentNullException.ThrowIfNull(form); - - form.Disposed += (_, _) => scope.Dispose(); + var form = await synchronizationContext + .InvokeAsync(GetScopedForm, scope) + .ConfigureAwait(false); return form; } - catch + finally { - scope.Dispose(); - throw; + _ = _semaphore.Release(); } } - - /// - public T GetScopedForm([NotNull] IServiceScope scope) - where T : Form - { - ArgumentNullException.ThrowIfNull(scope); - - var form = scope.ServiceProvider.GetService(); - - ArgumentNullException.ThrowIfNull(form); - - return form; - } } diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/IServiceCollectionExtensions.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/IServiceCollectionExtensions.cs index 260a307..8880c49 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/IServiceCollectionExtensions.cs +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/IServiceCollectionExtensions.cs @@ -21,6 +21,8 @@ public static IServiceCollection AddWindowsFormsLifetime( return services // Add the WindowsFormsLifetime .AddSingleton() + // Add the FormularProvider + .AddSingleton() // Add the SyncronizationContext provider for WindowsForms .AddSingleton() .AddSingleton(sp => diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsLifetime.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsLifetime.cs index ab37497..72e748a 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsLifetime.cs +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsLifetime.cs @@ -21,8 +21,10 @@ ILoggerFactory loggerFactory private readonly WindowsFormsOptions _options = optionsGetter.Value; private readonly ILogger _logger = loggerFactory.CreateLogger("Microsoft.Hosting.Lifetime"); + /// public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + /// public Task WaitForStartAsync(CancellationToken cancellationToken) { if (!_options.SuppressStatusMessages) diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs index 3e97d38..76a74fe 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs @@ -2,21 +2,25 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms; +/// +[SuppressMessage( + "Usage", + "VSTHRD001:Avoid legacy thread switching APIs", + Justification = "As designed." +)] internal sealed class WindowsFormsSynchronizationContextProvider - : IWindowsFormsSynchronizationContextProvider, - IDisposable + : IWindowsFormsSynchronizationContextProvider { - private bool _disposedValue; - - public WindowsFormsSynchronizationContext Context { get; internal set; } = default!; + internal SynchronizationContext Context { get; set; } = default!; /// public void Invoke([NotNull] Action action) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); Context.Send( delegate @@ -32,6 +36,7 @@ public void Invoke([NotNull] Action action) public TResult Invoke([NotNull] Func action) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); TResult result = default!; Context.Send( @@ -41,6 +46,7 @@ public TResult Invoke([NotNull] Func action) }, null ); + return result; } @@ -49,6 +55,7 @@ public TResult Invoke([NotNull] Func action) public TResult Invoke([NotNull] Func action, TInput input) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); TResult result = default!; Context.Send( @@ -58,13 +65,18 @@ public TResult Invoke([NotNull] Func action, T }, null ); + return result; } /// - public async ValueTask InvokeAsync([NotNull] Action action) + public async ValueTask InvokeAsync( + [NotNull] Action action, + CancellationToken cancellationToken = default + ) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); var tcs = new TaskCompletionSource(); Context.Post( @@ -72,6 +84,7 @@ public async ValueTask InvokeAsync([NotNull] Action action) { try { + action(); tcs.SetResult(); } catch (Exception e) @@ -82,13 +95,17 @@ public async ValueTask InvokeAsync([NotNull] Action action) tcs ); - await tcs.Task.ConfigureAwait(true); + await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(true); } /// - public async ValueTask InvokeAsync([NotNull] Func action) + public async ValueTask InvokeAsync( + [NotNull] Func action, + CancellationToken cancellationToken = default + ) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); var tcs = new TaskCompletionSource(); Context.Post( @@ -106,16 +123,19 @@ public async ValueTask InvokeAsync([NotNull] Func act }, tcs ); - return await tcs.Task.ConfigureAwait(true); + + return await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(true); } /// public async ValueTask InvokeAsync( [NotNull] Func action, - TInput input + TInput input, + CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(action); + ArgumentNullException.ThrowIfNull(Context); var tcs = new TaskCompletionSource(); Context.Post( @@ -133,26 +153,7 @@ TInput input }, tcs ); - return await tcs.Task.ConfigureAwait(true); - } - private void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - Context.Dispose(); - } - - _disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(true); - GC.SuppressFinalize(this); + return await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(true); } } diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/README.md b/src/NetEvolve.Extensions.Hosting.WinForms/README.md index 570f4e7..a60638b 100644 --- a/src/NetEvolve.Extensions.Hosting.WinForms/README.md +++ b/src/NetEvolve.Extensions.Hosting.WinForms/README.md @@ -1 +1,58 @@ -Please give the customer a brief introduction about this library, and how to use it. +# NetEvolve.Extensions.Hosting.WinForms + +[![Nuget](https://img.shields.io/nuget/v/NetEvolve.Extensions.Hosting.WinForms?logo=nuget)](https://www.nuget.org/packages/NetEvolve.Extensions.Hosting.WinForms/) +[![Nuget](https://img.shields.io/nuget/dt/NetEvolve.Extensions.Hosting.WinForms?logo=nuget)](https://www.nuget.org/packages/NetEvolve.Extensions.Hosting.WinForms/) + +The main purpose of this package is to provide a way to use the `Microsoft.Extensions.Hosting` for WinForms applications, allowing the use of dependency injection, configuration, logging, and other features provided by the `Microsoft.Extensions` libraries. + +:bulb: This package is available for .NET 6.0 and later. + +## Installation +To use this package, you need to add the package to your project. You can do this by using the NuGet package manager or by using the dotnet CLI. +```powershell +dotnet add package NetEvolve.Extensions.Hosting.WinForms +``` + +## Usage +To use the `Microsoft.Extensions.Hosting` in a WinForms application, you just need to create a new `HostBuilder` and configure it as you would do in a console application. + +```csharp +namespace WinForms; + +using Microsoft.Extensions.Hosting; +using NetEvolve.Extensions.Hosting.WinForms; + +internal static class Program +{ + internal static async Task Main() => + await CreateHostBuilder().Build().RunAsync().ConfigureAwait(false); + + public static IHostBuilder CreateHostBuilder() => + Host.CreateDefaultBuilder().UseWindowsForms(); +} +``` + +Therefore, you can use for example the `Microsoft.Extensions.DependencyInjection` to register services and inject them into your forms. + +```csharp +namespace WinForms; + +using Microsoft.Extensions.DependencyInjection; +using System.Windows.Forms; + +public partial class Form1 : Form +{ + private readonly ILogger _logger; + + public Form1(ILogger logger) + { + _logger = logger; + InitializeComponent(); + } + + private void Form1_Load(object sender, EventArgs e) + { + _logger.LogInformation("Form loaded."); + } +} +``` \ No newline at end of file diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration/IHostBuilderExtensionsTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration/IHostBuilderExtensionsTests.cs new file mode 100644 index 0000000..6d529b2 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration/IHostBuilderExtensionsTests.cs @@ -0,0 +1,291 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Integration; + +using System; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +public class IHostBuilderExtensionsTests +{ + [Fact] + public async Task UseWindowsForms_IHostBuilder_StartForm_ConfigureNull_Expected() + { + using var host = Host.CreateDefaultBuilder().UseWindowsForms().Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + + [Fact] + public async Task UseWindowsForms_IHostBuilder_StartForm_ConfigureFine_Expected() + { + using var host = Host.CreateDefaultBuilder() + .UseWindowsForms(options => + { + options.EnableConsoleShutdown = true; + options.EnableVisualStyles = false; + }) + .Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + + [Fact] + public async Task UseWindowsForms_IHostBuilder_ApplicationContext_Expected() + { + using var host = Host.CreateDefaultBuilder() + .UseWindowsForms() + .Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + + [Fact] + public async Task UseWindowsForms_IHostBuilder_ApplicationContextFactory_Expected() + { + using var host = Host.CreateDefaultBuilder() + .UseWindowsForms(sp => new TestApplicationContext(new TestForm())) + .Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + + [Fact] + public async Task UseWindowsForms_IHostBuilder_AdvancedFactory_Expected() + { + using var host = Host.CreateDefaultBuilder() + .UseWindowsForms( + (sp, form) => new TestApplicationContext(form) + ) + .Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + +#if NET7_0_OR_GREATER + [Fact] + public async Task UseWindowsForms_HostApplicationBuilder_StartForm_ConfigureNull_Expected() + { + var builder = Host.CreateApplicationBuilder(); + _ = builder.UseWindowsForms(); + using var host = builder.Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + + [Fact] + public async Task UseWindowsForms_HostApplicationBuilder_StartForm_ConfigureFine_Expected() + { + var builder = Host.CreateApplicationBuilder(); + _ = builder.UseWindowsForms(options => + { + options.EnableConsoleShutdown = true; + options.EnableVisualStyles = false; + }); + using var host = builder.Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + + [Fact] + public async Task UseWindowsForms_HostApplicationBuilder_ApplicationContext_Expected() + { + var builder = Host.CreateApplicationBuilder(); + _ = builder.UseWindowsForms(); + using var host = builder.Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + + [Fact] + public async Task UseWindowsForms_HostApplicationBuilder_ApplicationContextFactory_Expected() + { + var builder = Host.CreateApplicationBuilder(); + _ = builder.UseWindowsForms(sp => new TestApplicationContext(new TestForm())); + using var host = builder.Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } + + [Fact] + public async Task UseWindowsForms_HostApplicationBuilder_AdvancedFactory_Expected() + { + var builder = Host.CreateApplicationBuilder(); + _ = builder.UseWindowsForms( + (sp, form) => new TestApplicationContext(form) + ); + using var host = builder.Build(); + + await host.StartAsync(); + + var provider = host.Services.GetService()!; + var mainForm = await provider.GetMainFormularAsync(); + + do + { + // This test runs too fast for the handle to be created. + // Therefore, we have to slow down a little. + await Task.Delay(15); + } while (!mainForm.IsHandleCreated); + + Assert.NotNull(mainForm); + _ = Assert.IsType(mainForm); + + await host.StopAsync(); + } +#endif + +#pragma warning disable CA1812 + private sealed class TestApplicationContext : ApplicationContext + { + public TestApplicationContext() +#pragma warning disable CA2000 // Dispose objects before losing scope + : this(new TestForm()) { } +#pragma warning restore CA2000 // Dispose objects before losing scope + + public TestApplicationContext(Form form) + : base(form) { } + } + + private sealed class TestForm : Form + { + public TestForm() => Load += (_, _) => Visible = false; + } +#pragma warning restore CA1812 +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/IHostBuilderExtensionsTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/IHostBuilderExtensionsTests.cs index faf50ca..b4929c3 100644 --- a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/IHostBuilderExtensionsTests.cs +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/IHostBuilderExtensionsTests.cs @@ -1,6 +1,7 @@ namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Unit; using System; +using System.Windows.Forms; using Microsoft.Extensions.Hosting; using NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.Internals; using NSubstitute; @@ -15,7 +16,7 @@ public void UseWindowsForms_TStartForm_IHostBuilderNull_ThrowArgumentNullExcepti _ = Assert.Throws( "builder", - () => builder.UseWindowsForms() + () => builder.UseWindowsForms() ); } @@ -37,7 +38,7 @@ public void UseWindowsForms_TApplicationContextTStartForm_IHostBuilderNull_Throw _ = Assert.Throws( "builder", - () => builder.UseWindowsForms(null!) + () => builder.UseWindowsForms(null!) ); } @@ -48,7 +49,7 @@ public void UseWindowsForms_TApplicationContextTStartForm_IHostBuilderSet_Contex _ = Assert.Throws( "contextFactory", - () => builder.UseWindowsForms(null!) + () => builder.UseWindowsForms(null!) ); } @@ -60,7 +61,7 @@ public void UseWindowsForms_TStartForm_HostApplicationBuilderNull_ThrowArgumentN _ = Assert.Throws( "builder", - () => builder!.UseWindowsForms() + () => builder!.UseWindowsForms() ); } @@ -82,7 +83,7 @@ public void UseWindowsForms_TApplicationContextTStartForm_HostApplicationBuilder _ = Assert.Throws( "builder", - () => builder.UseWindowsForms(null!) + () => builder.UseWindowsForms(null!) ); } @@ -93,8 +94,12 @@ public void UseWindowsForms_TApplicationContextTStartForm_HostApplicationBuilder _ = Assert.Throws( "contextFactory", - () => builder.UseWindowsForms(null!) + () => builder.UseWindowsForms(null!) ); } #endif + +#pragma warning disable CA1812 + private sealed class TestFormFine : Form { } +#pragma warning restore CA1812 } diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/FormularProviderTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/FormularProviderTests.cs new file mode 100644 index 0000000..70c2f84 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/FormularProviderTests.cs @@ -0,0 +1,326 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.Internals; + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.Extensions.Hosting.WinForms.Internals; +using Xunit; + +public class FormularProviderTests +{ + [Fact] + public void GetFormular_EverythingFine_Expected() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act + using var resultForm = formularProvider.GetFormular(); + + // Assert + Assert.NotNull(resultForm); + } + + [Fact] + public void GetFormular_InvalidForm_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act / Assert + _ = Assert.Throws( + () => formularProvider.GetFormular() + ); + } + + [Fact] + public async Task GetFormularAsync_EverythingFine_Expected() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act + using var resultForm = await formularProvider.GetFormularAsync(); + + // Assert + Assert.NotNull(resultForm); + } + + [Fact] + public async Task GetFormularAsync_InvalidForm_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act / Assert + _ = await Assert.ThrowsAsync( + async () => await formularProvider.GetFormularAsync() + ); + } + + [Fact] + public void GetScopedForm_EverythingFine_Expected() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act + using var resultForm = formularProvider.GetScopedForm(); + + // Assert + Assert.NotNull(resultForm); + } + + [Fact] + public void GetScopedForm_InvalidForm_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act / Assert + _ = Assert.Throws( + () => formularProvider.GetScopedForm() + ); + } + + [Fact] + public async Task GetScopedFormAsync_EverythingFine_Expected() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act + using var resultForm = await formularProvider.GetScopedFormAsync(); + + // Assert + Assert.NotNull(resultForm); + } + + [Fact] + public async Task GetScopedFormAsync_InvalidForm_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act / Assert + _ = await Assert.ThrowsAsync( + async () => await formularProvider.GetScopedFormAsync() + ); + } + + [Fact] + public void GetScopedForm_WithScope_EverythingFine_Expected() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + using var scope = serviceProvider.CreateScope(); + + // Act + using var resultForm = formularProvider.GetScopedForm(scope); + + // Assert + Assert.NotNull(resultForm); + } + + [Fact] + public void GetScopedForm_WithScope_InvalidForm_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + using var scope = serviceProvider.CreateScope(); + + // Act / Assert + _ = Assert.Throws( + () => formularProvider.GetScopedForm(scope) + ); + } + + [Fact] + public async Task GetScopedFormAsync_WithScope_EverythingFine_Expected() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + using var scope = serviceProvider.CreateScope(); + + // Act + using var resultForm = await formularProvider.GetScopedFormAsync(scope); + + // Assert + Assert.NotNull(resultForm); + } + + [Fact] + public async Task GetScopedFormAsync_WithScope_InvalidForm_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection().AddScoped(); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + using var scope = serviceProvider.CreateScope(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + async () => await formularProvider.GetScopedFormAsync(scope) + ); + } + + [Fact] + public void GetMainFormular_EverythingFine_Expected() + { + // Arrange + var services = new ServiceCollection() + .AddScoped() + .AddSingleton(sp => new ApplicationContext(sp.GetRequiredService())); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act + using var resultForm = formularProvider.GetMainFormular(); + + // Assert + Assert.NotNull(resultForm); + } + + [Fact] + public void GetMainFormular_InvalidForm_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection() + .AddScoped() + .AddSingleton(sp => new ApplicationContext()); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act / Assert + _ = Assert.Throws(() => formularProvider.GetMainFormular()); + } + + [Fact] + public async Task GetMainFormularAsync_EverythingFine_Expected() + { + // Arrange + var services = new ServiceCollection() + .AddScoped() + .AddSingleton(sp => new ApplicationContext(sp.GetRequiredService())); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act + using var resultForm = await formularProvider.GetMainFormularAsync(); + + // Assert + Assert.NotNull(resultForm); + } + + [Fact] + public async Task GetMainFormularAsync_InvalidForm_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection() + .AddScoped() + .AddSingleton(sp => new ApplicationContext()); + var serviceProvider = services.BuildServiceProvider(); + var synchronizationContext = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + using var formularProvider = new FormularProvider(serviceProvider, synchronizationContext); + + // Act / Assert + _ = await Assert.ThrowsAsync( + async () => await formularProvider.GetMainFormularAsync() + ); + } + +#pragma warning disable CA1812 +#pragma warning disable S2094 // Classes should not be empty + private sealed class TestFormFine : Form { } + + private sealed class TestFormNotRegistered : Form { } +#pragma warning restore S2094 // Classes should not be empty +#pragma warning restore CA1812 +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/IServiceCollectionExtensionsTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/IServiceCollectionExtensionsTests.cs index 6ba7098..bfab398 100644 --- a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/IServiceCollectionExtensionsTests.cs +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/IServiceCollectionExtensionsTests.cs @@ -32,9 +32,9 @@ public void AddWindowsFormsLifetime_ConfigurationNull_Expected( public static TheoryData?> AddWindowsFormsData => new TheoryData?> { - { 4, null }, + { 5, null }, { - 10, + 11, options => { options.EnableConsoleShutdown = true; diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.cs deleted file mode 100644 index a356ce0..0000000 --- a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.Internals; - -using System.Windows.Forms; - -#pragma warning disable CA1812 -internal sealed partial class TestForm : Form { } -#pragma warning restore CA1812 diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/WindowsFormsSynchronizationContextProviderTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/WindowsFormsSynchronizationContextProviderTests.cs new file mode 100644 index 0000000..c967d5f --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/WindowsFormsSynchronizationContextProviderTests.cs @@ -0,0 +1,383 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.Internals; + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using NetEvolve.Extensions.Hosting.WinForms.Internals; +using Xunit; + +public class WindowsFormsSynchronizationContextProviderTests +{ + [Fact] + public void Invoke_ActionNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws("action", () => provider.Invoke(null!)); + } + + [Fact] + public void Invoke_Action_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws("Context", () => provider.Invoke(() => { })); + } + + [Fact] + public void Invoke_Action_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act / Assert + provider.Invoke(() => { }); + + Assert.True(true, "No exception was thrown."); + } + + [Fact] + public void Invoke_FuncNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws("action", () => provider.Invoke((Func)null!)); + } + + [Fact] + public void Invoke_Func_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws("Context", () => provider.Invoke(() => 42)); + } + + [Fact] + public void Invoke_Func_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + var result = provider.Invoke(() => 42); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public void Invoke_FuncWithInputNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws( + "action", + () => provider.Invoke((Func)null!, 42) + ); + } + + [Fact] + public void Invoke_FuncWithInput_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = Assert.Throws( + "Context", + () => provider.Invoke((int input) => input * 2, 21) + ); + } + + [Fact] + public void Invoke_FuncWithInput_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + var result = provider.Invoke((int input) => input * 2, 21); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public async Task InvokeAsync_ActionNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "action", + async () => await provider.InvokeAsync(null!) + ); + } + + [Fact] + public async Task InvokeAsync_Action_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "Context", + async () => await provider.InvokeAsync(() => { }) + ); + } + + [Fact] + public async Task InvokeAsync_ActionThrows_ExpectedException() + { // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => + await provider.InvokeAsync(() => + { + throw new NotImplementedException(); + }) + ); + } + + [Fact] + public async Task InvokeAsync_Action_CancellationTokenCanceled_ThrowsTaskCanceledException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => await provider.InvokeAsync(() => { }, new CancellationToken(true)) + ); + } + + [Fact] + public async Task InvokeAsync_Action_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + await provider.InvokeAsync(() => { }); + + // Assert + Assert.True(true, "No exception was thrown."); + } + + [Fact] + public async Task InvokeAsync_FuncNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "action", + async () => await provider.InvokeAsync((Func)null!) + ); + } + + [Fact] + public async Task InvokeAsync_Func_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "Context", + async () => await provider.InvokeAsync(() => 42) + ); + } + + [Fact] + public async Task InvokeAsync_FuncThrows_ExpectedException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => + await provider.InvokeAsync(() => + { + throw new NotImplementedException(); + }) + ); + } + + [Fact] + public async Task InvokeAsync_Func_CancellationTokenCanceled_ThrowsTaskCanceledException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => await provider.InvokeAsync(() => 42, new CancellationToken(true)) + ); + } + + [Fact] + public async Task InvokeAsync_Func_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + var result = await provider.InvokeAsync(() => 42); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public async Task InvokeAsync_FuncWithInputNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "action", + async () => await provider.InvokeAsync((Func)null!, 42) + ); + } + + [Fact] + public async Task InvokeAsync_FuncWithInput_ContextNull_ThrowsArgumentNullException() + { + // Arrange + var provider = new WindowsFormsSynchronizationContextProvider(); + + // Act / Assert + _ = await Assert.ThrowsAsync( + "Context", + async () => await provider.InvokeAsync((int input) => input * 2, 21) + ); + } + + [Fact] + public async Task InvokeAsync_FuncWithInputThrows_ExpectedException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => + await provider.InvokeAsync( + (int input) => + { + throw new NotImplementedException(); + }, + 42 + ) + ); + } + + [Fact] + public async Task InvokeAsync_FuncWithInput_CancellationTokenCanceled_ThrowsTaskCanceledException() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + _ = await Assert.ThrowsAsync( + async () => + await provider.InvokeAsync( + (int input) => input * 2, + 21, + new CancellationToken(true) + ) + ); + } + + [Fact] + public async Task InvokeAsync_FuncWithInput_Expected() + { + // Arrange + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + var provider = new WindowsFormsSynchronizationContextProvider + { + Context = SynchronizationContext.Current! + }; + + // Act + var result = await provider.InvokeAsync((int input) => input * 2, 21); + + // Assert + Assert.Equal(42, result); + } +} diff --git a/xample/Xample.Simple/Form1.Designer.cs b/xample/Xample.Simple/Form1.Designer.cs new file mode 100644 index 0000000..5008edd --- /dev/null +++ b/xample/Xample.Simple/Form1.Designer.cs @@ -0,0 +1,45 @@ +namespace Xample.Simple +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + SuspendLayout(); + // + // Form1 + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(800, 450); + Name = "Form1"; + Text = "Form1"; + ResumeLayout(false); + } + + #endregion + } +} diff --git a/xample/Xample.Simple/Form1.cs b/xample/Xample.Simple/Form1.cs new file mode 100644 index 0000000..5251efe --- /dev/null +++ b/xample/Xample.Simple/Form1.cs @@ -0,0 +1,19 @@ +namespace Xample.Simple; + +using Microsoft.Extensions.Logging; + +internal partial class Form1 : Form +{ + private readonly ILogger _logger; + + public Form1(ILogger logger) + { + _logger = logger; + InitializeComponent(); + + Form1Created(); + } + + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "From1 created.")] + public partial void Form1Created(); +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.resx b/xample/Xample.Simple/Form1.resx similarity index 93% rename from tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.resx rename to xample/Xample.Simple/Form1.resx index 1af7de1..af32865 100644 --- a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.resx +++ b/xample/Xample.Simple/Form1.resx @@ -1,17 +1,17 @@  - diff --git a/xample/Xample.Simple/Program.cs b/xample/Xample.Simple/Program.cs new file mode 100644 index 0000000..e75d079 --- /dev/null +++ b/xample/Xample.Simple/Program.cs @@ -0,0 +1,13 @@ +namespace Xample.Simple; + +using Microsoft.Extensions.Hosting; +using NetEvolve.Extensions.Hosting.WinForms; + +internal static class Program +{ + internal static async Task Main() => + await CreateHostBuilder().Build().RunAsync().ConfigureAwait(false); + + public static IHostBuilder CreateHostBuilder() => + Host.CreateDefaultBuilder().UseWindowsForms(); +} diff --git a/xample/Xample.Simple/Xample.Simple.csproj b/xample/Xample.Simple/Xample.Simple.csproj new file mode 100644 index 0000000..0f3f027 --- /dev/null +++ b/xample/Xample.Simple/Xample.Simple.csproj @@ -0,0 +1,15 @@ + + + + WinExe + $(_TargetFrameworks) + enable + true + enable + + + + + + + \ No newline at end of file