WPF命令与MVVM模式:打造优雅的应用程序架构
🎮 打造优雅的应用程序架构
- 1. 🧩 命令系统基础
- 1.1 🤔 为什么需要命令?
- 1.2 🏗️ ICommand接口
- 1.3 🛠️ 实现基本命令
- 2. 🏛️ MVVM模式详解
- 2.1 🧱 MVVM三大组件
- 2.2 🏗️ 创建ViewModel基类
- 2.3 🎯 典型ViewModel示例
- 3. 🧩 命令绑定实战
- 3.1 🎨 View中的命令绑定
- 3.2 🎭 带参数的命令
- 4. 🚀 MVVM进阶技巧
- 4.1 🧩 消息传递
- 4.2 🏗️ 依赖注入
- 5. 🧪 测试与调试
- 5.1 🧪 单元测试ViewModel
- 5.2 🐞 常见问题解决
- 6. 🏆 实战:完整的CRUD应用
- 6.1 📦 数据服务层
- 6.2 🏗️ 完整ViewModel
- 7. 🚀 总结与进阶
1. 🧩 命令系统基础
1.1 🤔 为什么需要命令?
在传统事件处理模型中,我们这样处理按钮点击:
private void Button_Click(object sender, RoutedEventArgs e)
{// 处理点击逻辑
}
这种模式存在几个问题:
- 🚫 业务逻辑与UI代码混杂
- 🔌 难以实现启用/禁用状态管理
- 🔄 复用性差
命令系统解决了这些问题!
1.2 🏗️ ICommand接口
WPF命令系统基于ICommand接口:
public interface ICommand
{event EventHandler CanExecuteChanged;bool CanExecute(object parameter);void Execute(object parameter);
}
1.3 🛠️ 实现基本命令
创建一个简单的命令类:
/// <summary>
/// 实现ICommand接口的RelayCommand类,用于将UI操作(如按钮点击)绑定到ViewModel中的方法
/// </summary>
public class RelayCommand : ICommand
{// 私有字段,存储要执行的委托private readonly Action _execute;// 私有字段,存储判断命令是否可以执行的委托(可选)private readonly Func<bool> _canExecute;/// <summary>/// 构造函数/// </summary>/// <param name="execute">要执行的委托方法</param>/// <param name="canExecute">判断命令是否可以执行的委托方法(可选)</param>/// <exception cref="ArgumentNullException">当execute参数为null时抛出</exception>public RelayCommand(Action execute, Func<bool> canExecute = null){// 检查execute参数是否为null,如果是则抛出异常_execute = execute ?? throw new ArgumentNullException(nameof(execute));// 可选地设置canExecute委托_canExecute = canExecute;}/// <summary>/// 当命令的可执行状态可能已更改时触发的事件/// </summary>public event EventHandler CanExecuteChanged;/// <summary>/// 确定此命令是否可以在其当前状态下执行/// </summary>/// <param name="parameter">命令使用的数据(此实现中未使用)</param>/// <returns>如果此命令可以执行,则为true;否则为false</returns>public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;/// <summary>/// 执行命令/// </summary>/// <param name="parameter">命令使用的数据(此实现中未使用)</param>public void Execute(object parameter) => _execute();/// <summary>/// 引发CanExecuteChanged事件,通知命令的可执行状态可能已更改/// </summary>public void RaiseCanExecuteChanged(){// 使用空事件参数触发CanExecuteChanged事件CanExecuteChanged?.Invoke(this, EventArgs.Empty);}
}
2. 🏛️ MVVM模式详解
2.1 🧱 MVVM三大组件
组件 | 职责 | 示例 |
---|---|---|
🎨 View | 用户界面展示 | MainWindow.xaml |
📦 ViewModel | 业务逻辑和状态 | MainViewModel.cs |
🗃️ Model | 数据模型和业务规则 | Product.cs |
2.2 🏗️ 创建ViewModel基类
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 SetField<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;}
}
2.3 🎯 典型ViewModel示例
public class ProductViewModel : ViewModelBase
{private string _searchText;private ObservableCollection<Product> _products;public string SearchText{get => _searchText;set{if (SetField(ref _searchText, value)){SearchCommand.RaiseCanExecuteChanged();}}}public ObservableCollection<Product> Products{get => _products;private set => SetField(ref _products, value);}public RelayCommand SearchCommand { get; }public RelayCommand<Product> DeleteCommand { get; }public ProductViewModel(){Products = new ObservableCollection<Product>(LoadProducts());SearchCommand = new RelayCommand(SearchProducts, () => !string.IsNullOrWhiteSpace(SearchText));DeleteCommand = new RelayCommand<Product>(DeleteProduct, product => product != null);}private void SearchProducts(){var results = _productService.Search(SearchText);Products = new ObservableCollection<Product>(results);}private void DeleteProduct(Product product){Products.Remove(product);_productService.Delete(product.Id);}
}
3. 🧩 命令绑定实战
3.1 🎨 View中的命令绑定
<Window x:Class="WpfApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:WpfApp"Title="产品管理" Height="450" Width="800"><Grid Margin="10"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions><!-- 搜索区域 --><StackPanel Orientation="Horizontal" Grid.Row="0"><TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" Width="200" Margin="0,0,10,0"/><Button Content="搜索" Command="{Binding SearchCommand}" Padding="10,0"/></StackPanel><!-- 产品列表 --><ListView Grid.Row="1" ItemsSource="{Binding Products}" SelectedItem="{Binding SelectedProduct}"><ListView.View><GridView><GridViewColumn Header="名称" DisplayMemberBinding="{Binding Name}"/><GridViewColumn Header="价格" DisplayMemberBinding="{Binding Price, StringFormat=C}"/></GridView></ListView.View></ListView><!-- 操作按钮 --><StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right"><Button Content="删除" Command="{Binding DeleteCommand}" CommandParameter="{Binding SelectedProduct}" Margin="0,0,10,0" Padding="15,5"/></StackPanel></Grid>
</Window>
3.2 🎭 带参数的命令
public class RelayCommand<T> : ICommand
{private readonly Action<T> _execute;private readonly Func<T, bool> _canExecute;public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null){_execute = execute ?? throw new ArgumentNullException(nameof(execute));_canExecute = canExecute;}public bool CanExecute(object parameter) => _canExecute?.Invoke((T)parameter) ?? true;public void Execute(object parameter) => _execute((T)parameter);public event EventHandler CanExecuteChanged;public void RaiseCanExecuteChanged(){CanExecuteChanged?.Invoke(this, EventArgs.Empty);}
}
4. 🚀 MVVM进阶技巧
4.1 🧩 消息传递
使用消息总线解耦ViewModel
:
/// <summary>
/// 消息传递器,用于在应用程序不同部分之间传递消息(发布-订阅模式)
/// </summary>
public class Messenger
{// 使用静态字典存储消息类型和对应的处理动作列表// Key: 消息类型(Type)// Value: 该类型消息的处理动作列表(List<Action<object>>)private static readonly Dictionary<Type, List<Action<object>>> _actions = new Dictionary<Type, List<Action<object>>>();/// <summary>/// 注册消息处理器/// </summary>/// <typeparam name="T">要处理的消息类型</typeparam>/// <param name="action">处理消息的回调方法</param>public static void Register<T>(Action<T> action){// 获取消息类型var type = typeof(T);// 如果字典中不存在该类型的处理列表,则创建一个新的列表if (!_actions.ContainsKey(type)){_actions[type] = new List<Action<object>>();}// 将强类型回调转换为泛型object类型回调,并添加到处理列表中_actions[type].Add(obj => action((T)obj));}/// <summary>/// 发送消息/// </summary>/// <typeparam name="T">消息类型</typeparam>/// <param name="message">要发送的消息内容</param>public static void Send<T>(T message){// 获取消息类型var type = typeof(T);// 检查是否有该类型的消息处理器if (_actions.ContainsKey(type)){// 遍历所有注册的处理方法并执行foreach (var action in _actions[type]){action(message);}}}
}
4.2 🏗️ 依赖注入
在MVVM中使用DI容器:
public class ViewModelLocator
{private readonly IServiceProvider _serviceProvider;public ViewModelLocator(){var services = new ServiceCollection();// 注册服务services.AddSingleton<IProductService, ProductService>();// 注册ViewModelservices.AddTransient<MainViewModel>();_serviceProvider = services.BuildServiceProvider();}public MainViewModel MainViewModel => _serviceProvider.GetRequiredService<MainViewModel>();
}
在App.xaml中设置:
<Application ...><Application.Resources><local:ViewModelLocator x:Key="Locator"/></Application.Resources>
</Application>
5. 🧪 测试与调试
5.1 🧪 单元测试ViewModel
[TestClass]
public class ProductViewModelTests
{[TestMethod]public void SearchCommand_WhenExecuted_UpdatesProductsList(){// 准备var mockService = new Mock<IProductService>();mockService.Setup(x => x.Search("test")).Returns(new List<Product> { new Product() });var vm = new ProductViewModel(mockService.Object){SearchText = "test"};// 执行vm.SearchCommand.Execute(null);// 断言Assert.AreEqual(1, vm.Products.Count);}
}
5.2 🐞 常见问题解决
命令不执行:
- 检查CanExecute是否返回false
- 确认CommandParameter是否正确绑定
- 验证DataContext是否设置正确
内存泄漏:
- 确保注销事件处理程序
- 使用WeakEventManager处理事件
6. 🏆 实战:完整的CRUD应用
6.1 📦 数据服务层
public interface IProductService
{IEnumerable<Product> GetAll();IEnumerable<Product> Search(string term);void Add(Product product);void Update(Product product);void Delete(int id);
}public class ProductService : IProductService
{private readonly List<Product> _products = new List<Product>();public IEnumerable<Product> GetAll() => _products;public IEnumerable<Product> Search(string term) => _products.Where(p => p.Name.Contains(term));public void Add(Product product) => _products.Add(product);public void Update(Product product){var existing = _products.FirstOrDefault(p => p.Id == product.Id);if (existing != null){existing.Name = product.Name;existing.Price = product.Price;}}public void Delete(int id) => _products.RemoveAll(p => p.Id == id);
}
6.2 🏗️ 完整ViewModel
public class ProductManagementViewModel : ViewModelBase
{private readonly IProductService _productService;private Product _selectedProduct;private string _searchText;public ObservableCollection<Product> Products { get; }public Product SelectedProduct{get => _selectedProduct;set => SetField(ref _selectedProduct, value);}public string SearchText{get => _searchText;set{if (SetField(ref _searchText, value)){SearchCommand.RaiseCanExecuteChanged();}}}public RelayCommand SearchCommand { get; }public RelayCommand AddCommand { get; }public RelayCommand UpdateCommand { get; }public RelayCommand DeleteCommand { get; }public ProductManagementViewModel(IProductService productService){_productService = productService;Products = new ObservableCollection<Product>(_productService.GetAll());SearchCommand = new RelayCommand(SearchProducts, () => !string.IsNullOrWhiteSpace(SearchText));AddCommand = new RelayCommand(AddProduct);UpdateCommand = new RelayCommand(UpdateProduct, () => SelectedProduct != null);DeleteCommand = new RelayCommand(DeleteProduct, () => SelectedProduct != null);}private void SearchProducts(){var results = _productService.Search(SearchText);Products.Clear();foreach (var product in results){Products.Add(product);}}private void AddProduct(){var newProduct = new Product { Name = "新产品", Price = 0 };_productService.Add(newProduct);Products.Add(newProduct);SelectedProduct = newProduct;}private void UpdateProduct(){if (SelectedProduct != null){_productService.Update(SelectedProduct);}}private void DeleteProduct(){if (SelectedProduct != null){_productService.Delete(SelectedProduct.Id);Products.Remove(SelectedProduct);}}
}
7. 🚀 总结与进阶
通过本教程,你已经掌握了:
- 🎮 WPF命令系统的核心概念
- 🏛️ MVVM模式的三层架构
- 🧩 命令绑定和参数传递
- 🏗️ ViewModel的最佳实践
- 🧪可测试的应用程序架构
进阶学习方向:
1.框架应用:
- Prism:企业级MVVM框架
- MVVM Light:轻量级MVVM工具包
2.高级主题:
- 路由导航
- 对话框服务
- 多语言支持
- 主题切换
3.性能优化:
- 虚拟化列表
- 异步数据加载
- 数据分页
MVVM不是银弹,但它是构建可维护、可测试WPF应用程序的强大模式!💪🚀