structuring-wpf-projects
2
总安装量
2
周安装量
#71903
全站排名
安装命令
npx skills add https://github.com/christian289/dotnet-with-claudecode --skill structuring-wpf-projects
Agent 安装分布
gemini-cli
2
opencode
2
codebuddy
2
github-copilot
2
codex
2
kimi-cli
2
Skill 文档
WPF Solution and Project Structure
A guide for designing WPF project solution and project structure based on Clean Architecture.
Template Project
The templates folder contains a Clean Architecture WPF solution example (use latest .NET per version mapping).
templates/
âââ GameDataTool.sln â Solution file
âââ Directory.Build.props â Common build settings
âââ src/
â âââ GameDataTool.Domain/ â ðµ Core - Pure domain models
â âââ GameDataTool.Application/ â ð¢ Core - Use Cases
â âââ GameDataTool.Infrastructure/ â ð¡ Infrastructure - External systems
â âââ GameDataTool.ViewModels/ â ð Presentation - ViewModel
â âââ GameDataTool.WpfServices/ â ð Presentation - WPF services
â âââ GameDataTool.UI/ â ð´ Presentation - Custom Controls
â âââ GameDataTool.WpfApp/ â ð´ Composition Root
âââ tests/
âââ GameDataTool.Domain.Tests/
âââ GameDataTool.Application.Tests/
âââ GameDataTool.ViewModels.Tests/
Solution Structure Principles
Solution name is the application name
- Example:
GameDataToolsolution = executable .NET Assembly name
Project Structure (Clean Architecture)
SolutionName/
âââ src/
â â
â â ââââââââââââââ Core (No Dependencies) ââââââââââââââ
â â
â âââ SolutionName.Domain // ðµ Entities - Pure domain models
â â âââ Entities/
â â âââ ValueObjects/
â â âââ Interfaces/ // - Domain service interfaces only
â â
â âââ SolutionName.Application // ð¢ Use Cases - Business logic coordination
â â âââ Interfaces/ // - IRepository, IExternalService, etc.
â â âââ Services/ // - Application services
â â âââ DTOs/
â â
â â ââââââââââââââ Infrastructure ââââââââââââââ
â â
â âââ SolutionName.Infrastructure // ð¡ External system implementation
â â âââ Persistence/ // - Data access implementation
â â âââ FileSystem/ // - File system access
â â âââ ExternalServices/ // - HTTP, API, etc.
â â
â â ââââââââââââââ Presentation (WPF) ââââââââââââââ
â â
â âââ SolutionName.ViewModels // ð ViewModels (Interface Adapter role)
â â âââ (Depends on Application only)
â â
â âââ SolutionName.WpfServices // ð WPF-specific services
â â âââ (Dialog, Navigation, etc.)
â â
â âââ SolutionName.UI // ð´ Custom Controls & Styles
â â
â âââ SolutionName.WpfApp // ð´ Composition Root (Entry point)
â âââ App.xaml.cs // - DI setup, connect all implementations
â
âââ tests/
âââ SolutionName.Domain.Tests
âââ SolutionName.Application.Tests
âââ SolutionName.ViewModels.Tests
Roles by Project Type
Core Layer (No Dependencies)
| Project | Role | Contents |
|---|---|---|
.Domain |
ðµ Entities | Pure domain models, ValueObjects, domain interfaces |
.Application |
ð¢ Use Cases | Business logic coordination, IRepository/IService interfaces, DTOs |
Infrastructure Layer
| Project | Role | Contents |
|---|---|---|
.Infrastructure |
ð¡ External Systems | Repository implementation, file system, HTTP/API clients |
Presentation Layer (WPF)
| Project | Role | Contents |
|---|---|---|
.ViewModels |
ð Interface Adapter | MVVM ViewModel (depends on Application only, no WPF reference) |
.WpfServices |
ð WPF Services | DialogService, NavigationService, WindowService |
.UI |
ð´ Custom Controls | ResourceDictionary, CustomControl, Themes |
.WpfApp |
ð´ Composition Root | App.xaml, DI setup, Views, entry point |
Project Dependency Hierarchy
âââââââââââââââââââââââââââââââââââââââ
â SolutionName.WpfApp â â Composition Root
â (References all projects, DI setup) â
âââââââââââââââââââââââââââââââââââââââ
â
âââââââââââââââââââââââââââââ¼ââââââââââââââââââââââââââââ
â¼ â¼ â¼
âââââââââââââââââââ âââââââââââââââââââââââ âââââââââââââââââââ
â .ViewModels â â .Infrastructure â â .WpfServices â
â (Application â â (Application ref) â â (Application â
â ref) â â â â ref) â
ââââââââââ¬âââââââââ ââââââââââââ¬âââââââââââ ââââââââââ¬âââââââââ
â â â
âââââââââââââââââââââââââââââ¼ââââââââââââââââââââââââââââ
â¼
âââââââââââââââââââââââââââââââââââââââ
â SolutionName.Application â â Use Cases
â (Domain ref only) â
âââââââââââââââââââââââââââââââââââââââ
â
â¼
âââââââââââââââââââââââââââââââââââââââ
â SolutionName.Domain â â Core (No dependencies)
â (No references) â
âââââââââââââââââââââââââââââââââââââââ
Detailed Layer Descriptions
Domain Layer (Pure Domain)
// Domain/Entities/User.cs
namespace GameDataTool.Domain.Entities;
public sealed class User
{
public Guid Id { get; init; }
public string Name { get; private set; } = string.Empty;
public Email Email { get; private set; } = null!;
public void UpdateName(string name)
{
// Domain business rule validation
if (string.IsNullOrWhiteSpace(name))
throw new DomainException("Name is required.");
Name = name;
}
}
// Domain/ValueObjects/Email.cs
namespace GameDataTool.Domain.ValueObjects;
public sealed record Email
{
public string Value { get; }
public Email(string value)
{
if (!IsValid(value))
throw new DomainException("Invalid email format.");
Value = value;
}
private static bool IsValid(string email) =>
!string.IsNullOrWhiteSpace(email) && email.Contains('@');
}
Application Layer (Use Cases)
// Application/Interfaces/IUserRepository.cs
namespace GameDataTool.Application.Interfaces;
public interface IUserRepository
{
Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<User>> GetAllAsync(CancellationToken cancellationToken = default);
Task AddAsync(User user, CancellationToken cancellationToken = default);
Task UpdateAsync(User user, CancellationToken cancellationToken = default);
}
// Application/Services/UserService.cs
namespace GameDataTool.Application.Services;
public sealed class UserService(IUserRepository userRepository)
{
private readonly IUserRepository _userRepository = userRepository;
public async Task<UserDto?> GetUserAsync(Guid id, CancellationToken cancellationToken = default)
{
var user = await _userRepository.GetByIdAsync(id, cancellationToken);
return user is null ? null : new UserDto(user.Id, user.Name, user.Email.Value);
}
public async Task UpdateUserNameAsync(Guid id, string newName, CancellationToken cancellationToken = default)
{
var user = await _userRepository.GetByIdAsync(id, cancellationToken)
?? throw new NotFoundException("User not found.");
user.UpdateName(newName);
await _userRepository.UpdateAsync(user, cancellationToken);
}
}
// Application/DTOs/UserDto.cs
namespace GameDataTool.Application.DTOs;
public sealed record UserDto(Guid Id, string Name, string Email);
Infrastructure Layer (External System Implementation)
// Infrastructure/Persistence/UserRepository.cs
namespace GameDataTool.Infrastructure.Persistence;
public sealed class UserRepository(AppDbContext dbContext) : IUserRepository
{
private readonly AppDbContext _dbContext = dbContext;
public async Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default) =>
await _dbContext.Users.FindAsync([id], cancellationToken);
public async Task<IReadOnlyList<User>> GetAllAsync(CancellationToken cancellationToken = default) =>
await _dbContext.Users.ToListAsync(cancellationToken);
public async Task AddAsync(User user, CancellationToken cancellationToken = default)
{
await _dbContext.Users.AddAsync(user, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task UpdateAsync(User user, CancellationToken cancellationToken = default)
{
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync(cancellationToken);
}
}
ViewModels Layer (Presentation – Depends on Application Only)
// ViewModels/UserViewModel.cs
namespace GameDataTool.ViewModels;
public sealed partial class UserViewModel(UserService userService) : ObservableObject
{
private readonly UserService _userService = userService;
[ObservableProperty] private string _userName = string.Empty;
[ObservableProperty] private string _userEmail = string.Empty;
[RelayCommand]
private async Task LoadUserAsync(Guid userId)
{
var user = await _userService.GetUserAsync(userId);
if (user is null) return;
UserName = user.Name;
UserEmail = user.Email;
}
}
WpfApp Layer (Composition Root – DI Setup)
// WpfApp/App.xaml.cs
namespace GameDataTool.WpfApp;
public partial class App : Application
{
private readonly IHost _host;
public App()
{
_host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// Domain - No registration needed (pure models)
// Application Layer
services.AddTransient<UserService>();
// Infrastructure Layer
services.AddDbContext<AppDbContext>();
services.AddScoped<IUserRepository, UserRepository>();
// Presentation Layer
services.AddTransient<UserViewModel>();
services.AddTransient<MainViewModel>();
// WPF Services
services.AddSingleton<IDialogService, DialogService>();
services.AddSingleton<INavigationService, NavigationService>();
// Views
services.AddSingleton<MainWindow>();
})
.Build();
}
protected override async void OnStartup(StartupEventArgs e)
{
await _host.StartAsync();
_host.Services.GetRequiredService<MainWindow>().Show();
base.OnStartup(e);
}
protected override async void OnExit(ExitEventArgs e)
{
using (_host)
{
await _host.StopAsync();
}
base.OnExit(e);
}
}
Actual Folder Structure Example
GameDataTool/
âââ src/
â âââ GameDataTool.Domain/
â â âââ Entities/
â â â âââ User.cs
â â â âââ GameData.cs
â â âââ ValueObjects/
â â â âââ Email.cs
â â â âââ GameVersion.cs
â â âââ Interfaces/
â â â âââ IDomainEventPublisher.cs
â â âââ Exceptions/
â â â âââ DomainException.cs
â â âââ GlobalUsings.cs
â â âââ GameDataTool.Domain.csproj
â â
â âââ GameDataTool.Application/
â â âââ Interfaces/
â â â âââ IUserRepository.cs
â â â âââ IGameDataRepository.cs
â â â âââ IFileExportService.cs
â â âââ Services/
â â â âââ UserService.cs
â â â âââ GameDataService.cs
â â âââ DTOs/
â â â âââ UserDto.cs
â â â âââ GameDataDto.cs
â â âââ Exceptions/
â â â âââ NotFoundException.cs
â â âââ GlobalUsings.cs
â â âââ GameDataTool.Application.csproj
â â
â âââ GameDataTool.Infrastructure/
â â âââ Persistence/
â â â âââ AppDbContext.cs
â â â âââ UserRepository.cs
â â â âââ GameDataRepository.cs
â â âââ FileSystem/
â â â âââ ExcelExportService.cs
â â âââ ExternalServices/
â â â âââ ApiClient.cs
â â âââ GlobalUsings.cs
â â âââ GameDataTool.Infrastructure.csproj
â â
â âââ GameDataTool.ViewModels/
â â âââ MainViewModel.cs
â â âââ UserViewModel.cs
â â âââ GameDataViewModel.cs
â â âââ GlobalUsings.cs
â â âââ GameDataTool.ViewModels.csproj
â â
â âââ GameDataTool.WpfServices/
â â âââ DialogService.cs
â â âââ NavigationService.cs
â â âââ WindowService.cs
â â âââ GlobalUsings.cs
â â âââ GameDataTool.WpfServices.csproj
â â
â âââ GameDataTool.UI/
â â âââ Themes/
â â â âââ Generic.xaml
â â â âââ CustomButton.xaml
â â âââ CustomControls/
â â â âââ CustomButton.cs
â â âââ Properties/
â â â âââ AssemblyInfo.cs
â â âââ GameDataTool.UI.csproj
â â
â âââ GameDataTool.WpfApp/
â âââ Views/
â â âââ MainWindow.xaml
â â âââ MainWindow.xaml.cs
â â âââ UserView.xaml
â â âââ UserView.xaml.cs
â âââ App.xaml
â âââ App.xaml.cs
â âââ Mappings.xaml
â âââ GlobalUsings.cs
â âââ GameDataTool.WpfApp.csproj
â
âââ tests/
â âââ GameDataTool.Domain.Tests/
â â âââ Entities/
â â â âââ UserTests.cs
â â âââ GameDataTool.Domain.Tests.csproj
â â
â âââ GameDataTool.Application.Tests/
â â âââ Services/
â â â âââ UserServiceTests.cs
â â âââ GameDataTool.Application.Tests.csproj
â â
â âââ GameDataTool.ViewModels.Tests/
â âââ UserViewModelTests.cs
â âââ GameDataTool.ViewModels.Tests.csproj
â
âââ GameDataTool.sln
âââ Directory.Build.props
Assembly Reference Rules
Domain Project
- â Does not reference any project
- â Uses pure C# BCL only
Application Project
- â References Domain only
- â Cannot reference Infrastructure, Presentation
Infrastructure Project
- â References Domain, Application
- â Can use external NuGet packages (EF Core, HttpClient, etc.)
ViewModels Project
- â References Application only
- â Cannot reference WPF assemblies (WindowsBase, PresentationFramework, etc.)
- â Can use CommunityToolkit.Mvvm
WpfApp Project (Composition Root)
- â References all projects
- â Connects all implementations in DI container
Clean Architecture Advantages
- Independence: Core layer is independent from external frameworks
- Testability: Each layer can be tested independently
- Maintainability: Clear scope of change impact
- Flexibility: Easy to replace Infrastructure (DB, API, etc.)