名称: wpf-mvvm-脚手架 description: 生成包含 ViewModelBase、RelayCommand、INotifyPropertyChanged 和依赖注入设置的 WPF MVVM 架构 允许使用的工具: Read, Write, Edit, Bash, Glob, Grep tags: [wpf, mvvm, dotnet, csharp, 架构]
wpf-mvvm-脚手架
生成包含 ViewModelBase、RelayCommand、INotifyPropertyChanged 实现和依赖注入设置的 WPF MVVM 架构脚手架。此技能为 WPF 应用程序创建生产就绪的 MVVM 基础。
功能
- 生成包含 INotifyPropertyChanged 的 ViewModelBase
- 创建 RelayCommand/AsyncRelayCommand 实现
- 使用 Microsoft.Extensions.DependencyInjection 设置依赖注入
- 生成导航服务模式
- 创建消息/事件聚合器
- 设置设计时数据支持
- 为 ViewModels 生成单元测试脚手架
- 配置 MVVM 工具包集成
输入模式
{
"type": "object",
"properties": {
"projectPath": {
"type": "string",
"description": "WPF 项目路径"
},
"projectName": {
"type": "string",
"description": "项目名称"
},
"mvvmFramework": {
"enum": ["custom", "mvvm-toolkit", "prism", "caliburn"],
"default": "mvvm-toolkit"
},
"features": {
"type": "array",
"items": {
"enum": ["navigation", "messenger", "validation", "dialogs", "design-time"]
},
"default": ["navigation", "validation"]
},
"diFramework": {
"enum": ["microsoft-di", "autofac", "ninject"],
"default": "microsoft-di"
},
"generateViewModels": {
"type": "array",
"items": { "type": "string" },
"description": "要生成的初始 ViewModels"
}
},
"required": ["projectPath", "projectName"]
}
输出模式
{
"type": "object",
"properties": {
"success": { "type": "boolean" },
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": { "type": "string" },
"type": { "enum": ["base", "viewmodel", "service", "command"] }
}
}
},
"nugetPackages": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["success"]
}
项目结构
MyApp/
├── App.xaml
├── App.xaml.cs
├── ViewModels/
│ ├── Base/
│ │ ├── ViewModelBase.cs
│ │ └── RelayCommand.cs
│ ├── MainViewModel.cs
│ ├── ShellViewModel.cs
│ └── Settings/
│ └── SettingsViewModel.cs
├── Views/
│ ├── MainView.xaml
│ ├── ShellView.xaml
│ └── Settings/
│ └── SettingsView.xaml
├── Services/
│ ├── INavigationService.cs
│ ├── NavigationService.cs
│ ├── IDialogService.cs
│ └── DialogService.cs
├── Models/
│ └── ...
└── Infrastructure/
├── Bootstrapper.cs
├── ServiceLocator.cs
└── Messenger.cs
生成代码示例
ViewModelBase.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MyApp.ViewModels.Base;
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
protected bool SetProperty<T>(ref T field, T value, Action onChanged,
[CallerMemberName] string? propertyName = null)
{
if (SetProperty(ref field, value, propertyName))
{
onChanged?.Invoke();
return true;
}
return false;
}
// 设计时支持
public static bool IsInDesignMode =>
DesignerProperties.GetIsInDesignMode(new DependencyObject());
}
RelayCommand.cs
using System.Windows.Input;
namespace MyApp.ViewModels.Base;
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Predicate<object?>? _canExecute;
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public RelayCommand(Action execute, Func<bool>? canExecute = null)
: this(_ => execute(), canExecute != null ? _ => canExecute() : null)
{
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public void RaiseCanExecuteChanged() => CommandManager.InvalidateRequerySuggested();
}
public class AsyncRelayCommand : ICommand
{
private readonly Func<object?, Task> _execute;
private readonly Predicate<object?>? _canExecute;
private bool _isExecuting;
public AsyncRelayCommand(Func<object?, Task> execute, Predicate<object?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
: this(_ => execute(), canExecute != null ? _ => canExecute() : null)
{
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) =>
!_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
public async void Execute(object? parameter)
{
if (!CanExecute(parameter)) return;
_isExecuting = true;
RaiseCanExecuteChanged();
try
{
await _execute(parameter);
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public void RaiseCanExecuteChanged() => CommandManager.InvalidateRequerySuggested();
}
MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp.ViewModels;
public partial class MainViewModel : ViewModelBase
{
private readonly INavigationService _navigationService;
private readonly IDataService _dataService;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string _title = string.Empty;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private ObservableCollection<ItemViewModel> _items = new();
public MainViewModel(INavigationService navigationService, IDataService dataService)
{
_navigationService = navigationService;
_dataService = dataService;
if (IsInDesignMode)
{
LoadDesignTimeData();
}
}
public ICommand SaveCommand => new RelayCommand(
async () => await SaveAsync(),
() => !string.IsNullOrEmpty(Title) && !IsLoading);
public ICommand NavigateToSettingsCommand => new RelayCommand(
() => _navigationService.NavigateTo<SettingsViewModel>());
private async Task SaveAsync()
{
IsLoading = true;
try
{
await _dataService.SaveAsync(Title);
}
finally
{
IsLoading = false;
}
}
public async Task LoadDataAsync()
{
IsLoading = true;
try
{
var data = await _dataService.GetItemsAsync();
Items = new ObservableCollection<ItemViewModel>(data.Select(d => new ItemViewModel(d)));
}
finally
{
IsLoading = false;
}
}
private void LoadDesignTimeData()
{
Title = "设计时标题";
Items = new ObservableCollection<ItemViewModel>
{
new("项目 1"),
new("项目 2"),
new("项目 3")
};
}
}
App.xaml.cs with DI
using Microsoft.Extensions.DependencyInjection;
namespace MyApp;
public partial class App : Application
{
private readonly IServiceProvider _serviceProvider;
public App()
{
var services = new ServiceCollection();
ConfigureServices(services);
_serviceProvider = services.BuildServiceProvider();
}
private void ConfigureServices(IServiceCollection services)
{
// 服务
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IDialogService, DialogService>();
services.AddTransient<IDataService, DataService>();
// ViewModels
services.AddTransient<MainViewModel>();
services.AddTransient<SettingsViewModel>();
services.AddSingleton<ShellViewModel>();
// 视图
services.AddTransient<MainView>();
services.AddTransient<SettingsView>();
services.AddSingleton<ShellView>();
}
protected override void OnStartup(StartupEventArgs e)
{
var shell = _serviceProvider.GetRequiredService<ShellView>();
shell.DataContext = _serviceProvider.GetRequiredService<ShellViewModel>();
shell.Show();
base.OnStartup(e);
}
}
NavigationService.cs
namespace MyApp.Services;
public interface INavigationService
{
void NavigateTo<TViewModel>() where TViewModel : ViewModelBase;
void NavigateTo<TViewModel>(object parameter) where TViewModel : ViewModelBase;
void GoBack();
bool CanGoBack { get; }
}
public class NavigationService : ViewModelBase, INavigationService
{
private readonly IServiceProvider _serviceProvider;
private readonly Stack<ViewModelBase> _navigationStack = new();
public NavigationService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
private ViewModelBase? _currentViewModel;
public ViewModelBase? CurrentViewModel
{
get => _currentViewModel;
private set => SetProperty(ref _currentViewModel, value);
}
public bool CanGoBack => _navigationStack.Count > 1;
public void NavigateTo<TViewModel>() where TViewModel : ViewModelBase
{
NavigateTo<TViewModel>(null);
}
public void NavigateTo<TViewModel>(object? parameter) where TViewModel : ViewModelBase
{
var viewModel = _serviceProvider.GetRequiredService<TViewModel>();
if (viewModel is INavigationAware navigationAware)
{
navigationAware.OnNavigatedTo(parameter);
}
if (CurrentViewModel is INavigationAware currentNavigationAware)
{
currentNavigationAware.OnNavigatedFrom();
}
_navigationStack.Push(viewModel);
CurrentViewModel = viewModel;
}
public void GoBack()
{
if (!CanGoBack) return;
if (CurrentViewModel is INavigationAware currentNavigationAware)
{
currentNavigationAware.OnNavigatedFrom();
}
_navigationStack.Pop();
CurrentViewModel = _navigationStack.Peek();
if (CurrentViewModel is INavigationAware navigationAware)
{
navigationAware.OnNavigatedTo(null);
}
}
}
NuGet 包
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
最佳实践
- 保持 ViewModels 与 UI 无关:不引用 WPF 类型
- 使用异步命令:用于长时间运行的操作
- 实现 INotifyDataErrorInfo:用于验证
- 设计时数据:支持 Blend/VS 设计器
- 单一职责:一个 View 对应一个 ViewModel
- 单元测试 ViewModels:模拟服务
相关技能
wpf-xaml-style-generator- UI 样式msix-package-generator- 打包desktop-unit-testing流程 - 测试
相关代理
wpf-dotnet-expert- WPF 专业知识architecture-pattern-advisor- MVVM 模式