"MainWindow.xaml, App.xaml, AssemblyInfo.cs를 왜 지우냐고요? 그게 DI를 막고 있거든요."
들어가며
Visual Studio에서 WPF 프로젝트를 새로 만들면 파일 3개가 딸려옵니다.
📁 WpfExplorer
├── App.xaml (+ App.xaml.cs)
├── MainWindow.xaml (+ MainWindow.xaml.cs)
└── AssemblyInfo.cs
그런데 제 프로젝트는 이렇게 생겼습니다.
📁 WpfExplorer
├── App.cs
└── Starter.cs
셋 다 지우고, 파일 두 개를 손으로 만들었습니다. 처음 보면 "굳이 왜?" 싶으실 겁니다. 이 글에서 그 이유를 처음부터 차근차근 풀어보겠습니다. 결론부터 말씀드리면 딱 한 문장입니다.
윈도우를 누가 만들 것인가 — 그 주도권을 WPF 프레임워크에서 뺏어와 DI 컨테이너에게 넘기기 위해서.
나머지 이야기는 전부 이 한 문장에서 파생됩니다. 자, 시작해 볼까요.
1. 그런데 WPF 앱은 대체 어디서 시작되나요?
C# 콘솔 앱을 만들어 보신 분은 아실 겁니다. 모든 .NET 프로그램은 Main 메서드에서 시작합니다. 예외는 없습니다.
static void Main(string[] args)
{
Console.WriteLine("여기서 시작!");
}
그럼 질문 하나 드리겠습니다.
WPF 프로젝트를 열어보면 Main이 어디에 있나요?
한번 찾아보세요. App.xaml.cs를 열어봐도 없고, MainWindow.xaml.cs를 열어봐도 없습니다. 그런데 F5를 누르면 앱이 실행됩니다. 이상하죠?
정답: 빌드할 때 자동으로 만들어집니다
비밀은 App.xaml에 있습니다. 이 파일을 우클릭해서 속성을 보면 빌드 작업(Build Action) 이 ApplicationDefinition으로 되어 있습니다. 다른 XAML 파일들이 Page인 것과 다릅니다.
이 표시를 본 MSBuild는 빌드할 때 몰래 코드를 하나 생성합니다. obj 폴더에 있는 App.g.cs라는 파일인데요, 열어보면 대충 이렇게 생겼습니다.
// obj/Debug/net8.0-windows/App.g.cs ← 자동 생성 파일
public partial class App : System.Windows.Application
{
public void InitializeComponent()
{
this.StartupUri = new System.Uri("MainWindow.xaml", UriKind.Relative);
}
[System.STAThreadAttribute()]
public static void Main()
{
App app = new App();
app.InitializeComponent();
app.Run();
}
}
여기 있었네요, Main이!
정리하면 이렇습니다.
내가 쓴 것 빌드가 만들어 준 것
| App.xaml (ApplicationDefinition) | Main 메서드 |
| StartupUri="MainWindow.xaml" | 앱 시작 시 MainWindow를 자동으로 띄우는 코드 |
이걸 알고 나면 다음 이야기가 술술 풀립니다.
2. 문제의 발단 — new를 손으로 치기 시작하면
이제 실전 앱을 만든다고 해봅시다. 파일 탐색기를 만들 거라, 파일 목록을 읽어오는 서비스가 필요합니다.
public class MainWindow : Window
{
private readonly FileService _fileService;
public MainWindow()
{
_fileService = new FileService(); // 일단 이렇게
}
}
잘 돌아갑니다. 그런데 시간이 지나면서 요구사항이 붙습니다.
- 파일 목록을 읽을 때 로그를 남겨야 한다 → FileService가 Logger를 필요로 함
- 로그는 파일로 저장해야 한다 → Logger가 LogWriter를 필요로 함
- 설정 파일에서 로그 경로를 읽어야 한다 → LogWriter가 ConfigService를 필요로 함
그럼 코드가 이렇게 됩니다.
public MainWindow()
{
_fileService = new FileService(
new Logger(
new LogWriter(
new ConfigService()
)
)
);
}
이걸 의존성 지옥이라고 부릅니다. 그리고 진짜 문제는 이겁니다.
- Logger를 쓰는 곳이 10군데면, 이 new 덩어리를 10번 복붙해야 합니다.
- Logger 생성자에 인자가 하나 추가되면? 10군데를 전부 고쳐야 합니다.
- 테스트할 때 FileService를 가짜(Mock)로 바꾸고 싶은데, new로 박아놨으니 바꿀 방법이 없습니다.
해결책: 조립은 남에게 맡기자
그래서 나온 게 DI(Dependency Injection, 의존성 주입) 입니다. 발상은 아주 단순합니다.
"나는 FileService가 필요하다고만 말할게. 그걸 만드는 건 네가 해."
말로 하면 이렇게 바뀝니다.
public class MainWindow : Window
{
private readonly FileService _fileService;
// 필요한 걸 생성자로 "받기만" 한다. 만들지 않는다.
public MainWindow(FileService fileService)
{
_fileService = fileService;
}
}
그럼 누가 만들어 주냐고요? DI 컨테이너가 만들어 줍니다. 이렇게 미리 등록만 해두면 됩니다.
services.AddSingleton<ConfigService>();
services.AddSingleton<LogWriter>();
services.AddSingleton<Logger>();
services.AddSingleton<FileService>();
services.AddSingleton<MainWindow>();
이제 컨테이너에게 MainWindow 하나만 달라고 하면, 컨테이너가 알아서 순서를 계산해서 ConfigService → LogWriter → Logger → FileService → MainWindow 순으로 조립해서 완성품을 건네줍니다.
이게 우리가 쓸 Microsoft.Extensions.DependencyInjection 패키지가 하는 일입니다.
dotnet add package Microsoft.Extensions.DependencyInjection
3. 그런데 StartupUri가 이걸 막습니다
자, 이제 핵심입니다. 이 글의 심장부니까 집중해 주세요.
방금 배운 대로 MainWindow에 생성자를 만들어 봅시다.
public partial class MainWindow : Window
{
public MainWindow(FileService fileService) // 파라미터 추가!
{
InitializeComponent();
}
}
F5를 누릅니다. 그리고 터집니다.
1강에서 본 자동 생성 코드를 다시 떠올려 보세요.
this.StartupUri = new System.Uri("MainWindow.xaml", UriKind.Relative);
StartupUri는 WPF에게 이렇게 말하는 겁니다.
"앱이 켜지면 MainWindow.xaml을 찾아서, 직접 new로 만들어서 화면에 띄워줘."
여기서 문제가 발생합니다. WPF는 이 윈도우를 만들 때 매개변수 없는 생성자만 호출할 수 있습니다. FileService를 어디서 구해다 넣어줄지 WPF는 알 방법이 없으니까요.
정리하면 이렇습니다.
윈도우를 만드는 주체 결과
| StartupUri 방식 | WPF 프레임워크 | 매개변수 없는 생성자만 가능 → DI 불가 |
| 우리가 원하는 방식 | DI 컨테이너 | 생성자 주입 가능 |
주도권 다툼입니다. 둘 다 가질 수는 없어요. 그래서 우리는 StartupUri를 버리기로 합니다.
그런데 StartupUri는 App.xaml 안에 있고, App.xaml은 Main도 같이 만들어 주고 있었죠. 그래서 App.xaml을 지우면 Main도 같이 사라집니다.
바로 이 지점에서 Starter.cs가 등장합니다.
4. Starter.cs — 사라진 Main을 되찾기
App.xaml을 지웠으니 Main을 직접 써줘야 합니다.
namespace WpfExplorer
{
internal class Starter
{
[STAThread]
private static void Main(string[] args)
{
_ = new App().Run();
}
}
}
딱 5줄입니다. 그런데 이 5줄 안에 알아야 할 게 세 가지 있습니다.
① [STAThread]는 절대 빼먹으면 안 됩니다
STA는 Single-Threaded Apartment의 약자로, 윈도우의 오래된 COM 기술에서 온 개념입니다. 어렵게 생각할 필요 없고, 그냥 이렇게 기억하세요.
"윈도우 UI는 반드시 한 스레드에서만 다뤄야 한다" 는 규칙을 지키겠다는 선언.
클립보드, 드래그 앤 드롭, 파일 열기 대화상자 같은 기능들이 전부 이 규칙 위에서 동작합니다. 이걸 빼면 앱이 켜지자마자 이런 예외를 던지고 죽습니다.
System.InvalidOperationException:
The calling thread must be STA, because many UI components require this.
원래 자동 생성 Main에 붙어 있던 거라 있는지도 몰랐던 건데, 직접 쓰게 되니 챙겨야 하는 겁니다.
② Run()이 실제로 앱을 굴립니다
new App()은 그냥 객체 하나 만든 것뿐이고, 실제로 창을 띄우고 사용자 입력을 기다리는 건 Run()입니다. Run() 안에서는 대략 이런 무한 루프가 돌아갑니다.
메시지 대기 → 마우스 클릭 도착 → 버튼에게 전달 → 다시 대기 → ...
사용자가 창을 닫으면 이 루프가 끝나고, Run()이 반환되면서 프로그램이 종료됩니다. 그래서 Main이 5줄이어도 앱이 바로 꺼지지 않는 겁니다.
③ 왜 App에 안 넣고 굳이 Starter를 따로 만들까?
솔직히 말씀드리면, App 클래스 안에 static Main을 넣어도 똑같이 동작합니다. 이건 취향과 설계 철학의 문제인데, 이유를 대자면 이렇습니다.
- App의 역할 = 애플리케이션의 생명주기 관리 + DI 컨테이너 구성
- Starter의 역할 = 운영체제가 프로세스를 시작하는 진입 지점
이 둘은 성격이 다른 일입니다. 섞어놓으면 App 클래스가 "앱 그 자체"이면서 동시에 "앱을 켜는 사람"이 되어버려서, 읽을 때 살짝 혼란스럽습니다. 파일을 나눠두면 역할이 눈에 딱 보이죠.
5. App.cs 한 줄씩 뜯어보기
이제 본체입니다. 전체 코드를 먼저 보고, 블록별로 나눠서 보겠습니다.
using Microsoft.Extensions.DependencyInjection;
using System.Windows;
namespace WpfExplorer
{
internal class App : Application
{
private readonly ServiceProvider _provider;
public App()
{
ServiceCollection services = new();
RegisterTypes(services);
_provider = services.BuildServiceProvider();
}
private void RegisterTypes(IServiceCollection services)
{
}
private Window CreateShell()
{
return new Window();
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Window shell = CreateShell();
MainWindow = shell;
shell.Show();
}
protected override void OnExit(ExitEventArgs e)
{
_provider.Dispose();
base.OnExit(e);
}
}
}
생성자 — 부품 등록하고 공장 짓기
public App()
{
ServiceCollection services = new(); // ① 부품 목록표를 꺼낸다
RegisterTypes(services); // ② 목록표에 부품을 적는다
_provider = services.BuildServiceProvider(); // ③ 목록표로 공장을 짓는다
}
비유하자면 이렇습니다.
- ServiceCollection = 주문서. "이런 부품들이 필요합니다"라고 적는 종이.
- BuildServiceProvider() = 공장 준공. 주문서를 보고 실제로 물건을 찍어낼 수 있는 공장을 세우는 것.
- ServiceProvider = 완성된 공장. 이제 여기다 "MainWindow 하나 주세요" 하면 나옵니다.
여기서 중요한 건 순서입니다. 공장을 지은 뒤에는 주문서를 고쳐도 소용없습니다. 등록은 반드시 BuildServiceProvider() 이전에 끝나야 합니다.
RegisterTypes — 여기가 여러분이 채울 자리
지금은 비어 있지만, 실제 프로젝트에서는 이렇게 채웁니다.
private void RegisterTypes(IServiceCollection services)
{
// 인터페이스 ↔ 구현체 연결
services.AddSingleton<IFileService, FileService>();
services.AddSingleton<IDialogService, DialogService>();
// 뷰모델
services.AddSingleton<MainViewModel>();
services.AddTransient<FileItemViewModel>();
// 윈도우도 등록 대상입니다
services.AddSingleton<MainWindow>();
}
생명주기 3종 세트도 짚고 넘어가겠습니다.
메서드 의미 언제 쓰나
| AddSingleton | 앱 전체에서 딱 하나만 | 설정, 로거, 공용 서비스, 메인 윈도우 |
| AddTransient | 요청할 때마다 새로 | 목록의 각 항목 뷰모델, 매번 새로 띄우는 대화상자 |
| AddScoped | 스코프 안에서 하나 | 웹에서 주로 쓰고, 데스크톱에선 드묾 |
💡 팁: 매개변수가 ServiceCollection이 아니라 IServiceCollection(인터페이스)인 걸 눈여겨보세요. "너는 등록만 할 수 있어, 공장을 짓거나 다른 짓은 못 해"라고 권한을 좁혀준 겁니다. 작지만 좋은 습관입니다.
CreateShell — 3강에서 뺏어온 주도권을 행사하는 곳
Shell(셸)은 "앱의 껍데기", 즉 메인 윈도우를 뜻하는 이름입니다. 지금은 빈 창을 만들고 있지만,
private Window CreateShell()
{
return new Window(); // 임시
}
실제로는 이렇게 바꿉니다.
private Window CreateShell()
{
return _provider.GetRequiredService<MainWindow>();
}
이 한 줄이 이 글 전체의 결론입니다. 3강에서 WPF에게서 뺏어온 "윈도우 생성 권한"을, 여기서 DI 컨테이너에게 넘겨주고 있는 겁니다. 이제 MainWindow는 생성자에 뭘 요구하든 자유롭습니다.
참고로 RegisterTypes, CreateShell이라는 이름은 제가 지은 게 아닙니다. Prism이라는 유명한 WPF 프레임워크가 쓰는 이름을 그대로 가져온 겁니다. 즉 이 구조는 "Prism을 통째로 쓰긴 부담스러우니, 부트스트랩 부분만 손으로 흉내 내자" 에 가깝습니다. 나중에 Prism으로 갈아탈 때도 자연스럽게 이어집니다.
OnStartup — StartupUri의 대체품
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e); // ① 부모 먼저 (관례)
Window shell = CreateShell(); // ② 컨테이너에서 윈도우 꺼내기
MainWindow = shell; // ③ 대표 창으로 지정
shell.Show(); // ④ 화면에 띄우기
}
③번이 뭔지 궁금하실 텐데요. Application.MainWindow 프로퍼티는 "이 앱의 대표 창이 누구냐" 를 알려주는 값입니다. 이걸 지정해두면 Owner를 설정하지 않은 대화상자가 알아서 이 창을 기준으로 뜨고, 기본 종료 정책(ShutdownMode.OnMainWindowClose)도 제대로 동작합니다. StartupUri를 쓸 땐 WPF가 알아서 채워주던 값인데, 이제 우리가 채워야 합니다.
OnExit — 뒷정리
protected override void OnExit(ExitEventArgs e)
{
_provider.Dispose();
base.OnExit(e);
}
컨테이너가 만들어낸 객체 중에는 IDisposable을 구현한 것들이 있습니다. DB 커넥션, 파일 핸들, 파일 감시자(FileSystemWatcher), 네트워크 소켓 같은 것들이죠.
_provider.Dispose()를 호출하면 컨테이너가 자기가 만든 애들을 역순으로 돌면서 전부 정리해 줍니다. 이 한 줄이 없으면 앱을 껐는데도 파일이 잠겨 있거나, 커넥션이 남아 있는 문제가 생길 수 있습니다.
OnStartup은 base를 먼저 부르고, OnExit은 base를 나중에 부르는 것도 눈여겨보세요. 열 때는 안쪽부터, 닫을 때는 바깥쪽부터 — 자연스러운 순서입니다.
6. AssemblyInfo.cs는 왜 지우나요?
이건 앞의 이야기와 무관한, 별개의 정리 작업입니다.
옛날 .NET Framework 시절에는 어셈블리 버전, 회사명, 저작권 같은 정보를 이 파일에 적었습니다.
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyCompany("MyCompany")]
그런데 요즘 SDK 스타일 프로젝트(<Project Sdk="Microsoft.NET.Sdk">)에서는 이 정보를 csproj에서 관리하고, 빌드할 때 자동으로 생성해 줍니다.
<PropertyGroup>
<Version>1.0.0</Version>
<Company>MyCompany</Company>
</PropertyGroup>
그래서 AssemblyInfo.cs를 그대로 두면 같은 속성이 두 번 정의됐다며 컴파일 에러가 나거나, 운 좋게 안 나더라도 버전 정보를 관리하는 곳이 두 군데로 쪼개져서 헷갈립니다.
WPF 템플릿의 AssemblyInfo.cs에 실제로 남아 있는 내용은 보통 이거 하나입니다.
[assembly: ThemeInfo(ResourceDictionaryLocation.None,
ResourceDictionaryLocation.SourceAssembly)]
이건 커스텀 컨트롤 라이브러리가 Themes/Generic.xaml을 어디서 찾을지 알려주는 속성입니다. 라이브러리를 만드는 프로젝트에는 필요하지만, 실행 파일(애플리케이션) 프로젝트에는 사실상 의미가 없습니다. 그래서 지워도 무방합니다.
7. 잃어버린 것도 있습니다 — 리소스 딕셔너리
여기서 솔직하게 짚고 넘어가야 할 부분이 있습니다.
App.xaml을 지우면 InitializeComponent()도 함께 사라집니다. 그 말은 App.xaml에 있던 전역 리소스도 사라진다는 뜻입니다.
<!-- 이런 걸 넣을 곳이 없어졌습니다 -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/Dark.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
전역 스타일이나 테마가 필요하다면 코드로 넣어주면 됩니다.
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Resources.MergedDictionaries.Add(new ResourceDictionary
{
Source = new Uri("pack://application:,,,/Themes/Dark.xaml", UriKind.Absolute)
});
Window shell = CreateShell();
MainWindow = shell;
shell.Show();
}
XAML 한 줄이면 될 걸 코드로 쓰는 게 번거롭긴 합니다. 이건 트레이드오프예요. 테마 전환 기능이 있는 앱이라면 오히려 코드로 관리하는 게 편하고, 정적인 스타일만 쓴다면 App.xaml을 남겨두는 절충안도 충분히 합리적입니다.
💡 절충안: App.xaml은 남기되 StartupUri 속성만 지우는 방법도 있습니다. 그럼 리소스는 XAML로 관리하면서 DI도 쓸 수 있습니다. 다만 이 경우 Main이 자동 생성되므로 Starter.cs는 필요 없습니다.
8. 실전 — 실제로 굴러가는 예제
여태 배운 걸 전부 합쳐 보겠습니다.
1) 서비스와 뷰모델 준비
public interface IFileService
{
IEnumerable<string> GetFiles(string path);
}
public class FileService : IFileService
{
public IEnumerable<string> GetFiles(string path)
=> Directory.GetFiles(path);
}
public class MainViewModel
{
private readonly IFileService _fileService;
// 컨테이너가 알아서 넣어줍니다
public MainViewModel(IFileService fileService)
{
_fileService = fileService;
}
}
2) 윈도우도 주입받게 만들기
public partial class MainWindow : Window
{
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel; // 바인딩 연결
}
}
3) 등록하고 꺼내 쓰기
private void RegisterTypes(IServiceCollection services)
{
services.AddSingleton<IFileService, FileService>();
services.AddSingleton<MainViewModel>();
services.AddSingleton<MainWindow>();
}
private Window CreateShell()
{
return _provider.GetRequiredService<MainWindow>();
}
끝입니다. GetRequiredService<MainWindow>() 한 번 호출하면 컨테이너가 FileService → MainViewModel → MainWindow 순서로 알아서 조립해서 돌려줍니다. new를 한 번도 안 썼다는 걸 보세요.
9강. 실무에서 챙기면 좋은 것들
기본 골격은 완성됐습니다. 여기서 몇 가지만 더 얹으면 훨씬 단단해집니다.
① 종료 코드를 살려두기
[STAThread]
private static int Main(string[] args)
{
return new App().Run();
}
원래 코드는 _ = new App().Run();으로 반환값을 버리고 있는데요, Run()은 Shutdown(exitCode)로 전달된 종료 코드를 돌려줍니다. 배치 스크립트나 CI에서 "이 앱이 정상 종료했나?"를 판별해야 한다면 살려두는 게 좋습니다. 그럴 일이 없다면 지금 코드도 문제없습니다.
② 컨테이너 검증 켜기
_provider = services.BuildServiceProvider(new ServiceProviderOptions
{
ValidateOnBuild = true,
ValidateScopes = true
});
등록을 깜빡한 타입이 있으면 앱 시작하자마자 알려줍니다. 이걸 안 켜면 한참 쓰다가 특정 화면을 열었을 때야 터지는데, 디버깅이 훨씬 괴로워집니다.
③ 전역 예외 처리 붙이기
지금 구조에서는 UI 스레드에서 예외가 하나 터지면 앱이 그대로 죽습니다. 최소한 로그는 남기고 죽어야죠.
protected override void OnStartup(StartupEventArgs e)
{
DispatcherUnhandledException += (s, args) =>
{
MessageBox.Show($"오류가 발생했습니다:\n{args.Exception.Message}");
args.Handled = true; // 앱을 죽이지 않고 계속 진행
};
base.OnStartup(e);
// ...
}
④ csproj 확인하기
App.xaml이 없어도 이 두 줄은 반드시 있어야 합니다.
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
혹시 진입점 후보가 여러 개라 컴파일러가 헷갈려한다면 명시해 주세요.
<StartupObject>WpfExplorer.Starter</StartupObject>
⑤ 컨테이너를 여기저기서 만지지 않기
가장 흔히 하는 실수입니다.
// ❌ 이러지 마세요
public class SomeViewModel
{
public void DoWork()
{
var service = App.Provider.GetService<IFileService>();
}
}
이걸 서비스 로케이터 안티패턴이라고 부릅니다. 겉보기엔 편한데, 이 클래스가 뭘 필요로 하는지 생성자만 봐서는 알 수 없게 되고 테스트도 어려워집니다.
원칙은 이겁니다.
컨테이너를 직접 만지는 곳은 CreateShell() 딱 한 군데. 그 아래로는 전부 생성자 주입으로 흘려보낸다.
대화상자처럼 동적으로 만들어야 하는 게 있다면 Func<T>나 팩토리 인터페이스를 주입받으세요.
services.AddTransient<SettingsDialog>();
services.AddSingleton<Func<SettingsDialog>>(
sp => () => sp.GetRequiredService<SettingsDialog>());
마무리 — 한 장으로 정리
지운 것 그게 하던 일 대체품
| App.xaml | Main 자동 생성 + StartupUri로 창 띄우기 | Starter.cs의 Main + OnStartup |
| MainWindow.xaml | 매개변수 없는 생성자로 만들어지는 창 | 컨테이너가 조립해 주는 창 |
| AssemblyInfo.cs | 어셈블리 메타데이터 | csproj의 <Version> 등 |
흐름은 이렇게 흘러갑니다.
OS가 프로세스 시작
→ Starter.Main() [STAThread 선언]
→ new App() [부품 등록 → 컨테이너 생성]
→ app.Run() [메시지 루프 시작]
→ OnStartup() [컨테이너에서 창 꺼내서 Show]
→ ... 사용자가 앱을 사용 ...
→ 창 닫힘
→ OnExit() [컨테이너 Dispose]
→ Run() 반환, 프로세스 종료
처음엔 "파일 3개 지우고 2개 만드는 게 뭐 대단한 일이냐" 싶지만, 실은 WPF가 자동으로 해주던 일을 전부 눈에 보이는 곳으로 끄집어낸 것입니다. 코드가 몇 줄 늘어난 대신, 앱이 어떻게 시작하고 끝나는지가 전부 내 손 안에 들어옵니다.
작은 앱이라면 굳이 이렇게까지 안 해도 됩니다. 하지만 화면이 열 개를 넘어가고, 서비스가 얽히기 시작하고, 테스트를 쓰기 시작하는 순간부터는 이 구조가 확실히 값을 합니다.
다음 단계로 관심이 있다면 — MVVM 툴킷(CommunityToolkit.Mvvm)으로 뷰모델 보일러플레이트 줄이기, IHostBuilder로 설정 파일과 로깅까지 통합하기, 그리고 이 모든 걸 이미 만들어둔 Prism 도입하기 순으로 넘어가시면 자연스럽습니다.
출처: 이재웅 & 구양 WPF Inside Out <-링크를 누르면 사이트로 이동합니다.
'if ( Programming ) > WPF' 카테고리의 다른 글
| [WPF] 캡처 방지 기능을 만들어 보자 - SetWindowDisplayAffinity (2) | 2023.07.18 |
|---|---|
| [WPF] Mutex를 사용하여 중복 실행을 막아보자! (0) | 2023.01.31 |
| [WPF] MVVM 패턴으로 WPF 시작하기 - 05 Converter (0) | 2023.01.26 |
| [WPF] MVVM 패턴으로 WPF 시작하기 - 04 Textbox와 ComboBox (2) | 2023.01.25 |
| [WPF] MVVM 패턴으로 WPF 시작하기 - 03 ViewModel 구성 (0) | 2023.01.25 |