WPF优秀项目推荐:Stylet 一个非常轻量但强大的 ViewModel-First MVVM 框架
在WPF开发中,MVVM模式几乎是标准实践。但很多开发者面对Prism、MVVM Light等框架时,常常被复杂的配置和庞大的体积所困扰。今天我要介绍一个“小而美”的MVVM框架——Stylet,它只有几十KB,却提供了完整的ViewModel-First开发体验,特别适合中小型WPF项目。## 什么是Stylet?Stylet是一个基于.NET的开源MVVM框架,由知名开发者Caleb Jenkins创建。它的核心设计理念是“ViewModel-First”,即优先设计ViewModel,然后让View自动绑定。相比传统View-First模式,这种方式让代码更易于测试和维护。Stylet的特点:- 极简设计:核心DLL仅30KB左右- 零配置:无需定义View-ViewModel映射- 自动绑定:通过命名约定自动连接View和ViewModel- 内置IoC容器:支持依赖注入- 事件聚合器:轻松实现模块间通信- 完全开源:MIT许可证,可自由使用## 为什么选择Stylet?想象一下,你只需要写一个ViewModel类,再创建一个同名的View(如MainViewModel对应MainView),Stylet就会自动将它们关联起来。这种“约定优于配置”的思路,让开发体验非常流畅。### 安装Stylet通过NuGet包管理器安装:bashInstall-Package Stylet或者使用.NET CLI:bashdotnet add package Stylet## 基础示例:一个简单的计数器让我们从最经典的计数器例子开始,感受Stylet的简洁。### 第一步:创建ViewModelcsharp// ViewModels/CounterViewModel.csusing Stylet;namespace MyApp.ViewModels{ // 继承Screen基类,获得属性变更通知等MVVM基础功能 public class CounterViewModel : Screen { private int _count = 0; // 属性:通过NotifyOfPropertyChange自动通知View更新 public int Count { get => _count; set { SetAndNotify(ref _count, value); // 每次Count变化时,检查按钮状态 NotifyOfPropertyChange(() => CanDecrement); } } // 命令:通过命名约定自动生成ICommand // 方法名为Increment,则View中可以直接绑定 public void Increment() { Count++; } // 带条件的命令:CanDecrement属性控制按钮是否可用 public bool CanDecrement => Count > 0; public void Decrement() { Count--; } // 构造函数:可以注入其他服务 public CounterViewModel() { Count = 0; } }}### 第二步:创建Viewxaml<!-- Views/CounterView.xaml --><Window x:Class="MyApp.Views.CounterView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Stylet 计数器" Height="200" Width="300"> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <TextBlock Text="{Binding Count}" FontSize="48" HorizontalAlignment="Center"/> <StackPanel Orientation="Horizontal" Margin="0,20,0,0"> <Button Content="增加" Command="{Binding Increment}" Width="80" Height="30"/> <Button Content="减少" Command="{Binding Decrement}" Width="80" Height="30" Margin="10,0,0,0" IsEnabled="{Binding CanDecrement}"/> </StackPanel> </StackPanel></Window>### 第三步:配置App启动csharp// App.xaml.csusing Stylet;using StyletIoC;namespace MyApp{ public partial class App : StyletApplication { // 配置IoC容器 protected override void ConfigureIoC(IStyletIoCBuilder builder) { // 注册ViewModel和View(Stylet会自动扫描) // 也可以注册其他服务:builder.Bind<IService>().To<Service>(); } // 指定启动的ViewModel protected override void OnStart() { // 显示主窗口 base.OnStart(); } }}``````xaml<!-- App.xaml --><Application x:Class="MyApp.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="https://github.com/canton7/Stylet"> <Application.Resources> <!-- 默认主题(可选) --> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <s:ApplicationTheme /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources></Application>运行效果:点击按钮,数字会实时更新,当Count为0时,“减少”按钮自动禁用。## 深入示例:带服务注入的Todo列表现在让我们构建一个更实用的Todo列表应用,展示依赖注入和事件聚合器的用法。### 第一步:创建服务接口和实现csharp// Services/ITodoService.csusing System.Collections.ObjectModel;namespace MyApp.Services{ public interface ITodoService { ObservableCollection<TodoItem> GetTodos(); void AddTodo(string title); void RemoveTodo(TodoItem item); } public class TodoItem { public string Title { get; set; } public bool IsCompleted { get; set; } }}``````csharp// Services/TodoService.csusing System.Collections.ObjectModel;namespace MyApp.Services{ public class TodoService : ITodoService { private readonly ObservableCollection<TodoItem> _todos = new(); public ObservableCollection<TodoItem> GetTodos() => _todos; public void AddTodo(string title) { _todos.Add(new TodoItem { Title = title }); } public void RemoveTodo(TodoItem item) { _todos.Remove(item); } }}### 第二步:创建ViewModelcsharp// ViewModels/TodoListViewModel.csusing Stylet;using MyApp.Services;namespace MyApp.ViewModels{ public class TodoListViewModel : Screen { private readonly ITodoService _todoService; private string _newTodoTitle; // 绑定到列表控件 public ObservableCollection<TodoItem> Todos { get; } // 新任务的标题 public string NewTodoTitle { get => _newTodoTitle; set { SetAndNotify(ref _newTodoTitle, value); NotifyOfPropertyChange(() => CanAddTodo); } } // 添加按钮是否可用(标题不为空) public bool CanAddTodo => !string.IsNullOrWhiteSpace(NewTodoTitle); // 添加任务命令 public void AddTodo() { if (!string.IsNullOrWhiteSpace(NewTodoTitle)) { _todoService.AddTodo(NewTodoTitle); NewTodoTitle = string.Empty; // 清空输入框 } } // 删除任务命令(参数通过CommandParameter传递) public void RemoveTodo(TodoItem item) { _todoService.RemoveTodo(item); } // 构造函数注入服务 public TodoListViewModel(ITodoService todoService) { _todoService = todoService; Todos = _todoService.GetTodos(); } }}### 第三步:创建Viewxaml<!-- Views/TodoListView.xaml --><Window x:Class="MyApp.Views.TodoListView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Stylet Todo列表" Height="400" Width="500"> <Grid Margin="20"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 输入区域 --> <StackPanel Orientation="Horizontal" Grid.Row="0"> <TextBox Text="{Binding NewTodoTitle, UpdateSourceTrigger=PropertyChanged}" Width="300" Height="30"/> <Button Content="添加" Command="{Binding AddTodo}" Width="80" Height="30" Margin="10,0,0,0"/> </StackPanel> <!-- 列表区域 --> <ListBox Grid.Row="1" ItemsSource="{Binding Todos}" Margin="0,20,0,0"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <TextBlock Text="{Binding Title}" VerticalAlignment="Center"/> <Button Grid.Column="1" Content="删除" Command="{Binding DataContext.RemoveTodo, RelativeSource={RelativeSource AncestorType=ListBox}}" CommandParameter="{Binding}"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid></Window>### 第四步:注册服务和启动csharp// App.xaml.cs(修改ConfigureIoC方法)protected override void ConfigureIoC(IStyletIoCBuilder builder){ // 注册服务 builder.Bind<ITodoService>().To<TodoService>().InSingletonScope(); // 注册ViewModel(自动扫描) builder.Bind<TodoListViewModel>().ToSelf();}效果展示:输入任务标题后点击“添加”,任务会出现在列表中;点击每项旁的“删除”按钮,可移除对应任务。所有数据通过依赖注入管理,View只负责显示。## 核心特性深度解析### 1. ViewModel-First模式传统MVVM框架(如Prism)通常是View-First:先创建View,然后在View的Code-behind中获取ViewModel。Stylet反其道而行,你只需要定义ViewModel,框架会自动创建对应的View并绑定。**工作流程:**1. 在IoC容器中注册ViewModel2. 调用WindowManager.ShowWindow(viewModel)或使用导航3. Stylet根据命名约定(如MainViewModel→MainView)创建View4. 自动绑定DataContext和交互逻辑### 2. 命令系统Stylet的方法可直接作为命令绑定,无需显式定义ICommand:-public void MethodName()→ 绑定Command="{Binding MethodName}"-public bool CanMethodName→ 控制命令可用性- 支持带参数的方法:public void MethodName(ParameterType param)### 3. 属性变更通知通过SetAndNotify方法简化了INotifyPropertyChanged的实现:csharpprivate string _name;public string Name{ get => _name; set => SetAndNotify(ref _name, value);}### 4. 事件聚合器内置IEventAggregator接口,支持弱引用事件,避免内存泄漏:csharp// 发布事件_eventAggregator.Publish(new MyEvent());// 订阅事件_eventAggregator.Subscribe(this, handleEventMethod);## 与其他框架对比| 特性 | Stylet | Prism | MVVM Light ||------|--------|-------|------------|| 体积 | ~30KB | ~500KB | ~100KB || 配置复杂度 | 极低 | 较高 | 中等 || IoC容器 | 内置 | 可选 | 无 || 导航系统 | 内置 | 完整 | 无 || 学习曲线 | 平缓 | 陡峭 | 中等 || 适合项目 | 中小型 | 大型企业 | 小型 |## 总结Stylet是一个被低估的MVVM框架,它用极小的体积实现了日常开发中最需要的功能。如果你厌倦了Prism的复杂配置,或者觉得MVVM Light功能不足,不妨试试Stylet。它特别适合:- 快速原型开发- 中小型WPF项目- 追求代码简洁的开发者- 需要良好测试性的应用通过本文的两个示例,你应该已经感受到Stylet的简洁和强大。它没有魔法,只有精心设计的约定和API。下次你开启WPF项目时,别忘了这个轻量而强大的选择。