├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── README.md ├── TestWindowsFormsLifetime.yml ├── WindowsFormsLifetime.sln ├── samples ├── AppContext │ ├── AppContext.csproj │ ├── ExampleApplicationContext.cs │ ├── HiddenForm.Designer.cs │ ├── HiddenForm.cs │ ├── HiddenForm.resx │ └── Program.cs ├── BlazorHybrid │ ├── BlazorHybrid.csproj │ ├── Counter.razor │ ├── Form1.Designer.cs │ ├── Form1.cs │ ├── Form1.resx │ ├── MainLayout.razor │ ├── Program.cs │ ├── _Imports.razor │ └── wwwroot │ │ ├── css │ │ └── app.css │ │ └── index.html └── SampleApp │ ├── Form1.Designer.cs │ ├── Form1.cs │ ├── Form1.resx │ ├── Form2.Designer.cs │ ├── Form2.cs │ ├── Form2.resx │ ├── FormSpawnHostedService.cs │ ├── Program.cs │ ├── SampleApp.csproj │ ├── TickBag.cs │ └── TickingHostedService.cs ├── src └── WindowsFormsLifetime │ ├── FormProvider.cs │ ├── HostBuilderExtensions.cs │ ├── ServiceCollectionExtensions.cs │ ├── SynchronizationContextExtensions.cs │ ├── WindowsFormsHostedService.cs │ ├── WindowsFormsLifetime.cs │ ├── WindowsFormsLifetime.csproj │ ├── WindowsFormsLifetimeOptions.cs │ └── WindowsFormsSynchronizationContextProvider.cs └── tests └── WindowsFormsLifetime.Tests ├── FormProviderTests.cs ├── WindowsFormsLifetimeTests.cs └── WindowsFormsLifetimeTests.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb 341 | 342 | # Sqlite database 343 | *.db* -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2024 Oswald Technologies, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WindowsFormsLifetime 2 | 3 | [![Build Status](https://dev.azure.com/oswaldtechnologies/WindowsFormsLifetime/_apis/build/status/alex-oswald.WindowsFormsLifetime?branchName=main)](https://dev.azure.com/oswaldtechnologies/WindowsFormsLifetime/_build/latest?definitionId=21&branchName=main) 4 | [![Nuget](https://img.shields.io/nuget/v/OswaldTechnologies.Extensions.Hosting.WindowsFormsLifetime)](https://www.nuget.org/packages/OswaldTechnologies.Extensions.Hosting.WindowsFormsLifetime/) 5 | [![Nuget](https://img.shields.io/nuget/dt/OswaldTechnologies.Extensions.Hosting.WindowsFormsLifetime)](https://www.nuget.org/packages/OswaldTechnologies.Extensions.Hosting.WindowsFormsLifetime/) 6 | 7 | A Windows Forms hosting extension for the [.NET Generic Host](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host). 8 | This library enables you to configure the generic host to use the lifetime of Windows Forms. When configured, 9 | the generic host will start an `IHostedService` that runs Windows Forms in a separate UI specific thread. 10 | 11 | - The Generic Host will use Windows Forms as it's lifetime (when the main form closes, the host shuts down) 12 | - All the benefits of .NET and the Generic Host, dependency injection, configuration, logging... 13 | - Easier multi-threading in Windows Forms 14 | 15 | ## Quick Start 16 | 17 | Install the `OswaldTechnologies.Extensions.Hosting.WindowsFormsLifetime` package from NuGet. 18 | 19 | Using Powershell 20 | 21 | ```powershell 22 | Install-Package OswaldTechnologies.Extensions.Hosting.WindowsFormsLifetime 23 | ``` 24 | 25 | Using the .NET CLI 26 | 27 | ``` 28 | dotnet add package OswaldTechnologies.Extensions.Hosting.WindowsFormsLifetime 29 | ``` 30 | 31 | Create a new **Windows Forms App**. 32 | 33 | Replace the contents of `Program.cs` with the following. 34 | 35 | ```csharp 36 | using Microsoft.Extensions.Hosting; 37 | using WinFormsApp1; 38 | using WindowsFormsLifetime; 39 | 40 | var builder = Host.CreateApplicationBuilder(args); 41 | builder.UseWindowsFormsLifetime(); 42 | 43 | var app = builder.Build(); 44 | app.Run(); 45 | ``` 46 | 47 | **Run the app!** 48 | 49 | **Your Windows Forms app is now running with the Generic Host!** 50 | 51 | ### Passing options 52 | 53 | You can further configure the Windows Forms lifetime by passing `Action`. 54 | For example, with the default options: 55 | 56 | ```csharp 57 | builder.UseWindowsFormsLifetime(options => 58 | { 59 | options.HighDpiMode = HighDpiMode.SystemAware; 60 | options.EnableVisualStyles = true; 61 | options.CompatibleTextRenderingDefault = false; 62 | options.SuppressStatusMessages = false; 63 | options.EnableConsoleShutdown = true; 64 | }); 65 | ``` 66 | 67 | `EnableConsoleShutdown` 68 | Allows the use of Ctrl+C to shutdown the host while the console is being used. 69 | 70 | ### Instantiating and Showing Forms 71 | 72 | Add more forms to the DI container. 73 | 74 | ```csharp 75 | var builder = Host.CreateApplicationBuilder(args); 76 | builder.UseWindowsFormsLifetime(); 77 | builder.Services.AddTransient(); 78 | var app = builder.Build(); 79 | app.Run(); 80 | ``` 81 | 82 | To get a form, use the `IFormProvider`. The form provider fetches an instance of the form from the DI 83 | container on the GUI thread. `IFormProvider` has the method, `GetFormAsync`, used to fetch a form 84 | instance. 85 | 86 | In this example, we inject `IFormProvider` into the main form, and use that to instantiate a new 87 | instance of `Form2`, then show the form. 88 | 89 | ```csharp 90 | public partial class Form1 : Form 91 | { 92 | private readonly ILogger _logger; 93 | private readonly IFormProvider _formProvider; 94 | 95 | public Form1(ILogger logger, IFormProvider formProvider) 96 | { 97 | InitializeComponent(); 98 | _logger = logger; 99 | _formProvider = formProvider; 100 | } 101 | 102 | private async void button1_Click(object sender, EventArgs e) 103 | { 104 | _logger.LogInformation("Show Form2"); 105 | var form = await _formProvider.GetFormAsync(); 106 | form.Show(); 107 | } 108 | } 109 | ``` 110 | 111 | ### Invoking on the GUI thread 112 | 113 | Sometimes you need to invoke an action on the GUI thread. Say you want to spawn a form from a background 114 | service. Use the `IGuiContext` to invoke actions on the GUI thread. 115 | 116 | In this example, a form is fetched and shown, in an action that is invoked on the GUI thread. Then a second 117 | form is shown. This example shows how the GUI does not lock up during this process. 118 | 119 | ```csharp 120 | public class HostedService1 : BackgroundService 121 | { 122 | private readonly ILogger _logger; 123 | private readonly IFormProvider _fp; 124 | private readonly IGuiContext _guiContext; 125 | 126 | public HostedService1( 127 | ILogger logger, 128 | IFormProvider formProvider, 129 | IGuiContext guiContext) 130 | { 131 | _logger = logger; 132 | _fp = formProvider; 133 | _guiContext = guiContext; 134 | } 135 | 136 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 137 | { 138 | int count = 0; 139 | while (!stoppingToken.IsCancellationRequested) 140 | { 141 | await Task.Delay(5000, stoppingToken); 142 | if (count < 5) 143 | { 144 | await _guiContext.InvokeAsync(async () => 145 | { 146 | var form = await _fp.GetFormAsync(); 147 | form.Show(); 148 | }); 149 | } 150 | count++; 151 | _logger.LogInformation("HostedService1 Tick 1000ms"); 152 | } 153 | } 154 | } 155 | ``` 156 | 157 | ## Only use the Console while debugging 158 | 159 | I like to configure my `csproj` so that the `Console` runs only while my configuration is set to `Debug`, 160 | and doesn't run when set to `Release`. Here is an example of how to do this. Setting the `OutputType` to 161 | `Exe` will run the console, while setting it to `WinExe` will not. 162 | 163 | ```xml 164 | 165 | Exe 166 | 167 | 168 | 169 | WinExe 170 | 171 | ``` 172 | 173 | ## Credits 174 | 175 | The layout of the `WindowsFormsLifetime` class is based on .NET Core's 176 | [ConsoleLifetime](https://github.com/dotnet/extensions/blob/b83b27d76439497459fe9cf7337d5128c900eb5a/src/Hosting/Hosting/src/Internal/ConsoleLifetime.cs). 177 | 178 | [ExecutionContext vs SynchronizationContext](https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/) 179 | 180 | [Implementing a SynchronizationContext.SendAsync method](https://devblogs.microsoft.com/pfxteam/implementing-a-synchronizationcontext-sendasync-method/) 181 | -------------------------------------------------------------------------------- /TestWindowsFormsLifetime.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - main 5 | tags: 6 | include: ['*'] 7 | 8 | pool: 9 | vmImage: 'windows-latest' 10 | 11 | variables: 12 | buildConfiguration: 'Release' 13 | solution: '**/*.sln' 14 | libraryName: 'WindowsFormsLifetime' 15 | 16 | steps: 17 | - task: UseDotNet@2 18 | displayName: 'Use .NET 9.x SDK' 19 | inputs: 20 | version: 9.x 21 | 22 | - task: DotNetCoreCLI@2 23 | displayName: 'Restore Solution' 24 | inputs: 25 | command: restore 26 | projects: '$(solution)' 27 | 28 | - task: DotNetCoreCLI@2 29 | displayName: 'Build Projects' 30 | inputs: 31 | command: build 32 | arguments: '--configuration $(buildConfiguration)' 33 | projects: '$(solution)' 34 | 35 | - pwsh: | 36 | dotnet test "$(Build.SourcesDirectory)/tests/$(libraryName).Tests/$(libraryName)Tests.csproj" ` 37 | --configuration $(buildConfiguration) ` 38 | --results-directory "$(Build.ArtifactStagingDirectory)" ` 39 | --collect:"XPlat Code Coverage" ` 40 | --logger "trx;LogFileName=$(libraryName).TestResult.xml" 41 | displayName: "Run Tests" 42 | 43 | - task: PublishCodeCoverageResults@2 44 | displayName: "Publish Code Coverage" 45 | inputs: 46 | summaryFileLocation: "$(Build.ArtifactStagingDirectory)/**/coverage.cobertura.xml" 47 | failIfCoverageEmpty: true 48 | 49 | - task: PublishTestResults@2 50 | displayName: "Publish Tests" 51 | inputs: 52 | testResultsFormat: "VSTest" 53 | testResultsFiles: "$(Build.ArtifactStagingDirectory)/$(libraryName).TestResult.xml" 54 | failTaskOnFailedTests: true 55 | 56 | # - task: BuildQualityChecks@9 57 | # displayName: 'Check Code Coverage' 58 | # inputs: 59 | # checkCoverage: true 60 | # coverageFailOption: fixed 61 | # coverageType: branches 62 | # coverageThreshold: 70 63 | 64 | - task: PublishBuildArtifacts@1 65 | inputs: 66 | pathtoPublish: '$(System.DefaultWorkingDirectory)' 67 | artifactName: BuildPackage -------------------------------------------------------------------------------- /WindowsFormsLifetime.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsFormsLifetime", "src\WindowsFormsLifetime\WindowsFormsLifetime.csproj", "{7062FBBC-95F3-4E6C-B7A8-8F770DF94534}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "samples\SampleApp\SampleApp.csproj", "{1C35BE2C-2DD6-4A14-BFCE-0F5AE5CFE66A}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsFormsLifetimeTests", "tests\WindowsFormsLifetime.Tests\WindowsFormsLifetimeTests.csproj", "{33444554-2175-4AD5-A018-70A5EF258D5C}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppContext", "samples\AppContext\AppContext.csproj", "{94297776-27CB-458B-9545-4C28423F8FE2}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A38F9AE3-0B79-42A8-BAF0-A4F5035FF59B}" 15 | ProjectSection(SolutionItems) = preProject 16 | .gitattributes = .gitattributes 17 | .gitignore = .gitignore 18 | azure-pipelines.yml = azure-pipelines.yml 19 | LICENSE.txt = LICENSE.txt 20 | README.md = README.md 21 | EndProjectSection 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{D6D44276-9DBE-4C4C-9C21-1AD507CE2002}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorHybrid", "samples\BlazorHybrid\BlazorHybrid.csproj", "{48CA1844-DC99-4C9D-9CEE-EA99B60F8587}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {7062FBBC-95F3-4E6C-B7A8-8F770DF94534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {7062FBBC-95F3-4E6C-B7A8-8F770DF94534}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {7062FBBC-95F3-4E6C-B7A8-8F770DF94534}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {7062FBBC-95F3-4E6C-B7A8-8F770DF94534}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {1C35BE2C-2DD6-4A14-BFCE-0F5AE5CFE66A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {1C35BE2C-2DD6-4A14-BFCE-0F5AE5CFE66A}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {1C35BE2C-2DD6-4A14-BFCE-0F5AE5CFE66A}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {1C35BE2C-2DD6-4A14-BFCE-0F5AE5CFE66A}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {33444554-2175-4AD5-A018-70A5EF258D5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {33444554-2175-4AD5-A018-70A5EF258D5C}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {33444554-2175-4AD5-A018-70A5EF258D5C}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {33444554-2175-4AD5-A018-70A5EF258D5C}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {94297776-27CB-458B-9545-4C28423F8FE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {94297776-27CB-458B-9545-4C28423F8FE2}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {94297776-27CB-458B-9545-4C28423F8FE2}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {94297776-27CB-458B-9545-4C28423F8FE2}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {48CA1844-DC99-4C9D-9CEE-EA99B60F8587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {48CA1844-DC99-4C9D-9CEE-EA99B60F8587}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {48CA1844-DC99-4C9D-9CEE-EA99B60F8587}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {48CA1844-DC99-4C9D-9CEE-EA99B60F8587}.Release|Any CPU.Build.0 = Release|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | GlobalSection(NestedProjects) = preSolution 58 | {1C35BE2C-2DD6-4A14-BFCE-0F5AE5CFE66A} = {D6D44276-9DBE-4C4C-9C21-1AD507CE2002} 59 | {94297776-27CB-458B-9545-4C28423F8FE2} = {D6D44276-9DBE-4C4C-9C21-1AD507CE2002} 60 | {48CA1844-DC99-4C9D-9CEE-EA99B60F8587} = {D6D44276-9DBE-4C4C-9C21-1AD507CE2002} 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {A00619E4-0417-4284-A108-BCE3A2FEBBC1} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /samples/AppContext/AppContext.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0-windows 6 | true 7 | false 8 | enable 9 | enable 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/AppContext/ExampleApplicationContext.cs: -------------------------------------------------------------------------------- 1 | namespace AppContext; 2 | 3 | public class ExampleApplicationContext : ApplicationContext 4 | { 5 | public ExampleApplicationContext(Form mainForm) 6 | : base(mainForm) { } 7 | } -------------------------------------------------------------------------------- /samples/AppContext/HiddenForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace AppContext 2 | { 3 | partial class HiddenForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.SuspendLayout(); 32 | // 33 | // HiddenForm 34 | // 35 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); 36 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 37 | this.ClientSize = new System.Drawing.Size(800, 450); 38 | this.ResumeLayout(false); 39 | this.PerformLayout(); 40 | 41 | } 42 | 43 | #endregion 44 | } 45 | } -------------------------------------------------------------------------------- /samples/AppContext/HiddenForm.cs: -------------------------------------------------------------------------------- 1 | namespace AppContext; 2 | 3 | public partial class HiddenForm : Form 4 | { 5 | private readonly ILogger _logger; 6 | private readonly IHostApplicationLifetime _hostLifetime; 7 | 8 | public HiddenForm( 9 | ILogger logger, 10 | IHostApplicationLifetime hostLifetime) 11 | { 12 | _logger = logger; 13 | _hostLifetime = hostLifetime; 14 | Load += OnLoad; 15 | FormClosing += OnFormClosing; 16 | } 17 | 18 | private void OnFormClosing(object? sender, FormClosingEventArgs e) 19 | { 20 | _logger.LogInformation("Form closing"); 21 | } 22 | 23 | private async void OnLoad(object? sender, EventArgs e) 24 | { 25 | // Example of a hidden main form 26 | Visible = false; 27 | ShowInTaskbar = false; 28 | _logger.LogInformation("Form load"); 29 | await ExecuteAsync(); 30 | } 31 | 32 | public async Task ExecuteAsync(CancellationToken stoppingToken = default) 33 | { 34 | _logger.LogInformation("Execute"); 35 | int count = 0; 36 | while (!stoppingToken.IsCancellationRequested) 37 | { 38 | if (count == 10) 39 | { 40 | _hostLifetime.StopApplication(); 41 | } 42 | _logger.LogInformation("Do work {Count}", count++); 43 | await Task.Delay(1000, stoppingToken); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /samples/AppContext/HiddenForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /samples/AppContext/Program.cs: -------------------------------------------------------------------------------- 1 | using AppContext; 2 | using WindowsFormsLifetime; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | // Pass in a factory lambda that constructs an ApplicationContext using the start form 6 | builder.Host.UseWindowsFormsLifetime( 7 | startForm => new ExampleApplicationContext(startForm)); 8 | 9 | var app = builder.Build(); 10 | app.Run(); -------------------------------------------------------------------------------- /samples/BlazorHybrid/BlazorHybrid.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0-windows 6 | enable 7 | true 8 | enable 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/BlazorHybrid/Counter.razor: -------------------------------------------------------------------------------- 1 | 

Counter

2 | 3 |

Current count: @currentCount

4 | 5 | Click me 6 | 7 | @code { 8 | private int currentCount = 0; 9 | 10 | private void IncrementCount() 11 | { 12 | currentCount++; 13 | } 14 | } -------------------------------------------------------------------------------- /samples/BlazorHybrid/Form1.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorHybrid 2 | { 3 | partial class Form1 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.blazorWebView1 = new Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView(); 32 | this.SuspendLayout(); 33 | // 34 | // blazorWebView1 35 | // 36 | this.blazorWebView1.Dock = System.Windows.Forms.DockStyle.Fill; 37 | this.blazorWebView1.Location = new System.Drawing.Point(0, 0); 38 | this.blazorWebView1.Name = "blazorWebView1"; 39 | this.blazorWebView1.Size = new System.Drawing.Size(800, 450); 40 | this.blazorWebView1.TabIndex = 0; 41 | this.blazorWebView1.Text = "blazorWebView1"; 42 | // 43 | // Form1 44 | // 45 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); 46 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 47 | this.ClientSize = new System.Drawing.Size(800, 450); 48 | this.Controls.Add(this.blazorWebView1); 49 | this.Name = "Form1"; 50 | this.Text = "Form1"; 51 | this.ResumeLayout(false); 52 | 53 | } 54 | 55 | #endregion 56 | 57 | private Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView blazorWebView1; 58 | } 59 | } -------------------------------------------------------------------------------- /samples/BlazorHybrid/Form1.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebView.WindowsForms; 2 | 3 | namespace BlazorHybrid; 4 | 5 | public partial class Form1 : Form 6 | { 7 | public Form1(IServiceProvider sp) 8 | { 9 | InitializeComponent(); 10 | 11 | blazorWebView1.HostPage = "wwwroot\\index.html"; 12 | blazorWebView1.Services = sp; 13 | blazorWebView1.RootComponents.Add("#app"); 14 | } 15 | } -------------------------------------------------------------------------------- /samples/BlazorHybrid/Form1.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /samples/BlazorHybrid/MainLayout.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/BlazorHybrid/Program.cs: -------------------------------------------------------------------------------- 1 | using BlazorHybrid; 2 | using MudBlazor.Services; 3 | using WindowsFormsLifetime; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | builder.Host.UseWindowsFormsLifetime(); 7 | builder.Services.AddWindowsFormsBlazorWebView(); 8 | builder.Services.AddMudServices(); 9 | 10 | var app = builder.Build(); 11 | app.Run(); -------------------------------------------------------------------------------- /samples/BlazorHybrid/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | @using MudBlazor -------------------------------------------------------------------------------- /samples/BlazorHybrid/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /samples/BlazorHybrid/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | WinFormsBlazor 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Loading...
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/SampleApp/Form1.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace SampleApp 2 | { 3 | partial class Form1 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | button1 = new Button(); 32 | button2 = new Button(); 33 | ThreadLabel = new Label(); 34 | TickLabel = new Label(); 35 | SuspendLayout(); 36 | // 37 | // button1 38 | // 39 | button1.Location = new Point(12, 12); 40 | button1.Name = "button1"; 41 | button1.Size = new Size(150, 63); 42 | button1.TabIndex = 0; 43 | button1.Text = "Open Form2"; 44 | button1.UseVisualStyleBackColor = true; 45 | button1.Click += button1_Click; 46 | // 47 | // button2 48 | // 49 | button2.Location = new Point(168, 12); 50 | button2.Name = "button2"; 51 | button2.Size = new Size(150, 63); 52 | button2.TabIndex = 1; 53 | button2.Text = "Exit"; 54 | button2.UseVisualStyleBackColor = true; 55 | button2.Click += button2_Click; 56 | // 57 | // ThreadLabel 58 | // 59 | ThreadLabel.AutoSize = true; 60 | ThreadLabel.Location = new Point(49, 108); 61 | ThreadLabel.Name = "ThreadLabel"; 62 | ThreadLabel.Size = new Size(46, 15); 63 | ThreadLabel.TabIndex = 2; 64 | ThreadLabel.Text = "Thread:"; 65 | // 66 | // TickLabel 67 | // 68 | TickLabel.AutoSize = true; 69 | TickLabel.Location = new Point(49, 147); 70 | TickLabel.Name = "TickLabel"; 71 | TickLabel.Size = new Size(31, 15); 72 | TickLabel.TabIndex = 3; 73 | TickLabel.Text = "Tick:"; 74 | // 75 | // Form1 76 | // 77 | AutoScaleDimensions = new SizeF(7F, 15F); 78 | AutoScaleMode = AutoScaleMode.Font; 79 | ClientSize = new Size(446, 244); 80 | Controls.Add(TickLabel); 81 | Controls.Add(ThreadLabel); 82 | Controls.Add(button2); 83 | Controls.Add(button1); 84 | Name = "Form1"; 85 | Text = "Form1"; 86 | ResumeLayout(false); 87 | PerformLayout(); 88 | } 89 | 90 | #endregion 91 | 92 | private System.Windows.Forms.Button button1; 93 | private System.Windows.Forms.Button button2; 94 | private System.Windows.Forms.Label ThreadLabel; 95 | private Label TickLabel; 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /samples/SampleApp/Form1.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using WindowsFormsLifetime; 3 | 4 | namespace SampleApp; 5 | 6 | public partial class Form1 : Form 7 | { 8 | private readonly ILogger _logger; 9 | private readonly IFormProvider _formProvider; 10 | 11 | public Form1( 12 | ILogger logger, 13 | IFormProvider formProvider, 14 | TickBag tickBag) 15 | { 16 | InitializeComponent(); 17 | _logger = logger; 18 | _formProvider = formProvider; 19 | tickBag.OnTick += TickBag_OnTick; 20 | 21 | ThreadLabel.Text = $"Thread id = {Environment.CurrentManagedThreadId}, Thread name = {Thread.CurrentThread.Name}"; 22 | TickLabel.Text = $"Tick = {tickBag.CurrentTick}"; 23 | } 24 | 25 | private void TickBag_OnTick(object? sender, int e) 26 | { 27 | TickLabel.Text = $"Tick: {e}"; 28 | } 29 | 30 | private async void button1_Click(object sender, EventArgs e) 31 | { 32 | _logger.LogInformation("Show"); 33 | var form = await _formProvider.GetFormAsync(); 34 | form.Show(); 35 | } 36 | 37 | private void button2_Click(object sender, EventArgs e) 38 | { 39 | _logger.LogInformation("Close"); 40 | this.Close(); 41 | } 42 | } -------------------------------------------------------------------------------- /samples/SampleApp/Form1.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /samples/SampleApp/Form2.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace SampleApp 2 | { 3 | partial class Form2 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.button1 = new System.Windows.Forms.Button(); 32 | this.ThreadLabel = new System.Windows.Forms.Label(); 33 | this.SuspendLayout(); 34 | // 35 | // button1 36 | // 37 | this.button1.Location = new System.Drawing.Point(89, 45); 38 | this.button1.Name = "button1"; 39 | this.button1.Size = new System.Drawing.Size(131, 56); 40 | this.button1.TabIndex = 0; 41 | this.button1.Text = "button1"; 42 | this.button1.UseVisualStyleBackColor = true; 43 | this.button1.Click += new System.EventHandler(this.button1_Click); 44 | // 45 | // ThreadLabel 46 | // 47 | this.ThreadLabel.AutoSize = true; 48 | this.ThreadLabel.Location = new System.Drawing.Point(147, 196); 49 | this.ThreadLabel.Name = "ThreadLabel"; 50 | this.ThreadLabel.Size = new System.Drawing.Size(0, 15); 51 | this.ThreadLabel.TabIndex = 1; 52 | // 53 | // Form2 54 | // 55 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); 56 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 57 | this.ClientSize = new System.Drawing.Size(800, 450); 58 | this.Controls.Add(this.ThreadLabel); 59 | this.Controls.Add(this.button1); 60 | this.Name = "Form2"; 61 | this.Text = "Form2"; 62 | this.ResumeLayout(false); 63 | this.PerformLayout(); 64 | 65 | } 66 | 67 | #endregion 68 | 69 | private System.Windows.Forms.Button button1; 70 | private System.Windows.Forms.Label ThreadLabel; 71 | } 72 | } -------------------------------------------------------------------------------- /samples/SampleApp/Form2.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using WindowsFormsLifetime; 3 | 4 | namespace SampleApp; 5 | 6 | public partial class Form2 : Form 7 | { 8 | private readonly ILogger _logger; 9 | private readonly IGuiContext _guiContext; 10 | 11 | public Form2( 12 | ILogger logger, 13 | IGuiContext guiContext) 14 | { 15 | InitializeComponent(); 16 | _logger = logger; 17 | _guiContext = guiContext; 18 | 19 | ThreadLabel.Text = $"{Thread.CurrentThread.ManagedThreadId} {Thread.CurrentThread.Name}"; 20 | } 21 | 22 | private void button1_Click(object sender, EventArgs e) 23 | { 24 | // Running a task on the thread pool means that to update a control on the form, 25 | // we must invoke on the thread that created the control, or we get a cross thread exception 26 | // Use IGuiContext to invoke actions on the gui thread 27 | Task.Run(() => 28 | { 29 | _logger.LogInformation($"Task.Run Thread {Thread.CurrentThread.ManagedThreadId} {Thread.CurrentThread.Name}"); 30 | _guiContext.Invoke(new Action(async () => 31 | { 32 | await Task.Delay(1000); 33 | _logger.LogInformation($"GuiContext Thread {Thread.CurrentThread.ManagedThreadId} {Thread.CurrentThread.Name}"); 34 | button1.Text = new Random().Next(1, 10).ToString(); 35 | })); 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /samples/SampleApp/Form2.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /samples/SampleApp/FormSpawnHostedService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | using WindowsFormsLifetime; 4 | 5 | namespace SampleApp; 6 | 7 | public class FormSpawnHostedService : BackgroundService 8 | { 9 | private readonly ILogger _logger; 10 | private readonly IFormProvider _fp; 11 | private readonly IGuiContext _guiContext; 12 | 13 | public FormSpawnHostedService( 14 | ILogger logger, 15 | IFormProvider formProvider, 16 | IGuiContext guiContext) 17 | { 18 | _logger = logger; 19 | _fp = formProvider; 20 | _guiContext = guiContext; 21 | } 22 | 23 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 24 | { 25 | int count = 0; 26 | while (!stoppingToken.IsCancellationRequested) 27 | { 28 | await Task.Delay(5000, stoppingToken); 29 | if (count < 2) 30 | { 31 | // Fetch the form here using IFormProvider 32 | // The form provider will get the form from the DI container on the gui thread 33 | // Then we must invoke the Show method on the gui thread as well using IGuiContext 34 | _logger.LogInformation($"GetFormAsync {Thread.CurrentThread.ManagedThreadId} {Thread.CurrentThread.Name}"); 35 | var form = await _fp.GetFormAsync(); 36 | _guiContext.Invoke(() => form.Show()); 37 | } 38 | count++; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /samples/SampleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using SampleApp; 4 | using WindowsFormsLifetime; 5 | 6 | var builder = Host.CreateApplicationBuilder(args); 7 | builder.UseWindowsFormsLifetime(); 8 | builder.Services.AddHostedService(); 9 | builder.Services.AddHostedService(); 10 | builder.Services.AddTransient(); 11 | builder.Services.AddSingleton(); 12 | 13 | var app = builder.Build(); 14 | app.Run(); -------------------------------------------------------------------------------- /samples/SampleApp/SampleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0-windows 5 | true 6 | false 7 | enable 8 | enable 9 | 10 | 11 | 12 | Exe 13 | 14 | 15 | 16 | WinExe 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /samples/SampleApp/TickBag.cs: -------------------------------------------------------------------------------- 1 | namespace SampleApp; 2 | public class TickBag 3 | { 4 | private int _currentTick = 0; 5 | 6 | public event EventHandler? OnTick; 7 | 8 | public int CurrentTick => _currentTick; 9 | 10 | public void Increment() 11 | { 12 | Interlocked.Increment(ref _currentTick); 13 | OnTick?.Invoke(this, _currentTick); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/SampleApp/TickingHostedService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace SampleApp; 5 | 6 | public class TickingHostedService : BackgroundService 7 | { 8 | private readonly ILogger _logger; 9 | private readonly TickBag _tickBag; 10 | 11 | public TickingHostedService(ILogger logger, TickBag tickBag) 12 | { 13 | _logger = logger; 14 | _tickBag = tickBag; 15 | } 16 | 17 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 18 | { 19 | while (!stoppingToken.IsCancellationRequested) 20 | { 21 | await Task.Delay(2000, stoppingToken); 22 | _tickBag.Increment(); 23 | _logger.LogInformation( 24 | "Tick 2000ms, thread id = {threadId}, thread name = {threadName}, tick value={value}", 25 | Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Name, _tickBag.CurrentTick); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/FormProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace WindowsFormsLifetime; 4 | 5 | public interface IFormProvider 6 | { 7 | /// 8 | /// Gets the requested form type and ensures it is created on the UI thread. 9 | /// 10 | /// The form type to get. 11 | /// An instance of the form, asynchronously. 12 | Task GetFormAsync() where T : Form; 13 | 14 | /// 15 | /// Gets the requested form type and ensures it is created on the UI thread. Creates the form in the given scope. 16 | /// 17 | /// The form type to get. 18 | /// The scope in which the form should be created. 19 | /// An instance of the form, asynchronously. 20 | Task GetFormAsync(IServiceScope scope) where T : Form; 21 | 22 | Task
GetMainFormAsync(); 23 | 24 | /// 25 | /// Gets the requested form type on the current thread. Should only be called on the UI thread. All scoped and transient dependencies will be disposed when the form is disposed. 26 | /// 27 | /// The form type to get. 28 | /// An instance of the form. 29 | T GetForm() where T : Form; 30 | 31 | /// 32 | /// Gets the requested form type on the current thread. Should only be called on the UI thread. Creates the form in the given scope. 33 | /// 34 | /// The form type to get. 35 | /// The scope in which the form should be created. 36 | /// An instance of the form. 37 | T GetForm(IServiceScope scope) where T : Form; 38 | } 39 | 40 | public class FormProvider : IFormProvider 41 | { 42 | private readonly SemaphoreSlim _semaphore = new(1, 1); 43 | private readonly IServiceProvider _serviceProvider; 44 | private readonly IWindowsFormsSynchronizationContextProvider _syncContextManager; 45 | private readonly IServiceScopeFactory _serviceScopeFactory; 46 | 47 | public FormProvider( 48 | IServiceProvider serviceProvider, 49 | IWindowsFormsSynchronizationContextProvider syncContextManager, 50 | IServiceScopeFactory serviceScopeFactory) 51 | { 52 | _serviceProvider = serviceProvider; 53 | _syncContextManager = syncContextManager; 54 | _serviceScopeFactory = serviceScopeFactory; 55 | } 56 | 57 | public async Task GetFormAsync() 58 | where T : Form 59 | { 60 | // We are throttling this because there is only one gui thread 61 | await _semaphore.WaitAsync(); 62 | 63 | var form = await _syncContextManager.SynchronizationContext.InvokeAsync(GetForm); 64 | 65 | _semaphore.Release(); 66 | 67 | return form; 68 | } 69 | 70 | public async Task GetFormAsync(IServiceScope scope) where T : Form 71 | { 72 | // We are throttling this because there is only one gui thread 73 | await _semaphore.WaitAsync(); 74 | 75 | var form = await _syncContextManager.SynchronizationContext.InvokeAsync(() => scope.ServiceProvider.GetService()); 76 | 77 | _semaphore.Release(); 78 | 79 | return form; 80 | } 81 | 82 | public Task GetMainFormAsync() 83 | { 84 | var applicationContext = _serviceProvider.GetService(); 85 | return Task.FromResult(applicationContext.MainForm); 86 | } 87 | 88 | public T GetForm() where T : Form 89 | { 90 | T form = null; 91 | var scope = _serviceScopeFactory.CreateScope(); 92 | try 93 | { 94 | form = scope.ServiceProvider.GetService(); 95 | if (form == null) 96 | { 97 | scope.Dispose(); 98 | } 99 | else 100 | { 101 | form.Disposed += (s, e) => scope.Dispose(); 102 | } 103 | } 104 | catch 105 | { 106 | scope.Dispose(); 107 | throw; 108 | } 109 | 110 | return form; 111 | } 112 | 113 | public T GetForm(IServiceScope scope) where T : Form 114 | => scope.ServiceProvider.GetService(); 115 | 116 | public void Dispose() => _semaphore?.Dispose(); 117 | } 118 | -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/HostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | 3 | namespace WindowsFormsLifetime; 4 | 5 | public static class HostBuilderExtensions 6 | { 7 | /// 8 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 9 | /// then waits for the startup form to close before shutting down. 10 | /// 11 | /// The to configure. 12 | /// The delegate for configuring the . 13 | /// The same instance of the for chaining. 14 | public static IHostBuilder UseWindowsFormsLifetime( 15 | this IHostBuilder hostBuilder, Action configure = null) 16 | where TStartForm : Form 17 | => hostBuilder.ConfigureServices(services => services.AddWindowsFormsLifetime(configure)); 18 | 19 | /// 20 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 21 | /// then waits for the startup context to close before shutting down. 22 | /// 23 | /// The to configure. 24 | /// The factory. 25 | /// The delegate for configuring the . 26 | /// The same instance of the for chaining. 27 | public static IHostBuilder UseWindowsFormsLifetime( 28 | this IHostBuilder hostBuilder, Func applicationContextFactory = null, Action configure = null) 29 | where TAppContext : ApplicationContext 30 | => hostBuilder.ConfigureServices(services => services.AddWindowsFormsLifetime(applicationContextFactory, configure)); 31 | 32 | /// 33 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 34 | /// then waits for the startup context to close before shutting down. 35 | /// 36 | /// The to configure. 37 | /// The factory. 38 | /// The delegate for configuring the . 39 | /// The same instance of the for chaining. 40 | public static IHostBuilder UseWindowsFormsLifetime( 41 | this IHostBuilder hostBuilder, Func applicationContextFactory, Action configure = null) 42 | where TAppContext : ApplicationContext 43 | where TStartForm : Form 44 | => hostBuilder.ConfigureServices(services => services.AddWindowsFormsLifetime(applicationContextFactory, configure)); 45 | 46 | /// 47 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 48 | /// then waits for the startup form to close before shutting down. 49 | /// 50 | /// The to configure. 51 | /// The delegate for configuring the . 52 | /// The same instance of the for chaining. 53 | public static IHostApplicationBuilder UseWindowsFormsLifetime( 54 | this IHostApplicationBuilder hostAppBuilder, Action configure = null) 55 | where TStartForm : Form 56 | { 57 | hostAppBuilder.Services.AddWindowsFormsLifetime(configure); 58 | 59 | return hostAppBuilder; 60 | } 61 | 62 | /// 63 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 64 | /// then waits for the startup context to close before shutting down. 65 | /// 66 | /// The to configure. 67 | /// The factory. 68 | /// The delegate for configuring the . 69 | /// The same instance of the for chaining. 70 | public static IHostApplicationBuilder UseWindowsFormsLifetime( 71 | this IHostApplicationBuilder hostAppBuilder, Func applicationContextFactory = null, Action configure = null) 72 | where TAppContext : ApplicationContext 73 | { 74 | hostAppBuilder.Services.AddWindowsFormsLifetime(applicationContextFactory, configure); 75 | 76 | return hostAppBuilder; 77 | } 78 | 79 | /// 80 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 81 | /// then waits for the startup context to close before shutting down. 82 | /// 83 | /// The to configure. 84 | /// The factory. 85 | /// The delegate for configuring the . 86 | /// The same instance of the for chaining. 87 | public static IHostApplicationBuilder UseWindowsFormsLifetime( 88 | this IHostApplicationBuilder hostAppBuilder 89 | , Func applicationContextFactory, Action configure = null) 90 | where TAppContext : ApplicationContext 91 | where TStartForm : Form 92 | { 93 | hostAppBuilder.Services.AddWindowsFormsLifetime(applicationContextFactory, configure); 94 | 95 | return hostAppBuilder; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace WindowsFormsLifetime; 6 | 7 | public static class ServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddWindowsFormsLifetime( 10 | this IServiceCollection services, Action configure, Action preApplicationRunAction = null) 11 | { 12 | services.AddSingleton(); 13 | services.AddHostedService(sp => 14 | { 15 | var options = sp.GetRequiredService>(); 16 | var life = sp.GetRequiredService(); 17 | var sync = sp.GetRequiredService(); 18 | return new WindowsFormsHostedService(options, life, sp, sync, preApplicationRunAction); 19 | }); 20 | services.Configure(configure ?? (_ => new WindowsFormsLifetimeOptions())); 21 | 22 | services.AddSingleton(); 23 | 24 | // Synchronization context 25 | services.AddSingleton(); 26 | services.AddSingleton(sp => sp.GetRequiredService()); 27 | services.AddSingleton(sp => sp.GetRequiredService()); 28 | 29 | return services; 30 | } 31 | 32 | /// 33 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 34 | /// then waits for the startup form to close before shutting down. 35 | /// 36 | /// The to add the required services to. 37 | /// The delegate for configuring the . 38 | /// The delegate to execute before the application starts running. 39 | /// The same instance of the for chaining. 40 | public static IServiceCollection AddWindowsFormsLifetime( 41 | this IServiceCollection services, Action configure = null, Action preApplicationRunAction = null) 42 | where TStartForm : Form 43 | => services 44 | .AddSingleton() 45 | .AddSingleton(provider => new ApplicationContext(provider.GetRequiredService())) 46 | .AddWindowsFormsLifetime(configure, preApplicationRunAction); 47 | 48 | /// 49 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 50 | /// then waits for the startup context to close before shutting down. 51 | /// 52 | /// The to add the required services to. 53 | /// The factory. 54 | /// The delegate for configuring the . 55 | /// The delegate to execute before the application starts running. 56 | /// The same instance of the for chaining. 57 | public static IServiceCollection AddWindowsFormsLifetime( 58 | this IServiceCollection services, Func applicationContextFactory = null, Action configure = null, Action preApplicationRunAction = null) 59 | where TAppContext : ApplicationContext 60 | { 61 | services = applicationContextFactory is null 62 | ? services.AddSingleton() 63 | : services.AddSingleton(provider => applicationContextFactory()); 64 | 65 | services.AddSingleton(); 66 | services.AddWindowsFormsLifetime(configure, preApplicationRunAction); 67 | 68 | return services; 69 | } 70 | 71 | /// 72 | /// Enables Windows Forms support, builds and starts the host, starts the startup , 73 | /// then waits for the startup context to close before shutting down. 74 | /// 75 | /// The to add the required services to. 76 | /// The factory. 77 | /// The delegate for configuring the . 78 | /// The delegate to execute before the application starts running. 79 | /// The same instance of the for chaining. 80 | public static IServiceCollection AddWindowsFormsLifetime( 81 | this IServiceCollection services, Func applicationContextFactory, Action configure = null, Action preApplicationRunAction = null) 82 | where TAppContext : ApplicationContext 83 | where TStartForm : Form 84 | { 85 | services.AddSingleton(); 86 | services.AddSingleton(provider => 87 | { 88 | var startForm = provider.GetRequiredService(); 89 | return applicationContextFactory(startForm); 90 | }); 91 | services.AddSingleton(); 92 | services.AddWindowsFormsLifetime(configure, preApplicationRunAction); 93 | 94 | return services; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/SynchronizationContextExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WindowsFormsLifetime; 2 | 3 | internal static class SynchronizationContextExtensions 4 | { 5 | public static void Invoke(this SynchronizationContext context, Action action) 6 | { 7 | context.Send(delegate { 8 | action(); 9 | }, null); 10 | } 11 | 12 | public static TResult Invoke(this SynchronizationContext context, Func func) 13 | { 14 | TResult result = default; 15 | context.Send(delegate 16 | { 17 | result = func(); 18 | }, null); 19 | return result; 20 | } 21 | 22 | public static Task InvokeAsync(this SynchronizationContext context, Func func) 23 | { 24 | var tcs = new TaskCompletionSource(); 25 | context.Post(delegate { 26 | try 27 | { 28 | TResult result = func(); 29 | tcs.SetResult(result); 30 | } 31 | catch (Exception e) 32 | { 33 | tcs.SetException(e); 34 | } 35 | }, null); 36 | return tcs.Task; 37 | } 38 | 39 | public static Task InvokeAsync(this SynchronizationContext context, Func func, TInput input) 40 | { 41 | var tcs = new TaskCompletionSource(); 42 | context.Post(delegate { 43 | try 44 | { 45 | TResult result = func(input); 46 | tcs.SetResult(result); 47 | } 48 | catch (Exception e) 49 | { 50 | tcs.SetException(e); 51 | } 52 | }, null); 53 | return tcs.Task; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/WindowsFormsHostedService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace WindowsFormsLifetime; 6 | 7 | public class WindowsFormsHostedService : IHostedService, IDisposable 8 | { 9 | private CancellationTokenRegistration _applicationStoppingRegistration; 10 | private readonly WindowsFormsLifetimeOptions _options; 11 | private readonly IHostApplicationLifetime _hostApplicationLifetime; 12 | private readonly IServiceProvider _serviceProvider; 13 | private readonly WindowsFormsSynchronizationContextProvider _syncContextManager; 14 | 15 | public WindowsFormsHostedService( 16 | IOptions options, 17 | IHostApplicationLifetime hostApplicationLifetime, 18 | IServiceProvider serviceProvider, 19 | WindowsFormsSynchronizationContextProvider syncContextManager, 20 | Action preApplicationRunAction) 21 | { 22 | _options = options.Value; 23 | _hostApplicationLifetime = hostApplicationLifetime; 24 | _serviceProvider = serviceProvider; 25 | _syncContextManager = syncContextManager; 26 | PreApplicationRunAction = preApplicationRunAction; 27 | } 28 | 29 | public Action PreApplicationRunAction { get; private set; } 30 | 31 | public Task StartAsync(CancellationToken cancellationToken) 32 | { 33 | _applicationStoppingRegistration = _hostApplicationLifetime.ApplicationStopping.Register(state => 34 | { 35 | ((WindowsFormsHostedService)state).OnApplicationStopping(); 36 | }, 37 | this); 38 | 39 | Thread thread = new(StartUiThread) 40 | { 41 | Name = "WindowsFormsLifetime UI Thread" 42 | }; 43 | thread.SetApartmentState(ApartmentState.STA); 44 | thread.Start(); 45 | 46 | return Task.CompletedTask; 47 | } 48 | 49 | public Task StopAsync(CancellationToken cancellationToken) 50 | { 51 | return Task.CompletedTask; 52 | } 53 | 54 | private void StartUiThread() 55 | { 56 | Application.SetHighDpiMode(_options.HighDpiMode); 57 | if (_options.EnableVisualStyles) 58 | { 59 | Application.EnableVisualStyles(); 60 | } 61 | Application.SetCompatibleTextRenderingDefault(_options.CompatibleTextRenderingDefault); 62 | Application.ApplicationExit += OnApplicationExit; 63 | 64 | // Don't autoinstall since we are creating our own 65 | WindowsFormsSynchronizationContext.AutoInstall = false; 66 | 67 | // Create the sync context on our UI thread 68 | _syncContextManager.SynchronizationContext = new WindowsFormsSynchronizationContext(); 69 | SynchronizationContext.SetSynchronizationContext(_syncContextManager.SynchronizationContext); 70 | 71 | var applicationContext = _serviceProvider.GetService(); 72 | PreApplicationRunAction?.Invoke(_serviceProvider); 73 | Application.Run(applicationContext); 74 | } 75 | 76 | private void OnApplicationStopping() 77 | { 78 | var applicationContext = _serviceProvider.GetService(); 79 | var form = applicationContext.MainForm; 80 | 81 | // If the form is closed then the handle no longer exists 82 | // We would get an exception trying to invoke from the control when it is already closed 83 | if (form != null && form.IsHandleCreated) 84 | { 85 | // If the host lifetime is stopped, gracefully close and dispose of forms in the service provider 86 | form.Invoke(new Action(() => 87 | { 88 | form.Close(); 89 | form.Dispose(); 90 | })); 91 | } 92 | } 93 | 94 | private void OnApplicationExit(object sender, EventArgs e) 95 | { 96 | _hostApplicationLifetime.StopApplication(); 97 | } 98 | 99 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "")] 100 | public void Dispose() 101 | { 102 | Application.ApplicationExit -= OnApplicationExit; 103 | _applicationStoppingRegistration.Dispose(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/WindowsFormsLifetime.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace WindowsFormsLifetime; 6 | 7 | /// 8 | /// Listens for the startup to close, then initiates shutdown. 9 | /// 10 | public class WindowsFormsLifetime : IHostLifetime, IDisposable 11 | { 12 | private CancellationTokenRegistration _applicationStartedRegistration; 13 | private CancellationTokenRegistration _applicationStoppingRegistration; 14 | private readonly WindowsFormsLifetimeOptions _options; 15 | private readonly IHostEnvironment _environment; 16 | private readonly IHostApplicationLifetime _applicationLifetime; 17 | private readonly ILogger _logger; 18 | 19 | public WindowsFormsLifetime( 20 | IOptions options, 21 | IHostEnvironment environment, 22 | IHostApplicationLifetime hostApplicationLifetime, 23 | ILoggerFactory loggerFactory) 24 | { 25 | _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); 26 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 27 | _applicationLifetime = hostApplicationLifetime ?? throw new ArgumentNullException(nameof(hostApplicationLifetime)); 28 | _logger = loggerFactory?.CreateLogger("Microsoft.Hosting.Lifetime") ?? throw new ArgumentNullException(nameof(loggerFactory)); 29 | } 30 | 31 | public Task WaitForStartAsync(CancellationToken cancellationToken) 32 | { 33 | if (!_options.SuppressStatusMessages) 34 | { 35 | _applicationStartedRegistration = _applicationLifetime.ApplicationStarted.Register(state => 36 | { 37 | ((WindowsFormsLifetime)state).OnApplicationStarted(); 38 | }, 39 | this); 40 | _applicationStoppingRegistration = _applicationLifetime.ApplicationStopping.Register(state => 41 | { 42 | ((WindowsFormsLifetime)state).OnApplicationStopping(); 43 | }, 44 | this); 45 | } 46 | 47 | if (_options.EnableConsoleShutdown) 48 | { 49 | Console.CancelKeyPress += OnCancelKeyPress; 50 | } 51 | 52 | // Windows Forms applications start immediately. 53 | return Task.CompletedTask; 54 | } 55 | 56 | private void OnApplicationStarted() 57 | { 58 | _logger.LogInformation("Application started. Close the startup Form" + (_options.EnableConsoleShutdown ? " or press Ctrl+C" : string.Empty) + " to shut down."); 59 | _logger.LogInformation("Hosting environment: {envName}", _environment.EnvironmentName); 60 | _logger.LogInformation("Content root path: {contentRoot}", _environment.ContentRootPath); 61 | } 62 | 63 | private void OnApplicationStopping() 64 | { 65 | _logger.LogInformation("Application is shutting down..."); 66 | } 67 | 68 | private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs e) 69 | { 70 | e.Cancel = true; 71 | _applicationLifetime.StopApplication(); 72 | } 73 | 74 | public Task StopAsync(CancellationToken cancellationToken) 75 | { 76 | // There's nothing to do here 77 | return Task.CompletedTask; 78 | } 79 | 80 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "")] 81 | public void Dispose() 82 | { 83 | _applicationStartedRegistration.Dispose(); 84 | _applicationStoppingRegistration.Dispose(); 85 | 86 | if (_options.EnableConsoleShutdown) 87 | { 88 | Console.CancelKeyPress -= OnCancelKeyPress; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/WindowsFormsLifetime.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0-windows;net9.0-windows 5 | disable 6 | enable 7 | OswaldTechnologies.Extensions.Hosting.WindowsFormsLifetime 8 | 1.2.0 9 | Alex Oswald 10 | Oswald Technologies, LLC 11 | .NET Core hosting infrastructure for Windows Forms. 12 | https://github.com/alex-oswald/WindowsFormsLifetime 13 | git 14 | WindowsFormsLifetime 15 | WindowsFormsLifetime 16 | true 17 | true 18 | snupkg 19 | https://github.com/alex-oswald/WindowsFormsLifetime 20 | README.md 21 | MIT 22 | netcore;hosting;winforms 23 | true 24 | true 25 | true 26 | true 27 | CS1591 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | all 37 | runtime; build; native; contentfiles; analyzers; buildtransitive 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/WindowsFormsLifetimeOptions.cs: -------------------------------------------------------------------------------- 1 | namespace WindowsFormsLifetime; 2 | 3 | public class WindowsFormsLifetimeOptions 4 | { 5 | /// 6 | /// Indicates the . 7 | /// The default is . 8 | /// 9 | public HighDpiMode HighDpiMode { get; set; } = HighDpiMode.SystemAware; 10 | 11 | /// 12 | /// Indicates if visual styles are enabled. 13 | /// The default is true. 14 | /// 15 | public bool EnableVisualStyles { get; set; } = true; 16 | 17 | /// 18 | /// Indicates if compatible text rendering is enabled. 19 | /// The default is false. 20 | /// 21 | public bool CompatibleTextRenderingDefault { get; set; } 22 | 23 | /// 24 | /// Indicates if host lifetime status messages should be supressed such as on startup. 25 | /// The default is false. 26 | /// 27 | public bool SuppressStatusMessages { get; set; } 28 | 29 | /// 30 | /// Enables listening for Ctrl+C to additionally initiate shutdown. 31 | /// The default is false. 32 | /// 33 | public bool EnableConsoleShutdown { get; set; } 34 | } 35 | -------------------------------------------------------------------------------- /src/WindowsFormsLifetime/WindowsFormsSynchronizationContextProvider.cs: -------------------------------------------------------------------------------- 1 | namespace WindowsFormsLifetime; 2 | 3 | public interface IGuiContext 4 | { 5 | void Invoke(Action action); 6 | 7 | TResult Invoke(Func func); 8 | 9 | Task InvokeAsync(Func func); 10 | 11 | Task InvokeAsync(Func func, TInput input); 12 | } 13 | 14 | public interface IWindowsFormsSynchronizationContextProvider 15 | { 16 | /// 17 | /// Gets the for the UI thread. 18 | /// 19 | WindowsFormsSynchronizationContext SynchronizationContext { get; } 20 | } 21 | 22 | public class WindowsFormsSynchronizationContextProvider : IWindowsFormsSynchronizationContextProvider, IGuiContext 23 | { 24 | public WindowsFormsSynchronizationContext SynchronizationContext { get; internal set; } 25 | 26 | public void Invoke(Action action) => SynchronizationContext.Invoke(action); 27 | 28 | public TResult Invoke(Func func) => SynchronizationContext.Invoke(func); 29 | 30 | public async Task InvokeAsync(Func func) => await SynchronizationContext.InvokeAsync(func); 31 | 32 | public async Task InvokeAsync(Func func, TInput input) => await SynchronizationContext.InvokeAsync(func, input); 33 | } 34 | -------------------------------------------------------------------------------- /tests/WindowsFormsLifetime.Tests/FormProviderTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using System.Windows.Forms; 4 | using WindowsFormsLifetime; 5 | using Xunit; 6 | using System.ComponentModel; 7 | 8 | namespace WindowsFormsLifetimeTests; 9 | 10 | // Put both test classes into the same collection so that their tests are not run in parallel. 11 | // Otherwise tests fail if both tests run a Host concurrently. 12 | [Collection("Host tests")] 13 | public class FormProviderTests(FormProviderTests.HostFixture host) : IClassFixture 14 | { 15 | private readonly HostFixture _host = host; 16 | 17 | public class HostFixture : IDisposable 18 | { 19 | public IHost Host { get; init; } 20 | public CancellationTokenSource TokenSource { get; init; } 21 | public Task HostTask { get; init; } 22 | 23 | public HostFixture() 24 | { 25 | var hostBuilder = new HostBuilder() 26 | .UseWindowsFormsLifetime() 27 | .ConfigureServices(services => 28 | { 29 | services.AddScoped(); 30 | services.AddSingleton(); 31 | services.AddTransient(); 32 | services.AddTransient(); 33 | }); 34 | Host = hostBuilder.Build(); 35 | 36 | TokenSource = new(); 37 | HostTask = Host.RunAsync(TokenSource.Token); 38 | 39 | Thread.Sleep(2000); 40 | } 41 | 42 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "")] 43 | public void Dispose() 44 | { 45 | TokenSource.Cancel(); 46 | } 47 | } 48 | 49 | public abstract class Dependency : IDisposable 50 | { 51 | public bool IsDisposed { get; private set; } 52 | 53 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "")] 54 | public void Dispose() => IsDisposed = true; 55 | } 56 | 57 | public class ScopedDependency : Dependency 58 | { 59 | } 60 | 61 | public class SingletonDependency : Dependency 62 | { 63 | } 64 | 65 | public class TransientDependency : Dependency 66 | { 67 | } 68 | 69 | public class TestFormWithDependencies( 70 | ScopedDependency scopedDependency, 71 | SingletonDependency singletonDependency, 72 | TransientDependency transientDependency) : Form 73 | { 74 | [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 75 | public ScopedDependency ScopedDependency { get; init; } = scopedDependency; 76 | 77 | [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 78 | public SingletonDependency SingletonDependency { get; init; } = singletonDependency; 79 | 80 | [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 81 | public TransientDependency TransientDependency { get; init; } = transientDependency; 82 | 83 | protected override void SetVisibleCore(bool value) 84 | { 85 | // Don't flash window when running unit tests 86 | base.SetVisibleCore(false); 87 | 88 | if (!IsHandleCreated) 89 | { 90 | CreateHandle(); 91 | OnLoad(EventArgs.Empty); 92 | } 93 | } 94 | } 95 | 96 | [Fact] 97 | public void Dependencies_Not_Disposed_Without_A_Scope() 98 | { 99 | ScopedDependency? scopedDep = null; 100 | SingletonDependency? singletonDep = null; 101 | TransientDependency? transientDep = null; 102 | using (var form = _host.Host.Services.GetService()) 103 | { 104 | Assert.NotNull(form); 105 | 106 | scopedDep = form.ScopedDependency; 107 | Assert.False(scopedDep.IsDisposed, "ScopedDependency is disposed, but should not be disposed."); 108 | 109 | singletonDep = form.SingletonDependency; 110 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 111 | 112 | transientDep = form.TransientDependency; 113 | Assert.False(transientDep.IsDisposed, "TransientDependency is disposed, but should not be disposed."); 114 | } 115 | 116 | // Scoped or transient dependencies won't be disposed without a scope. 117 | Assert.False(scopedDep.IsDisposed, "ScopedDependency is disposed, but should not be disposed."); 118 | 119 | Assert.False(transientDep.IsDisposed, "TransientDependency is disposed, but should not be disposed."); 120 | 121 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 122 | } 123 | 124 | [Fact] 125 | public void Dependencies_Disposed_With_Scope() 126 | { 127 | ScopedDependency? scopedDep = null; 128 | SingletonDependency? singletonDep = null; 129 | TransientDependency? transientDep = null; 130 | using (var form = _host.Host.Services.GetRequiredService().GetForm()) 131 | { 132 | Assert.NotNull(form); 133 | 134 | scopedDep = form.ScopedDependency; 135 | Assert.False(scopedDep.IsDisposed, "ScopedDependency is disposed, but should not be disposed."); 136 | 137 | singletonDep = form.SingletonDependency; 138 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 139 | 140 | transientDep = form.TransientDependency; 141 | Assert.False(transientDep.IsDisposed, "TransientDependency is disposed, but should not be disposed."); 142 | } 143 | 144 | // Scoped or transient dependencies will be disposed with a scope. 145 | Assert.True(scopedDep.IsDisposed, "ScopedDependency is not disposed, but should be disposed."); 146 | 147 | Assert.True(transientDep.IsDisposed, "TransientDependency is not disposed, but should be disposed."); 148 | 149 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 150 | } 151 | 152 | [Fact] 153 | public async Task Dependencies_Disposed_With_Scope_Async() 154 | { 155 | ScopedDependency? scopedDep = null; 156 | SingletonDependency? singletonDep = null; 157 | TransientDependency? transientDep = null; 158 | using (var form = await _host.Host.Services.GetRequiredService().GetFormAsync()) 159 | { 160 | Assert.NotNull(form); 161 | 162 | scopedDep = form.ScopedDependency; 163 | Assert.False(scopedDep.IsDisposed, "ScopedDependency is disposed, but should not be disposed."); 164 | 165 | singletonDep = form.SingletonDependency; 166 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 167 | 168 | transientDep = form.TransientDependency; 169 | Assert.False(transientDep.IsDisposed, "TransientDependency is disposed, but should not be disposed."); 170 | } 171 | 172 | // Scoped or transient dependencies will be disposed with a scope. 173 | Assert.True(scopedDep.IsDisposed, "ScopedDependency is not disposed, but should be disposed."); 174 | 175 | Assert.True(transientDep.IsDisposed, "TransientDependency is not disposed, but should be disposed."); 176 | 177 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 178 | } 179 | 180 | [Fact] 181 | public void Dependencies_Disposed_With_Shared_Scope() 182 | { 183 | ScopedDependency? scopedDep = null; 184 | SingletonDependency? singletonDep = null; 185 | TransientDependency? transientDep = null; 186 | using (var scope = _host.Host.Services.GetRequiredService().CreateScope()) 187 | { 188 | using (var form = _host.Host.Services.GetRequiredService().GetForm(scope)) 189 | { 190 | Assert.NotNull(form); 191 | 192 | scopedDep = form.ScopedDependency; 193 | Assert.False(scopedDep.IsDisposed, "ScopedDependency is disposed, but should not be disposed."); 194 | 195 | singletonDep = form.SingletonDependency; 196 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 197 | 198 | transientDep = form.TransientDependency; 199 | Assert.False(transientDep.IsDisposed, "TransientDependency is disposed, but should not be disposed."); 200 | 201 | using (var form2 = _host.Host.Services.GetRequiredService().GetForm(scope)) 202 | { 203 | Assert.NotNull(form2); 204 | 205 | // Separate forms were created 206 | Assert.NotSame(form, form2); 207 | 208 | // Transient dependencies should not the same 209 | Assert.NotSame(form.TransientDependency, form2.TransientDependency); 210 | 211 | // Scoped dependencies should be the same 212 | Assert.Same(form.ScopedDependency, form2.ScopedDependency); 213 | 214 | // Singleton instances should be the same 215 | Assert.Same(form.SingletonDependency, form2.SingletonDependency); 216 | } 217 | } 218 | 219 | // Dependencies are not disposed because the scope is not disposed. 220 | Assert.False(scopedDep.IsDisposed, "ScopedDependency is disposed, but should not be disposed."); 221 | 222 | Assert.False(transientDep.IsDisposed, "TransientDependency is disposed, but should not be disposed."); 223 | 224 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 225 | } // dispose scope 226 | 227 | // Scoped or transient dependencies will be disposed after the scope is disposed. 228 | Assert.True(scopedDep.IsDisposed, "ScopedDependency is not disposed, but should be disposed."); 229 | 230 | Assert.True(transientDep.IsDisposed, "TransientDependency is not disposed, but should be disposed."); 231 | 232 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 233 | } 234 | 235 | [Fact] 236 | public async Task Dependencies_Disposed_With_Shared_Scope_Async() 237 | { 238 | ScopedDependency? scopedDep = null; 239 | SingletonDependency? singletonDep = null; 240 | TransientDependency? transientDep = null; 241 | using (var scope = _host.Host.Services.GetRequiredService().CreateScope()) 242 | { 243 | using (var form = await _host.Host.Services.GetRequiredService().GetFormAsync(scope)) 244 | { 245 | Assert.NotNull(form); 246 | 247 | scopedDep = form.ScopedDependency; 248 | Assert.False(scopedDep.IsDisposed, "ScopedDependency is disposed, but should not be disposed."); 249 | 250 | singletonDep = form.SingletonDependency; 251 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 252 | 253 | transientDep = form.TransientDependency; 254 | Assert.False(transientDep.IsDisposed, "TransientDependency is disposed, but should not be disposed."); 255 | 256 | using (var form2 = await _host.Host.Services.GetRequiredService().GetFormAsync(scope)) 257 | { 258 | Assert.NotNull(form2); 259 | 260 | // Separate forms were created 261 | Assert.NotSame(form, form2); 262 | 263 | // Transient dependencies should not be the same 264 | Assert.NotSame(form.TransientDependency, form2.TransientDependency); 265 | 266 | // Scoped dependencies should be the same 267 | Assert.Same(form.ScopedDependency, form2.ScopedDependency); 268 | 269 | // Singleton instances should be the same 270 | Assert.Same(form.SingletonDependency, form2.SingletonDependency); 271 | } 272 | } 273 | 274 | // Dependencies are not disposed because the scope is not disposed. 275 | Assert.False(scopedDep.IsDisposed, "ScopedDependency is disposed, but should not be disposed."); 276 | 277 | Assert.False(transientDep.IsDisposed, "TransientDependency is disposed, but should not be disposed."); 278 | 279 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 280 | } // dispose scope 281 | 282 | // Scoped or transient dependencies will be disposed after the scope is disposed. 283 | Assert.True(scopedDep.IsDisposed, "ScopedDependency is not disposed, but should be disposed."); 284 | 285 | Assert.True(transientDep.IsDisposed, "TransientDependency is not disposed, but should be disposed."); 286 | 287 | Assert.False(singletonDep.IsDisposed, "SingletonDependency is disposed, but should not be disposed."); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /tests/WindowsFormsLifetime.Tests/WindowsFormsLifetimeTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using System.Windows.Forms; 4 | using WindowsFormsLifetime; 5 | using Xunit; 6 | using Timer = System.Windows.Forms.Timer; 7 | 8 | namespace WindowsFormsLifetimeTests; 9 | 10 | // Put both test classes into the same collection so that their tests are not run in parallel. 11 | // Otherwise tests fail if both tests run a Host concurrently. 12 | [Collection("Host tests")] 13 | public class WindowsFormsLifetimeTests 14 | { 15 | public class TestForm : Form 16 | { 17 | protected override void SetVisibleCore(bool value) 18 | { 19 | // Don't flash window when running unit tests 20 | base.SetVisibleCore(false); 21 | 22 | if (!IsHandleCreated) 23 | { 24 | CreateHandle(); 25 | OnLoad(EventArgs.Empty); 26 | } 27 | } 28 | } 29 | 30 | public class TestContext : ApplicationContext 31 | { 32 | public TestContext(Action? onStart = null) 33 | { 34 | // Let's invoke this after constructor has been run 35 | var timer = new Timer { Interval = 1, Enabled = true }; 36 | timer.Tick += (sender, args) => 37 | { 38 | timer.Enabled = false; 39 | onStart?.Invoke(this); 40 | }; 41 | } 42 | } 43 | 44 | [Fact] 45 | public void Services_Available_With_Form() 46 | { 47 | var hostBuilder = new HostBuilder().UseWindowsFormsLifetime(); 48 | 49 | using var host = hostBuilder.Build(); 50 | 51 | Assert.IsType(host.Services.GetService()); 52 | Assert.IsType(host.Services.GetService()); 53 | Assert.NotNull(host.Services.GetService()); 54 | Assert.NotNull(host.Services.GetService()); 55 | Assert.NotNull(host.Services.GetService()); 56 | } 57 | 58 | [Fact] 59 | public void Services_Available_With_ApplicationContext() 60 | { 61 | var hostBuilder = new HostBuilder().UseWindowsFormsLifetime(); 62 | 63 | using var host = hostBuilder.Build(); 64 | 65 | Assert.IsType(host.Services.GetService()); 66 | Assert.IsType(host.Services.GetService()); 67 | Assert.NotNull(host.Services.GetService()); 68 | Assert.NotNull(host.Services.GetService()); 69 | Assert.NotNull(host.Services.GetService()); 70 | Assert.Null(host.Services.GetService()); 71 | } 72 | 73 | [Fact] 74 | public void Services_Available_With_ApplicationContext_Form() 75 | { 76 | var hostBuilder = new HostBuilder().UseWindowsFormsLifetime((form) => new TestContext()); 77 | 78 | using var host = hostBuilder.Build(); 79 | 80 | Assert.IsType(host.Services.GetService()); 81 | Assert.IsType(host.Services.GetService()); 82 | Assert.NotNull(host.Services.GetService()); 83 | Assert.NotNull(host.Services.GetService()); 84 | Assert.NotNull(host.Services.GetService()); 85 | Assert.NotNull(host.Services.GetService()); 86 | } 87 | 88 | [Fact] 89 | public async Task Should_Run_And_Close_Form() 90 | { 91 | using var host = new HostBuilder().UseWindowsFormsLifetime().Build(); 92 | 93 | var form = host.Services.GetService(); 94 | form!.Load += (sender, args) => form.Invoke(new Action(Application.Exit)); 95 | 96 | await host.RunAsync(); 97 | 98 | // If we are here, nothing failed 99 | } 100 | 101 | [Fact] 102 | public async Task Should_Run_And_Close_Form_When_Cancelling() 103 | { 104 | using var host = new HostBuilder().UseWindowsFormsLifetime().Build(); 105 | using var cancelToken = new CancellationTokenSource(); 106 | 107 | var form = host.Services.GetService(); 108 | form!.Load += (sender, args) => cancelToken.Cancel(); 109 | 110 | await host.RunAsync(cancelToken.Token); 111 | 112 | // If we are here, nothing failed 113 | } 114 | 115 | [Fact] 116 | public async Task Should_Run_And_Close_ApplicationContext() 117 | { 118 | using var host = new HostBuilder() 119 | .UseWindowsFormsLifetime() 120 | .ConfigureServices(services => services.AddSingleton>(context => Application.Exit())) 121 | .Build(); 122 | 123 | await host.RunAsync(); 124 | 125 | // If we are here, nothing failed 126 | } 127 | 128 | [Fact] 129 | public async Task Should_Run_And_Close_ApplicationContext_When_Cancelling() 130 | { 131 | using var cancelToken = new CancellationTokenSource(); 132 | using var host = new HostBuilder() 133 | .UseWindowsFormsLifetime() 134 | .ConfigureServices(services => services.AddSingleton>(_ => cancelToken.Cancel())) 135 | .Build(); 136 | 137 | await host.RunAsync(cancelToken.Token); 138 | 139 | // If we are here, nothing failed 140 | } 141 | } -------------------------------------------------------------------------------- /tests/WindowsFormsLifetime.Tests/WindowsFormsLifetimeTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0-windows 5 | false 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------