From 93819c9cf13709ba9d3b4552de81f95d3836c984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 25 Mar 2024 20:29:05 +0100 Subject: [PATCH] feat: Implementation (#1) * feat: Implementation * build(deps): Updated NuGet packages * build: Updated `eng` * chore: Extended Tests * chore: Added Package props * chore: Updated xml documentation * chore: Reverted changes on Dependabot.yml --- .github/template-sync.yml | 29 -- .github/workflows/cicd.yml | 4 +- .github/workflows/template-sync.yml | 24 -- Directory.Build.props | 12 + Directory.Packages.props | 31 +- Extensions.Hosting.WinForms.sln | 68 ++++ eng | 2 +- new-project.ps1 | 2 +- new-solution.ps1 | 17 - .../IFormularProvider.cs | 30 ++ .../IHostBuilderExtensions.cs | 301 ++++++++++++++++++ ...dowsFormsSynchronizationContextProvider.cs | 25 ++ .../Internals/FormularProvider.cs | 165 ++++++++++ .../Internals/IServiceCollectionExtensions.cs | 31 ++ .../Internals/WindowsFormsHostedService.cs | 105 ++++++ .../Internals/WindowsFormsLifetime.cs | 134 ++++++++ ...dowsFormsSynchronizationContextProvider.cs | 158 +++++++++ ...tEvolve.Extensions.Hosting.WinForms.csproj | 14 + .../README.md | 1 + .../WindowsFormsOptions.cs | 39 +++ .../Internals/NamespaceTests.cs | 38 +++ .../NamespaceTests.cs | 37 +++ ...Hosting.WinForms.Tests.Architecture.csproj | 25 ++ .../ProjectArchitecture.cs | 25 ++ ....Hosting.WinForms.Tests.Integration.csproj | 24 ++ .../IHostBuilderExtensionsTests.cs | 100 ++++++ .../IServiceCollectionExtensionsTests.cs | 44 +++ .../Internals/TestApplicatonContext.cs | 7 + .../Internals/TestForm.cs | 7 + .../Internals/TestForm.resx | 120 +++++++ ...ensions.Hosting.WinForms.Tests.Unit.csproj | 25 ++ 31 files changed, 1557 insertions(+), 87 deletions(-) delete mode 100644 .github/template-sync.yml delete mode 100644 .github/workflows/template-sync.yml create mode 100644 Extensions.Hosting.WinForms.sln delete mode 100644 new-solution.ps1 create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/IFormularProvider.cs create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/IHostBuilderExtensions.cs create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/IWindowsFormsSynchronizationContextProvider.cs create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/Internals/FormularProvider.cs create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/Internals/IServiceCollectionExtensions.cs create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsHostedService.cs create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsLifetime.cs create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/NetEvolve.Extensions.Hosting.WinForms.csproj create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/README.md create mode 100644 src/NetEvolve.Extensions.Hosting.WinForms/WindowsFormsOptions.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/Internals/NamespaceTests.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/NamespaceTests.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture.csproj create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/ProjectArchitecture.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration.csproj create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/IHostBuilderExtensionsTests.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/IServiceCollectionExtensionsTests.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestApplicatonContext.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.cs create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.resx create mode 100644 tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.csproj diff --git a/.github/template-sync.yml b/.github/template-sync.yml deleted file mode 100644 index 6265aa4..0000000 --- a/.github/template-sync.yml +++ /dev/null @@ -1,29 +0,0 @@ -additional: - - "arguments" - - "analyzer" - - "article.benchmarks" - - "describe" - - "develop" - - "extensions.strings" - - "extensions.tasks" - - "extensions.test" - - "guard" - - "healthchecks" - - "http.correlation" - - "trydispose" - -files: - - "!**/*" - - ".editorconfig" - - ".gitignore" - - ".github/CODEOWNERS" - - ".github/dependabot.yml" - - ".github/release-drafter.yml" - - ".github/ISSUE_TEMPLATE/**/*" - - ".github/PULL_REQUEST_TEMPLATE/**/*" - - ".github/workflows/update-license.yml" - - # you probably want to exclude these files: - - "!.github/workflows/dependabot-merge.yml" - - "!.github/workflows/template-sync.yml" - - "!.github/template-sync.yml" diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 82c1115..9f563a8 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -26,5 +26,7 @@ jobs: dotnet-logging: ${{ inputs.dotnet-logging }} dotnet-version: | 7.x - solution: ###SOLUTION### + solution: ./Extensions.Hosting.WinForms.sln + runs-on-build: windows-latest + runs-on-tests: windows-latest secrets: inherit diff --git a/.github/workflows/template-sync.yml b/.github/workflows/template-sync.yml deleted file mode 100644 index 07adb4e..0000000 --- a/.github/workflows/template-sync.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Update template files - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - template-sync: - if: github.actor != 'dependabot[bot]' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: ahmadnassri/action-template-repository-sync@v2.5.0 - with: - github-token: ${{ secrets.TEMPLATE_SYNC }} - dry-run: false - skip-ci: true diff --git a/Directory.Build.props b/Directory.Build.props index 2958b68..49fc93d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,4 +9,16 @@ + + <_TargetFrameworks>net6.0-windows;net7.0-windows;net8.0-windows + + + + $(MSBuildProjectName) + .NET Hosting infrastructure for Windows Forms. + https://github.com/dailydevops/extensions.hosting.winforms.git + https://github.com/dailydevops/extensions.hosting.winforms + hosting;winforms + + diff --git a/Directory.Packages.props b/Directory.Packages.props index ff6e661..cc7e894 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,29 +1,32 @@ - true true - - + - - + + - - - - + + + + + + + + + + + + - - - - + + - diff --git a/Extensions.Hosting.WinForms.sln b/Extensions.Hosting.WinForms.sln new file mode 100644 index 0000000..05fd95a --- /dev/null +++ b/Extensions.Hosting.WinForms.sln @@ -0,0 +1,68 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{653A4058-FE7E-4E7D-8F48-CA19874137FD}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .filenesting.json = .filenesting.json + .gitignore = .gitignore + .gitmodules = .gitmodules + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + Directory.Solution.props = Directory.Solution.props + GitVersion.yml = GitVersion.yml + LICENSE = LICENSE + new-project.ps1 = new-project.ps1 + nuget.config = nuget.config + README.md = README.md + update-solution.ps1 = update-solution.ps1 + EndProjectSection +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}" +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}" +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}" +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}" +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 + {5501B170-5917-4568-9EAC-8853D14238A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5501B170-5917-4568-9EAC-8853D14238A1}.Release|Any CPU.Build.0 = Release|Any CPU + {F7BA85AD-CBD1-4F99-BC28-42DF1253BAE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7BA85AD-CBD1-4F99-BC28-42DF1253BAE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7BA85AD-CBD1-4F99-BC28-42DF1253BAE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7BA85AD-CBD1-4F99-BC28-42DF1253BAE8}.Release|Any CPU.Build.0 = Release|Any CPU + {22F991BF-06C3-4B35-BCA2-DD61959C187F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22F991BF-06C3-4B35-BCA2-DD61959C187F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22F991BF-06C3-4B35-BCA2-DD61959C187F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22F991BF-06C3-4B35-BCA2-DD61959C187F}.Release|Any CPU.Build.0 = Release|Any CPU + {DF47FD5F-2F7F-470B-98A2-9C5266AA0FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + 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} + EndGlobalSection +EndGlobal diff --git a/eng b/eng index 4b6ef52..11a8337 160000 --- a/eng +++ b/eng @@ -1 +1 @@ -Subproject commit 4b6ef52264c9865f0330757dba0bc1d86fb3a58f +Subproject commit 11a83379b2757b40289f86bd4654b4ae8d9c0b32 diff --git a/new-project.ps1 b/new-project.ps1 index fe28792..cbdfc08 100644 --- a/new-project.ps1 +++ b/new-project.ps1 @@ -49,7 +49,7 @@ New-Project ` -DisableTests $DisableTests ` -DisableUnitTests $DisableUnitTests ` -DisableIntegrationTests $DisableIntegrationTests ` - -SolutionFile "###SOLUTION###" ` + -SolutionFile "./Extensions.Hosting.WinForms.sln" ` -OutputDirectory (Get-Location) ` -EnableProjectGrouping $EnableProjectGrouping ` -DisableArchitectureTests $DisableArchitectureTests diff --git a/new-solution.ps1 b/new-solution.ps1 deleted file mode 100644 index 0c57ad1..0000000 --- a/new-solution.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -[CmdletBinding()] -param ( - # Name of the solution to be created. - [Parameter(Mandatory = $true)] - [string] - $SolutionName -) - -Write-Output "Updating submodules ..." -git submodule update --init --recursive --remote | Out-Null - -Write-Output "Creating $SolutionName.sln ..." -$location = Get-Location -. .\eng\scripts\new-solution.ps1 - -New-Solution -SolutionName $SolutionName -Output $location -Remove-Item -Path "$location\new-solution.ps1" -Force diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/IFormularProvider.cs b/src/NetEvolve.Extensions.Hosting.WinForms/IFormularProvider.cs new file mode 100644 index 0000000..5c418aa --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/IFormularProvider.cs @@ -0,0 +1,30 @@ +namespace NetEvolve.Extensions.Hosting.WinForms; + +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; + +public interface IFormularProvider +{ + T GetFormular() + where T : Form; + + ValueTask GetFormularAsync() + where T : Form; + + Form GetMainFormular(); + + ValueTask
GetMainFormularAsync(); + + ValueTask GetScopedFormAsync() + where T : Form; + + ValueTask GetScopedFormAsync(IServiceScope scope) + where T : Form; + + T GetScopedForm() + where T : Form; + + T GetScopedForm(IServiceScope scope) + where T : Form; +} diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/IHostBuilderExtensions.cs b/src/NetEvolve.Extensions.Hosting.WinForms/IHostBuilderExtensions.cs new file mode 100644 index 0000000..68ec520 --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/IHostBuilderExtensions.cs @@ -0,0 +1,301 @@ +namespace NetEvolve.Extensions.Hosting.WinForms; + +using System; +using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; +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 +/// +/// Extension methods for or to configure Windows Forms Lifetime. +/// +#else +/// +/// Extension methods for to configure Windows Forms Lifetime. +/// +#endif +public static class IHostBuilderExtensions +{ + /// + /// 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 IHostBuilder UseWindowsForms( + this IHostBuilder builder, + Action? configure = null + ) + where TStartForm : Form + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.ConfigureServices(services => + services + // Add the start form + .AddSingleton() + .AddSingleton(sp => new ApplicationContext(sp.GetRequiredService())) + // Default WindowsForms services + .AddWindowsFormsLifetime(configure) + ); + } + + /// + /// 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 IHostBuilder UseWindowsForms( + this IHostBuilder builder, + Func? contextFactory = null, + Action? configure = null + ) + where TApplicationContext : ApplicationContext + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.ConfigureServices(services => + { + services = contextFactory is null + ? services.AddSingleton() + : services.AddSingleton(sp => contextFactory.Invoke(sp)); + + _ = services + .AddSingleton() + // Default WindowsForms services + .AddWindowsFormsLifetime(configure); + }); + } + + /// + /// 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 IHostBuilder UseWindowsForms( + this IHostBuilder builder, + Func contextFactory, + Action? configure = null + ) + where TApplicationContext : ApplicationContext + where TStartForm : Form + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(contextFactory); + + return builder.ConfigureServices(services => + services + .AddSingleton() + .AddSingleton(sp => + { + var startForm = sp.GetRequiredService(); + return contextFactory(sp, startForm); + }) + .AddSingleton(sp => + sp.GetRequiredService() + ) + // Default WindowsForms services + .AddWindowsFormsLifetime(configure) + ); + } + +#if NET7_0 + /// + /// 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 HostApplicationBuilder UseWindowsForms( + this HostApplicationBuilder 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 HostApplicationBuilder UseWindowsForms( + this HostApplicationBuilder 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.Invoke(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 HostApplicationBuilder UseWindowsForms( + this HostApplicationBuilder 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 + +#if NET8_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. + /// + /// 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 new file mode 100644 index 0000000..a49a4df --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/IWindowsFormsSynchronizationContextProvider.cs @@ -0,0 +1,25 @@ +namespace NetEvolve.Extensions.Hosting.WinForms; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +public interface IWindowsFormsSynchronizationContextProvider +{ + void Invoke([NotNull] Action action); + + [return: MaybeNull] + TResult Invoke([NotNull] Func action); + + [return: MaybeNull] + TResult Invoke([NotNull] Func action, TInput input); + + ValueTask InvokeAsync([NotNull] Action action); + + ValueTask InvokeAsync([NotNull] Func action); + + ValueTask InvokeAsync( + [NotNull] Func action, + TInput input + ); +} diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/FormularProvider.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/FormularProvider.cs new file mode 100644 index 0000000..00412b0 --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/FormularProvider.cs @@ -0,0 +1,165 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Internals; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; + +internal sealed class FormularProvider( + IServiceProvider serviceProvider, + IWindowsFormsSynchronizationContextProvider synchronizationContext +) : IFormularProvider, IDisposable +{ + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private bool _disposedValue; + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _semaphore.Dispose(); + } + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public T GetFormular() + where T : Form + { + _semaphore.Wait(); + try + { + var form = synchronizationContext.Invoke(() => serviceProvider.GetService()); + ArgumentNullException.ThrowIfNull(form); + return form; + } + finally + { + _ = _semaphore.Release(); + } + } + + /// + public async ValueTask GetFormularAsync() + where T : Form + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + var form = await synchronizationContext + .InvokeAsync(() => serviceProvider.GetService()) + .ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(form); + return form; + } + finally + { + _ = _semaphore.Release(); + } + } + + /// + public Form GetMainFormular() + { + var context = serviceProvider.GetService(); + + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(context.MainForm); + + return context.MainForm; + } + + /// + public ValueTask GetMainFormularAsync() + { + var context = serviceProvider.GetService(); + + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(context.MainForm); + + return new ValueTask(context.MainForm); + } + + /// + public async ValueTask GetScopedFormAsync() + where T : Form + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + var form = await synchronizationContext + .InvokeAsync(GetScopedForm) + .ConfigureAwait(false); + return form; + } + finally + { + _ = _semaphore.Release(); + } + } + + /// + public async ValueTask GetScopedFormAsync(IServiceScope scope) + where T : Form + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + var form = await synchronizationContext + .InvokeAsync(GetScopedForm, scope) + .ConfigureAwait(false); + return form; + } + finally + { + _ = _semaphore.Release(); + } + } + + /// + public T GetScopedForm() + where T : Form + { + var factory = serviceProvider.GetService(); + var scope = factory!.CreateScope(); + try + { + var form = scope.ServiceProvider.GetService(); + + ArgumentNullException.ThrowIfNull(form); + + form.Disposed += (_, _) => scope.Dispose(); + return form; + } + catch + { + scope.Dispose(); + throw; + } + } + + /// + 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 new file mode 100644 index 0000000..260a307 --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/IServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Internals; + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +internal static class IServiceCollectionExtensions +{ + public static IServiceCollection AddWindowsFormsLifetime( + this IServiceCollection services, + Action? configure + ) + { + ArgumentNullException.ThrowIfNull(services); + + if (configure is not null) + { + _ = services.Configure(configure); + } + + return services + // Add the WindowsFormsLifetime + .AddSingleton() + // Add the SyncronizationContext provider for WindowsForms + .AddSingleton() + .AddSingleton(sp => + sp.GetRequiredService() + ) + .AddHostedService(); + } +} diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsHostedService.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsHostedService.cs new file mode 100644 index 0000000..dbbd148 --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsHostedService.cs @@ -0,0 +1,105 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Internals; + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +internal sealed class WindowsFormsHostedService( + IOptions options, + IHostApplicationLifetime applicationLifetime, + IServiceProvider serviceProvider, + WindowsFormsSynchronizationContextProvider synchronizationContextProvider +) : IHostedService, IDisposable +{ + private CancellationTokenRegistration _cancellationTokenRegistration; + private readonly WindowsFormsOptions _options = options.Value; + private bool _disposedValue; + + public Task StartAsync(CancellationToken cancellationToken) + { + _cancellationTokenRegistration = applicationLifetime.ApplicationStopping.Register( + state => + { + if (state is WindowsFormsHostedService hostedService) + { + hostedService.OnApplicationStopping(); + } + }, + this + ); + + var uiThread = new Thread(StartUIThread); + uiThread.SetApartmentState(ApartmentState.STA); + uiThread.Name = "Hosting UI Thread"; + uiThread.Start(); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + Application.ApplicationExit -= OnApplicationExit; + _cancellationTokenRegistration.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void OnApplicationExit(object? sender, EventArgs e) => + applicationLifetime.StopApplication(); + + private void OnApplicationStopping() + { + var applicationContext = serviceProvider.GetService(); + var mainForm = applicationContext?.MainForm; + + // If the main form is not null and the handle is created, close and dispose it. + if (mainForm is not null && mainForm.IsHandleCreated) + { + mainForm.Invoke(() => + { + mainForm.Close(); + mainForm.Dispose(); + }); + } + } + + private void StartUIThread() + { + _ = Application.SetHighDpiMode(_options.HighDpiMode); + if (_options.EnableVisualStyles) + { + Application.EnableVisualStyles(); + } + Application.SetCompatibleTextRenderingDefault(_options.CompatibleTextRenderingDefault); + Application.ApplicationExit += OnApplicationExit; + + // Disable the auto install of the WindowsFormsSynchronizationContext. + WindowsFormsSynchronizationContext.AutoInstall = false; + + // Create the WindowsFormsSynchronizationContext and set it as the current synchronization context. + synchronizationContextProvider.Context = new WindowsFormsSynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(synchronizationContextProvider.Context); + + var applicationContext = serviceProvider.GetRequiredService(); + + Application.Run(applicationContext); + } +} diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsLifetime.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsLifetime.cs new file mode 100644 index 0000000..ab37497 --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsLifetime.cs @@ -0,0 +1,134 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Internals; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +internal sealed partial class WindowsFormsLifetime( + IOptions optionsGetter, + IHostEnvironment environment, + IHostApplicationLifetime applicationLifetime, + ILoggerFactory loggerFactory +) : IHostLifetime, IDisposable +{ + private CancellationTokenRegistration _applicationStarted; + private CancellationTokenRegistration _applicationStopping; + private bool _disposedValue; + + 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) + { + _applicationStarted = applicationLifetime.ApplicationStarted.Register( + state => + { + if (state is WindowsFormsLifetime lifetime) + { + lifetime.OnApplicationStarted(); + } + }, + this + ); + _applicationStopping = applicationLifetime.ApplicationStopping.Register( + state => + { + if (state is WindowsFormsLifetime lifetime) + { + lifetime.OnApplicationStopping(); + } + }, + this + ); + } + + if (_options.EnableConsoleShutdown) + { + Console.CancelKeyPress += OnCancelKeyPress; + } + + return Task.CompletedTask; + } + + private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) + { + e.Cancel = true; + applicationLifetime.StopApplication(); + } + + private void OnApplicationStopping() => LogShuttingDown(_logger); + + private void OnApplicationStarted() + { + Action logAction = _options.EnableConsoleShutdown + ? LogStartedWithConsoleShutdown + : LogStarted; + logAction(_logger); + + LogStartedDetails(_logger, environment.EnvironmentName, environment.ContentRootPath); + } + + [LoggerMessage(1, LogLevel.Information, "Application is shutting down...")] + private static partial void LogShuttingDown(ILogger logger); + + [LoggerMessage( + 2, + LogLevel.Information, + "Application started. Close the startup Form to shut down." + )] + private static partial void LogStarted(ILogger logger); + + [LoggerMessage( + 3, + LogLevel.Information, + "Application started. Close the startup Form or press CTRL+C to shut down." + )] + private static partial void LogStartedWithConsoleShutdown(ILogger logger); + + [LoggerMessage( + 4, + LogLevel.Debug, + """ + Hosting environment: {EnvironmentName} + Content root path: {ContentRootPath} + """ + )] + private static partial void LogStartedDetails( + ILogger logger, + string environmentName, + string contentRootPath + ); + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _applicationStarted.Dispose(); + _applicationStopping.Dispose(); + + if (_options.EnableConsoleShutdown) + { + Console.CancelKeyPress -= OnCancelKeyPress; + } + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs new file mode 100644 index 0000000..3e97d38 --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/Internals/WindowsFormsSynchronizationContextProvider.cs @@ -0,0 +1,158 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Internals; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using System.Windows.Forms; + +internal sealed class WindowsFormsSynchronizationContextProvider + : IWindowsFormsSynchronizationContextProvider, + IDisposable +{ + private bool _disposedValue; + + public WindowsFormsSynchronizationContext Context { get; internal set; } = default!; + + /// + public void Invoke([NotNull] Action action) + { + ArgumentNullException.ThrowIfNull(action); + + Context.Send( + delegate + { + action(); + }, + null + ); + } + + /// + [return: MaybeNull] + public TResult Invoke([NotNull] Func action) + { + ArgumentNullException.ThrowIfNull(action); + + TResult result = default!; + Context.Send( + delegate + { + result = action(); + }, + null + ); + return result; + } + + /// + [return: MaybeNull] + public TResult Invoke([NotNull] Func action, TInput input) + { + ArgumentNullException.ThrowIfNull(action); + + TResult result = default!; + Context.Send( + delegate + { + result = action(input); + }, + null + ); + return result; + } + + /// + public async ValueTask InvokeAsync([NotNull] Action action) + { + ArgumentNullException.ThrowIfNull(action); + + var tcs = new TaskCompletionSource(); + Context.Post( + delegate + { + try + { + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }, + tcs + ); + + await tcs.Task.ConfigureAwait(true); + } + + /// + public async ValueTask InvokeAsync([NotNull] Func action) + { + ArgumentNullException.ThrowIfNull(action); + + var tcs = new TaskCompletionSource(); + Context.Post( + delegate + { + try + { + var result = action(); + tcs.SetResult(result); + } + catch (Exception e) + { + tcs.SetException(e); + } + }, + tcs + ); + return await tcs.Task.ConfigureAwait(true); + } + + /// + public async ValueTask InvokeAsync( + [NotNull] Func action, + TInput input + ) + { + ArgumentNullException.ThrowIfNull(action); + + var tcs = new TaskCompletionSource(); + Context.Post( + delegate + { + try + { + var result = action(input); + tcs.SetResult(result); + } + catch (Exception e) + { + tcs.SetException(e); + } + }, + 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); + } +} diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/NetEvolve.Extensions.Hosting.WinForms.csproj b/src/NetEvolve.Extensions.Hosting.WinForms/NetEvolve.Extensions.Hosting.WinForms.csproj new file mode 100644 index 0000000..1deba9c --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/NetEvolve.Extensions.Hosting.WinForms.csproj @@ -0,0 +1,14 @@ + + + + $(_TargetFrameworks) + + true + + + + + + + + diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/README.md b/src/NetEvolve.Extensions.Hosting.WinForms/README.md new file mode 100644 index 0000000..570f4e7 --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/README.md @@ -0,0 +1 @@ +Please give the customer a brief introduction about this library, and how to use it. diff --git a/src/NetEvolve.Extensions.Hosting.WinForms/WindowsFormsOptions.cs b/src/NetEvolve.Extensions.Hosting.WinForms/WindowsFormsOptions.cs new file mode 100644 index 0000000..5b25b77 --- /dev/null +++ b/src/NetEvolve.Extensions.Hosting.WinForms/WindowsFormsOptions.cs @@ -0,0 +1,39 @@ +namespace NetEvolve.Extensions.Hosting.WinForms; + +using System.Windows.Forms; + +/// +/// Options for the Windows Forms host. +/// +public sealed class WindowsFormsOptions +{ + /// + /// Indicates the . + /// The default is . + /// + public HighDpiMode HighDpiMode { get; set; } = HighDpiMode.SystemAware; + + /// + /// Indicates if visual styles are enabled. + /// The default is . + /// + public bool EnableVisualStyles { get; set; } = true; + + /// + /// Indicates if compatible text rendering is enabled. + /// The default is . + /// + public bool CompatibleTextRenderingDefault { get; set; } + + /// + /// Indicates if host lifetime status messages should be supressed such as on startup. + /// The default is . + /// + public bool SuppressStatusMessages { get; set; } + + /// + /// Enables listening for Ctrl+C to additionally initiate shutdown. + /// The default is . + /// + public bool EnableConsoleShutdown { get; set; } +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/Internals/NamespaceTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/Internals/NamespaceTests.cs new file mode 100644 index 0000000..c7604e6 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/Internals/NamespaceTests.cs @@ -0,0 +1,38 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture.Internals; + +using ArchUnitNET.Domain; +using ArchUnitNET.xUnit; +using NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture; +using Xunit; +using static ArchUnitNET.Fluent.ArchRuleDefinition; + +public class NamespaceTests +{ + private static readonly IObjectProvider _objects = Types() + .That() + .ResideInNamespace("NetEvolve.Extensions.Hosting.WinForms.Internals"); + + [Fact] + public void Classes_should_be_Internal() + { + var rule = Classes().That().Are(_objects).Should().BeInternal(); + + rule.Check(ProjectArchitecture.Instance); + } + + [Fact] + public void Classes_should_be_Sealed_Or_Static() + { + var rule = Classes().That().Are(_objects).Should().BeSealed(); + + rule.Check(ProjectArchitecture.Instance); + } + + [Fact] + public void Interfaces_should_be_public() + { + var rule = Interfaces().That().Are(_objects).Should().BePublic(); + + rule.Check(ProjectArchitecture.Instance); + } +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/NamespaceTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/NamespaceTests.cs new file mode 100644 index 0000000..8c90bef --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/NamespaceTests.cs @@ -0,0 +1,37 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture; + +using ArchUnitNET.Domain; +using ArchUnitNET.xUnit; +using Xunit; +using static ArchUnitNET.Fluent.ArchRuleDefinition; + +public class NamespaceTests +{ + private static readonly IObjectProvider _objects = Types() + .That() + .ResideInNamespace("NetEvolve.Extensions.Hosting.WinForms"); + + [Fact] + public void Classes_should_be_public() + { + var rule = Classes().That().Are(_objects).Should().BePublic(); + + rule.Check(ProjectArchitecture.Instance); + } + + [Fact] + public void Classes_should_be_Sealed_Or_Static() + { + var rule = Classes().That().Are(_objects).Should().BeSealed(); + + rule.Check(ProjectArchitecture.Instance); + } + + [Fact] + public void Interfaces_should_be_public() + { + var rule = Interfaces().That().Are(_objects).Should().BePublic(); + + rule.Check(ProjectArchitecture.Instance); + } +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture.csproj b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture.csproj new file mode 100644 index 0000000..9a0811d --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture.csproj @@ -0,0 +1,25 @@ + + + + $(_TargetFrameworks) + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/ProjectArchitecture.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/ProjectArchitecture.cs new file mode 100644 index 0000000..7490cc0 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture/ProjectArchitecture.cs @@ -0,0 +1,25 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Architecture; + +using System; +using System.Threading; +using ArchUnitNET.Domain; +using ArchUnitNET.Loader; + +internal static class ProjectArchitecture +{ + // TIP: load your architecture once at the start to maximize performance of your tests + private static readonly Lazy _instance = new Lazy( + () => LoadArchitecture(), + LazyThreadSafetyMode.PublicationOnly + ); + + public static Architecture Instance => _instance.Value; + + private static Architecture LoadArchitecture() + { + var architecture = new ArchLoader() + .LoadAssembly(typeof(IFormularProvider).Assembly) + .Build(); + return architecture; + } +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration.csproj b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration.csproj new file mode 100644 index 0000000..5ec37b3 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration/NetEvolve.Extensions.Hosting.WinForms.Tests.Integration.csproj @@ -0,0 +1,24 @@ + + + + $(_TargetFrameworks) + + + + + + + + + + + + + + + + + + + + diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/IHostBuilderExtensionsTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/IHostBuilderExtensionsTests.cs new file mode 100644 index 0000000..faf50ca --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/IHostBuilderExtensionsTests.cs @@ -0,0 +1,100 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Unit; + +using System; +using Microsoft.Extensions.Hosting; +using NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.Internals; +using NSubstitute; +using Xunit; + +public class IHostBuilderExtensionsTests +{ + [Fact] + public void UseWindowsForms_TStartForm_IHostBuilderNull_ThrowArgumentNullException() + { + IHostBuilder builder = null!; + + _ = Assert.Throws( + "builder", + () => builder.UseWindowsForms() + ); + } + + [Fact] + public void UseWindowsForms_TApplicationContext_IHostBuilderNull_ThrowArgumentNullException() + { + IHostBuilder builder = null!; + + _ = Assert.Throws( + "builder", + () => builder.UseWindowsForms() + ); + } + + [Fact] + public void UseWindowsForms_TApplicationContextTStartForm_IHostBuilderNull_ThrowArgumentNullException() + { + IHostBuilder builder = null!; + + _ = Assert.Throws( + "builder", + () => builder.UseWindowsForms(null!) + ); + } + + [Fact] + public void UseWindowsForms_TApplicationContextTStartForm_IHostBuilderSet_ContextFactoryNull_ThrowArgumentNullException() + { + var builder = Substitute.For(); + + _ = Assert.Throws( + "contextFactory", + () => builder.UseWindowsForms(null!) + ); + } + +#if NET7_0_OR_GREATER + [Fact] + public void UseWindowsForms_TStartForm_HostApplicationBuilderNull_ThrowArgumentNullException() + { + HostApplicationBuilder builder = null!; + + _ = Assert.Throws( + "builder", + () => builder!.UseWindowsForms() + ); + } + + [Fact] + public void UseWindowsForms_TApplicationContext_HostApplicationBuilderNull_ThrowArgumentNullException() + { + HostApplicationBuilder builder = null!; + + _ = Assert.Throws( + "builder", + () => builder.UseWindowsForms() + ); + } + + [Fact] + public void UseWindowsForms_TApplicationContextTStartForm_HostApplicationBuilderNull_ThrowArgumentNullException() + { + HostApplicationBuilder builder = null!; + + _ = Assert.Throws( + "builder", + () => builder.UseWindowsForms(null!) + ); + } + + [Fact] + public void UseWindowsForms_TApplicationContextTStartForm_HostApplicationBuilderSet_ContextFactoryNull_ThrowArgumentNullException() + { + var builder = new HostApplicationBuilder(); + + _ = Assert.Throws( + "contextFactory", + () => builder.UseWindowsForms(null!) + ); + } +#endif +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/IServiceCollectionExtensionsTests.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/IServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..6ba7098 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/IServiceCollectionExtensionsTests.cs @@ -0,0 +1,44 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.Internals; + +using System; +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.Extensions.Hosting.WinForms.Internals; +using Xunit; + +public class IServiceCollectionExtensionsTests +{ + [Fact] + public void AddWindowsFormsLifetime_ServicesNull_ThrowArgumentNullException() => + _ = Assert.Throws( + "services", + () => IServiceCollectionExtensions.AddWindowsFormsLifetime(null!, null) + ); + + [Theory] + [MemberData(nameof(AddWindowsFormsData))] + public void AddWindowsFormsLifetime_ConfigurationNull_Expected( + int expectedServices, + Action? configure + ) + { + var serviceCollection = new ServiceCollection().AddWindowsFormsLifetime(configure); + + var services = serviceCollection.BuildServiceProvider(); + + Assert.NotNull(services); + Assert.Equal(expectedServices, serviceCollection.Count); + } + + public static TheoryData?> AddWindowsFormsData => + new TheoryData?> + { + { 4, null }, + { + 10, + options => + { + options.EnableConsoleShutdown = true; + } + } + }; +} diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestApplicatonContext.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestApplicatonContext.cs new file mode 100644 index 0000000..cb4d810 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestApplicatonContext.cs @@ -0,0 +1,7 @@ +namespace NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.Internals; + +using System.Windows.Forms; + +#pragma warning disable CA1812 +internal sealed partial class TestApplicatonContext : ApplicationContext { } +#pragma warning restore CA1812 diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.cs b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.cs new file mode 100644 index 0000000..a356ce0 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.cs @@ -0,0 +1,7 @@ +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/TestForm.resx b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/Internals/TestForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.csproj b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.csproj new file mode 100644 index 0000000..c418c5e --- /dev/null +++ b/tests/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit/NetEvolve.Extensions.Hosting.WinForms.Tests.Unit.csproj @@ -0,0 +1,25 @@ + + + + $(_TargetFrameworks) + + + + + + + + + + + + + + + + + + + + +