WPF MVVM 标准示例教程
理解 WPF 的核心就在于理解 MVVM (Model-View-ViewModel) 模式。
在这个例子中,我们将创建一个简单的 “用户列表管理” 应用。
- 功能:输入姓名和年龄,点击添加按钮,列表会实时更新。
- 特点:完全不使用传统的事件处理器(如 `Button_Click`),而是使用 数据绑定 (Data Binding) 和 命令 (Command)。
1. 基础架构 (Infrastructure)
在标准的 MVVM 中,我们需要两个辅助类:
- ViewModelBase: 实现 `INotifyPropertyChanged` 接口,用于通知 UI 数据变了。
- RelayCommand: 实现 `ICommand` 接口,用于处理按钮点击逻辑。
using System; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; namespace WpfMvvmExample { // 1. 通知基类:当属性变化时,告诉 UI 刷新 public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected 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 (Equals(field, value)) return false; field = value; OnPropertyChanged(propertyName); return true; } } // 2. 命令类:将按钮点击行为绑定到方法 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; _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); public void Execute(object parameter) => _execute(parameter); public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } } }
2. 模型 (Model)
这是纯粹的数据对象,不包含任何 UI 逻辑。
namespace WpfMvvmExample { public class User { public string Name { get; set; } public int Age { get; set; } // 用于显示的格式化字符串 public string DisplayInfo => $"{Name} ({Age}岁)"; } }
3. 视图模型 (ViewModel)
这是 MVVM 的核心。它是 View(界面)和 Model(数据)的桥梁。
- 它持有数据列表 (`ObservableCollection`)。
- 它持有当前输入的字段。
- 它持有命令 (`AddUserCommand`)。
using System.Collections.ObjectModel; using System.Windows; namespace WpfMvvmExample { public class MainViewModel : ViewModelBase { // --- 状态字段 --- private string _inputName; private int _inputAge; // --- 绑定到界面的属性 --- public string InputName { get => _inputName; set => SetProperty(ref _inputName, value); // 值改变时通知 UI } public int InputAge { get => _inputAge; set => SetProperty(ref _inputAge, value); } // ObservableCollection 是 WPF 列表绑定的神器,集合变动会自动刷新界面 public ObservableCollection<User> Users { get; set; } // --- 命令 --- public RelayCommand AddUserCommand { get; set; } // --- 构造函数 --- public MainViewModel() { Users = new ObservableCollection<User> { new User { Name = "张三", Age = 25 } // 初始数据 }; // 初始化命令:指定执行逻辑(AddUser) 和 判断逻辑(CanAddUser) AddUserCommand = new RelayCommand(AddUser, CanAddUser); } // --- 逻辑方法 --- private void AddUser(object obj) { // 添加到集合 Users.Add(new User { Name = InputName, Age = InputAge }); // 清空输入框 InputName = string.Empty; InputAge = 0; } // 判断按钮是否可用(例如:名字不能为空) // 如果返回 false,WPF 会自动禁用按钮 private bool CanAddUser(object obj) { return !string.IsNullOrWhiteSpace(InputName); } } }
4. 视图 (View - XAML)
在 XAML 中,我们不写任何 C# 代码来处理逻辑,全部通过 `Binding` 连接到 ViewModel。
<Window x:Class="WpfMvvmExample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfMvvmExample" mc:Ignorable="d" Title="MVVM 标准示例" Height="350" Width="400"> <Grid Margin="20"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 输入区域 --> <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10"> <TextBlock Text="姓名:" VerticalAlignment="Center"/> <!-- UpdateSourceTrigger=PropertyChanged 表示每敲一个字都同步给 ViewModel --> <TextBox Text="{Binding InputName, UpdateSourceTrigger=PropertyChanged}" Width="100" Margin="5,0"/> <TextBlock Text="年龄:" VerticalAlignment="Center" Margin="10,0,0,0"/> <TextBox Text="{Binding InputAge, UpdateSourceTrigger=PropertyChanged}" Width="50" Margin="5,0"/> </StackPanel> <!-- 按钮区域 --> <!-- Command 绑定到 ViewModel 中的 AddUserCommand --> <Button Grid.Row="1" Content="添加用户" Command="{Binding AddUserCommand}" Width="100" HorizontalAlignment="Left" Margin="0,0,0,10"/> <!-- 列表区域 --> <!-- ItemsSource 绑定到 Users 集合 --> <ListBox Grid.Row="2" ItemsSource="{Binding Users}"> <ListBox.ItemTemplate> <DataTemplate> <!-- 显示 User 对象的 DisplayInfo 属性 --> <TextBlock Text="{Binding DisplayInfo}" FontSize="14"/> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
5. 胶水代码 (Code-Behind)
最后,我们需要把 View 和 ViewModel 连接起来。这通常在 `MainWindow.xaml.cs` 中完成。
using System.Windows; namespace WpfMvvmExample { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); // 关键步骤:设置 DataContext // 这告诉 View:“你的数据源和逻辑都在 MainViewModel 里” this.DataContext = new MainViewModel(); } } }
核心概念讲解
- DataContext (数据上下文):
- 在 `MainWindow.xaml.cs` 中,我们把 `MainViewModel` 赋值给了 `DataContext`。
- 这使得 XAML 中的 `{Binding InputName}` 知道去 `MainViewModel` 里找 `InputName` 属性。
- INotifyPropertyChanged:
- 当你在输入框打字时,`set` 访问器被调用,`OnPropertyChanged` 被触发。
- WPF 界面收到通知,知道数据变了。
- ICommand (命令):
- 注意 XAML 中的 `<Button Command=“{Binding AddUserCommand}” … />`。
- WPF 会自动调用 `AddUserCommand.Execute` 来运行逻辑。
- WPF 还会自动调用 `AddUserCommand.CanExecute`。如果我们在 `CanAddUser` 方法里返回 `false`(比如名字为空),按钮会自动变灰(禁用)。
- ObservableCollection:
- 普通的 `List<T>` 添加数据时,界面不会知道。
- `ObservableCollection<T>` 在添加/删除元素时会发出通知,列表控件(ListBox)会自动刷新显示新数据。
Tips WPF Binding 中 数据绑定Path 的用法详解
在 WPF 的数据绑定表达式 `{Binding …}` 中,`Path` 是最常用的属性之一,用于指定数据源中具体的属性名称。
1. 显式使用 Path
这是最标准的写法,明确指定了要绑定的属性路径。
- 语法: `{Binding Path=PropertyName}`
- 含义: 告诉绑定引擎,去 DataContext(数据上下文)中找名为 `PropertyName` 的属性。
示例代码:
<!-- 绑定到 ViewModel 的 UserName 属性 --> <TextBlock Text="{Binding Path=UserName}" /> <!-- 绑定到子属性 (例如 ViewModel 有个 User 对象,User 对象有 Name 属性) --> <TextBlock Text="{Binding Path=User.Name}" />
2. 隐式使用 Path (省略 Path=)
WPF 的绑定语法允许省略 `Path=` 关键字。如果绑定表达式中的第一个参数没有指定属性名,WPF 默认将其视为 `Path`。
- 语法: `{Binding PropertyName}`
- 含义: 等同于 `{Binding Path=PropertyName}`。这是最常见的简写方式。
示例代码:
<!-- 简写方式,效果同上 --> <TextBlock Text="{Binding UserName}" /> <!-- 嵌套属性简写 --> <TextBlock Text="{Binding User.Name}" />
3. Path 为 "." (或省略 Path 且为空)
有时候我们需要绑定到数据源对象本身,而不是它的某个属性。这在列表控件(如 ListBox、ItemsControl)的模板中非常常见,特别是当数据源是简单的字符串列表或整数列表时。
- 语法: `{Binding Path=.}` 或 `{Binding .}` 或 简单的 `{Binding}`
- 含义: 绑定到当前的 DataContext 对象本身。
示例代码:
<!-- 假设 DataContext 是一个字符串 "Hello World" --> <!-- 写法一:显式点号 --> <TextBlock Text="{Binding Path=.}" /> <!-- 写法二:隐式点号 --> <TextBlock Text="{Binding .}" /> <!-- 写法三:完全省略 (最推荐) --> <TextBlock Text="{Binding}" />
应用场景 (ItemsControl):
<ListBox ItemsSource="{Binding StringList}"> <ListBox.ItemTemplate> <DataTemplate> <!-- 这里 Binding 绑定的是 StringList 中的每一个字符串本身 --> <TextBlock Text="{Binding}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
4. 索引器语法
`Path` 支持使用索引器来访问集合或字典中的元素。
- 语法: `{Binding Path=[Index]}`
- 含义: 访问集合中特定位置的元素。
示例代码:
<!-- 绑定到某列表属性的第一个元素 --> <TextBlock Text="{Binding Path=MyList[0]}" /> <!-- 如果 DataContext 本身就是列表,访问其第二个元素 --> <TextBlock Text="{Binding Path=[1]}" /> <!-- 字典查表:绑定到 Key 为 "FirstName" 的值 --> <TextBlock Text="{Binding Path=MyDictionary[FirstName]}" />
5. 特殊字符转义
如果绑定的属性名称中包含特殊字符(虽然不推荐属性名包含特殊字符),或者使用了附加属性,通常需要用括号包裹。
- 语法: `{Binding Path=(OwnerType.AttachedProperty)}`
示例代码:
<!-- 绑定到 Grid 的 Row 附加属性 --> <TextBlock Text="{Binding Path=(Grid.Row)}" />
总结
| 写法 | 解释 | 备注 |
|---|---|---|
| `{Binding Path=Name}` | 标准写法 | 最清晰,适合初学者理解 |
| `{Binding Name}` | 省略 Path | 最常用,简洁 |
| `{Binding}` | 绑定到对象本身 | 常用于简单数据类型的列表模板 |
| `{Binding .}` | 绑定到对象本身 | 同上,显式写法 |