====== 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(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
===== 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 Users { get; set; }
// --- 命令 ---
public RelayCommand AddUserCommand { get; set; }
// --- 构造函数 ---
public MainViewModel()
{
Users = new ObservableCollection
{
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。
===== 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 中的 ``。
* WPF 会自动调用 `AddUserCommand.Execute` 来运行逻辑。
* WPF 还会自动调用 `AddUserCommand.CanExecute`。如果我们在 `CanAddUser` 方法里返回 `false`(比如名字为空),**按钮会自动变灰(禁用)**。
- **ObservableCollection**:
* 普通的 `List` 添加数据时,界面不会知道。
* `ObservableCollection` 在添加/删除元素时会发出通知,列表控件(ListBox)会自动刷新显示新数据。
=== Tips WPF Binding 中 数据绑定Path 的用法详解 ===
在 WPF 的数据绑定表达式 `{Binding ...}` 中,`Path` 是最常用的属性之一,用于指定数据源中具体的属性名称。
==== 1. 显式使用 Path ====
这是最标准的写法,明确指定了要绑定的属性路径。
* **语法**: `{Binding Path=PropertyName}`
* **含义**: 告诉绑定引擎,去 DataContext(数据上下文)中找名为 `PropertyName` 的属性。
**示例代码:**
==== 2. 隐式使用 Path (省略 Path=) ====
WPF 的绑定语法允许省略 `Path=` 关键字。如果绑定表达式中的第一个参数没有指定属性名,WPF 默认将其视为 `Path`。
* **语法**: `{Binding PropertyName}`
* **含义**: 等同于 `{Binding Path=PropertyName}`。这是最常见的简写方式。
**示例代码:**
==== 3. Path 为 "." (或省略 Path 且为空) ====
有时候我们需要绑定到**数据源对象本身**,而不是它的某个属性。这在列表控件(如 ListBox、ItemsControl)的模板中非常常见,特别是当数据源是简单的字符串列表或整数列表时。
* **语法**: `{Binding Path=.}` 或 `{Binding .}` 或 简单的 `{Binding}`
* **含义**: 绑定到当前的 DataContext 对象本身。
**示例代码:**
**应用场景 (ItemsControl):**
==== 4. 索引器语法 ====
`Path` 支持使用索引器来访问集合或字典中的元素。
* **语法**: `{Binding Path=[Index]}`
* **含义**: 访问集合中特定位置的元素。
**示例代码:**
==== 5. 特殊字符转义 ====
如果绑定的属性名称中包含特殊字符(虽然不推荐属性名包含特殊字符),或者使用了附加属性,通常需要用括号包裹。
* **语法**: `{Binding Path=(OwnerType.AttachedProperty)}`
**示例代码:**
=== 总结 ===
^ 写法 ^ 解释 ^ 备注 ^
| `{Binding Path=Name}` | 标准写法 | 最清晰,适合初学者理解 |
| `{Binding Name}` | 省略 Path | 最常用,简洁 |
| `{Binding}` | 绑定到对象本身 | 常用于简单数据类型的列表模板 |
| `{Binding .}` | 绑定到对象本身 | 同上,显式写法 |