Last active
March 25, 2025 13:22
-
-
Save rkttu/b056dc4b5e3e25744f65d6764af4c5af to your computer and use it in GitHub Desktop.
Windows Forms + Generic Host + Dependency Injection + MVVM + Command in .NET 8
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
<!-- Project Configuration --> | |
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>WinExe</OutputType> | |
<TargetFramework>net8.0-windows</TargetFramework> | |
<Nullable>enable</Nullable> | |
<UseWindowsForms>true</UseWindowsForms> | |
<UseWPF>false</UseWPF> | |
<ImplicitUsings>disable</ImplicitUsings> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> | |
</ItemGroup> | |
</Project> | |
*/ | |
#nullable enable | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.Extensions.Logging; | |
using System; | |
using System.ComponentModel; | |
using System.Diagnostics; | |
using System.Drawing; | |
using System.IO; | |
using System.Linq; | |
using System.Runtime.CompilerServices; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using System.Windows.Forms; | |
using System.Windows.Input; | |
internal static class Program | |
{ | |
private static async Task MainAsync(string[] args) | |
{ | |
using var cancellationTokenSource = new CancellationTokenSource(); | |
args ??= Environment.GetCommandLineArgs().Skip(1).ToArray(); | |
var builder = Host.CreateApplicationBuilder(args); | |
builder.Configuration | |
.AddCommandLine(args) | |
.AddEnvironmentVariables() | |
.AddUserSecrets(typeof(Program).Assembly, true); | |
builder.Logging | |
.AddConsole() | |
.AddDebug(); | |
builder.Services.AddSingleton<MainForm>(sp => | |
{ | |
var logger = sp.GetRequiredService<ILogger<MainForm>>(); | |
var viewModel = sp.GetRequiredService<MainFormViewModel>(); | |
return new MainForm(logger, viewModel) | |
{ | |
Text = "Hello, MVVM with DI in WinForm 8", | |
}; | |
}); | |
builder.Services.AddSingleton<MainFormViewModel>(_ => | |
{ | |
return new MainFormViewModel() | |
{ | |
Username = "James", | |
Age = 22, | |
Contact = "010-1234-1234", | |
Memo = "Hello!", | |
Active = false, | |
}; | |
}); | |
builder.Services.AddSingleton<ApplicationContext>(sp => | |
{ | |
var mainForm = sp.GetRequiredService<MainForm>(); | |
return new ApplicationContext(mainForm); | |
}); | |
builder.Services.AddHostedService<WindowsApp>(); | |
var app = builder.Build(); | |
await app.RunAsync(cancellationTokenSource.Token); | |
} | |
[STAThread] | |
private static int Main(string[] args) | |
{ | |
try | |
{ | |
MainAsync(args).GetAwaiter().GetResult(); | |
} | |
catch (Exception ex) | |
{ | |
Environment.ExitCode = 1; | |
Debug.WriteLine(ex.ToString()); | |
Console.Error.WriteLine(ex.ToString()); | |
} | |
return Environment.ExitCode; | |
} | |
} | |
public sealed class WindowsApp( | |
IHostApplicationLifetime HostAppLifetime, | |
ApplicationContext WinformAppContext | |
) : BackgroundService | |
{ | |
protected override Task ExecuteAsync(CancellationToken stoppingToken) | |
{ | |
return Task.Run(() => | |
{ | |
Application.OleRequired(); | |
Application.EnableVisualStyles(); | |
Application.SetHighDpiMode(HighDpiMode.SystemAware); | |
Application.Run(WinformAppContext); | |
}, stoppingToken) | |
.ContinueWith(_ => | |
{ | |
HostAppLifetime.StopApplication(); | |
}, stoppingToken); | |
} | |
} | |
public sealed class MainForm : Form | |
{ | |
public MainForm( | |
ILogger<MainForm> logger, | |
MainFormViewModel viewModel) | |
{ | |
_logger = logger; | |
_viewModel = viewModel; | |
_viewModel.RequestClose += (_sender, _e) => | |
{ | |
Close(); | |
}; | |
} | |
private ILogger _logger; | |
private MainFormViewModel _viewModel; | |
protected override void OnLoad(EventArgs e) | |
{ | |
base.OnLoad(e); | |
SuspendLayout(); | |
DoubleBuffered = true; | |
Size = new Size(640, 480); | |
Visible = true; | |
DataContext = _viewModel; | |
MinimumSize = new Size(640, 480); | |
MaximumSize = new Size(800, 600); | |
var tableLayout = new TableLayoutPanel() | |
{ | |
Parent = this, | |
Dock = DockStyle.Fill, | |
Padding = new Padding(10), | |
}; | |
// 표 레이아웃에 3행 추가 | |
tableLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); | |
tableLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); | |
tableLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); | |
tableLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize, 80f)); | |
tableLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); | |
tableLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); | |
// 표 레이아웃에 2개 열 추가 | |
tableLayout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize, 30f)); | |
tableLayout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize, 70f)); | |
// 이름 라벨 추가 | |
var nameLabel = new Label() | |
{ | |
Parent = tableLayout, | |
Text = "Name: ", | |
Dock = DockStyle.Fill, | |
TextAlign = ContentAlignment.MiddleRight, | |
}; | |
tableLayout.SetCellPosition(nameLabel, new TableLayoutPanelCellPosition(row: 1, column: 1)); | |
// 이름 텍스트박스 추가 | |
var nameTextbox = new TextBox() | |
{ | |
Parent = tableLayout, | |
Dock = DockStyle.Fill, | |
}; | |
nameTextbox.DataBindings.Add(new Binding( | |
nameof(nameTextbox.Text), | |
DataContext, | |
nameof(_viewModel.Username), | |
false, | |
DataSourceUpdateMode.OnPropertyChanged | |
)); | |
tableLayout.SetCellPosition(nameTextbox, new TableLayoutPanelCellPosition(row: 1, column: 2)); | |
// 나이 라벨 추가 | |
var ageLabel = new Label() | |
{ | |
Parent = tableLayout, | |
Text = "Age: ", | |
Dock = DockStyle.Fill, | |
TextAlign = ContentAlignment.MiddleRight, | |
}; | |
tableLayout.SetCellPosition(ageLabel, new TableLayoutPanelCellPosition(row: 2, column: 1)); | |
// 나이 숫자 텍스트박스 추가 | |
var ageUpDown = new NumericUpDown() | |
{ | |
Parent = tableLayout, | |
Dock = DockStyle.Fill, | |
Minimum = 0m, | |
Maximum = 200m, | |
}; | |
ageUpDown.DataBindings.Add(new Binding( | |
nameof(ageUpDown.Value), | |
DataContext, | |
nameof(_viewModel.Age), | |
false, | |
DataSourceUpdateMode.OnPropertyChanged | |
)); | |
tableLayout.SetCellPosition(ageUpDown, new TableLayoutPanelCellPosition(row: 2, column: 2)); | |
// 연락처 라벨 추가 | |
var contactLabel = new Label() | |
{ | |
Parent = tableLayout, | |
Text = "Contact: ", | |
Dock = DockStyle.Fill, | |
TextAlign = ContentAlignment.MiddleRight, | |
}; | |
tableLayout.SetCellPosition(contactLabel, new TableLayoutPanelCellPosition(row: 3, column: 1)); | |
// 연락처 마스크 텍스트 박스 추가 | |
var contactTextbox = new MaskedTextBox() | |
{ | |
Parent = tableLayout, | |
TextMaskFormat = MaskFormat.ExcludePromptAndLiterals, | |
Dock = DockStyle.Fill, | |
}; | |
contactTextbox.DataBindings.Add(new Binding( | |
nameof(contactTextbox.Text), | |
DataContext, | |
nameof(_viewModel.Contact), | |
false, | |
DataSourceUpdateMode.OnPropertyChanged | |
)); | |
tableLayout.SetCellPosition(contactTextbox, new TableLayoutPanelCellPosition(row: 3, column: 2)); | |
// 메모 텍스트박스 추가 (여러 줄) | |
var memoTextbox = new TextBox() | |
{ | |
Parent = tableLayout, | |
Dock = DockStyle.Fill, | |
Multiline = true, | |
Height = 120, | |
}; | |
memoTextbox.DataBindings.Add(new Binding( | |
nameof(memoTextbox.Text), | |
DataContext, | |
nameof(_viewModel.Memo), | |
false, | |
DataSourceUpdateMode.OnPropertyChanged | |
)); | |
tableLayout.SetCellPosition(memoTextbox, new TableLayoutPanelCellPosition(row: 4, column: 1)); | |
tableLayout.SetColumnSpan(memoTextbox, 2); | |
// 활성 회원 체크박스 추가 | |
var activeCheckbox = new CheckBox() | |
{ | |
Parent = tableLayout, | |
Dock = DockStyle.Fill, | |
Text = "Active member", | |
AutoSize = true, | |
}; | |
activeCheckbox.DataBindings.Add(new Binding( | |
nameof(activeCheckbox.Checked), | |
DataContext, | |
nameof(_viewModel.Active), | |
false, | |
DataSourceUpdateMode.OnPropertyChanged | |
)); | |
tableLayout.SetCellPosition(activeCheckbox, new TableLayoutPanelCellPosition(row: 5, column: 1)); | |
tableLayout.SetColumnSpan(activeCheckbox, 2); | |
// 버튼 패널 추가 | |
var buttonPanel = new FlowLayoutPanel() | |
{ | |
Parent = tableLayout, | |
Dock = DockStyle.Fill, | |
FlowDirection = FlowDirection.RightToLeft, | |
AutoSize = true, | |
}; | |
tableLayout.SetCellPosition(buttonPanel, new TableLayoutPanelCellPosition(row: 6, column: 1)); | |
tableLayout.SetColumnSpan(buttonPanel, 2); | |
// 확인 버튼 추가 | |
var okayButton = new Button() | |
{ | |
Parent = buttonPanel, | |
Text = "OK", | |
DialogResult = DialogResult.OK, | |
AutoSize = true, | |
}; | |
okayButton.DataBindings.Add(new Binding( | |
nameof(okayButton.Command), | |
DataContext, | |
nameof(_viewModel.AcceptCommand), | |
true, | |
DataSourceUpdateMode.OnPropertyChanged | |
)); | |
this.AcceptButton = okayButton; | |
// 취소 버튼 추가 | |
var cancelButton = new Button() | |
{ | |
Parent = buttonPanel, | |
Text = "Cancel", | |
DialogResult = DialogResult.Cancel, | |
AutoSize = true, | |
}; | |
cancelButton.DataBindings.Add(new Binding( | |
nameof(cancelButton.Command), | |
DataContext, | |
nameof(_viewModel.CancelCommand), | |
true, | |
DataSourceUpdateMode.OnPropertyChanged | |
)); | |
// RightToLeft 레이아웃에서 취소 버튼이 오른쪽에 먼저 표시되도록 수정 | |
cancelButton.BringToFront(); | |
this.CancelButton = cancelButton; | |
ResumeLayout(); | |
_logger.LogInformation("Application has been loaded."); | |
} | |
} | |
public sealed class MainFormViewModel : INotifyPropertyChanged | |
{ | |
public MainFormViewModel() | |
{ | |
_acceptCommand = new MainFormAcceptCommand(this); | |
_cancelCommand = new MainFormCancelCommand(this); | |
} | |
public ICommand AcceptCommand => _acceptCommand; | |
public ICommand CancelCommand => _cancelCommand; | |
public event PropertyChangedEventHandler? PropertyChanged; | |
private void OnPropertyChanged([CallerMemberName] string propertyName = "") | |
{ | |
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | |
_acceptCommand.RaiseCanExecuteChanged(); | |
_cancelCommand.RaiseCanExecuteChanged(); | |
} | |
private string _username = string.Empty; | |
private int _age = 21; | |
private string _contact = string.Empty; | |
private string _memo = string.Empty; | |
private bool _active = true; | |
private MainFormAcceptCommand _acceptCommand; | |
private MainFormCancelCommand _cancelCommand; | |
public string Username | |
{ | |
get => _username; | |
set | |
{ | |
if (_username != value) | |
{ | |
_username = value; | |
OnPropertyChanged(); | |
} | |
} | |
} | |
public int Age | |
{ | |
get => _age; | |
set | |
{ | |
if (_age != value) | |
{ | |
_age = value; | |
OnPropertyChanged(); | |
} | |
} | |
} | |
public string Contact | |
{ | |
get => _contact; | |
set | |
{ | |
if (_contact != value) | |
{ | |
_contact = value; | |
OnPropertyChanged(); | |
} | |
} | |
} | |
public string Memo | |
{ | |
get => _memo; | |
set | |
{ | |
if (_memo != value) | |
{ | |
_memo = value; | |
OnPropertyChanged(); | |
} | |
} | |
} | |
public bool Active | |
{ | |
get => _active; | |
set | |
{ | |
if (_active != value) | |
{ | |
_active = value; | |
OnPropertyChanged(); | |
} | |
} | |
} | |
public async Task SaveProfileDataAsync(string filePath) | |
{ | |
await File.WriteAllTextAsync( | |
filePath, | |
this.ToString(), | |
new UTF8Encoding(false)); | |
} | |
public void CloseMainWindow(object? parameter) | |
{ | |
SendOrPostCallback callback = _ => | |
{ | |
RequestClose?.Invoke(this, EventArgs.Empty); | |
}; | |
var context = SynchronizationContext.Current; | |
if (context != null) context.Send(callback, this); | |
else callback.Invoke(this); | |
} | |
public override string ToString() | |
=> $"Name: {_username}, Age: {_age}, Contact: {_contact}, Memo: {_memo}, Active: {_active}"; | |
public event EventHandler? RequestClose; | |
} | |
public sealed class MainFormAcceptCommand( | |
MainFormViewModel ViewModel | |
) : ICommand | |
{ | |
public event EventHandler? CanExecuteChanged; | |
public bool CanExecute(object? parameter) | |
=> ViewModel.Active; | |
public void Execute(object? parameter) | |
=> _ = ExecuteAsync(parameter); | |
public async Task ExecuteAsync(object? parameter) | |
{ | |
try | |
{ | |
await ViewModel.SaveProfileDataAsync( | |
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), | |
"Sample.txt")); | |
ViewModel.CloseMainWindow(parameter); | |
} | |
catch (Exception) { } | |
} | |
public void RaiseCanExecuteChanged() | |
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty); | |
} | |
public sealed class MainFormCancelCommand( | |
MainFormViewModel ViewModel | |
) : ICommand | |
{ | |
public event EventHandler? CanExecuteChanged; | |
public bool CanExecute(object? parameter) | |
=> true; | |
public void Execute(object? parameter) | |
=> ViewModel.CloseMainWindow(parameter); | |
public void RaiseCanExecuteChanged() | |
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment