差别
这里会显示出您选择的修订版和当前版本之间的差别。
| 两侧同时换到之前的修订记录 前一修订版 后一修订版 | 前一修订版 | ||
| csharp:wpf基本语法 [2025/11/26 15:40] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | csharp:wpf基本语法 [2025/12/15 15:25] (当前版本) – [核心概念讲解] 张叶安 | ||
|---|---|---|---|
| 行 1: | 行 1: | ||
| + | ====== WPF MVVM 标准示例教程 ====== | ||
| + | 理解 WPF 的核心就在于理解 **MVVM (Model-View-ViewModel)** 模式。 | ||
| + | |||
| + | 在这个例子中,我们将创建一个简单的 **“用户列表管理”** 应用。 | ||
| + | * **功能**:输入姓名和年龄,点击添加按钮,列表会实时更新。 | ||
| + | * **特点**:完全**不使用**传统的事件处理器(如 `Button_Click`),而是使用 **数据绑定 (Data Binding)** 和 **命令 (Command)**。 | ||
| + | |||
| + | ===== 1. 基础架构 (Infrastructure) ===== | ||
| + | |||
| + | 在标准的 MVVM 中,我们需要两个辅助类: | ||
| + | - **ViewModelBase**: | ||
| + | - **RelayCommand**: | ||
| + | |||
| + | <code csharp> | ||
| + | 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? | ||
| + | } | ||
| + | |||
| + | // 辅助方法:设置值并通知 | ||
| + | protected bool SetProperty< | ||
| + | { | ||
| + | if (Equals(field, | ||
| + | field = value; | ||
| + | OnPropertyChanged(propertyName); | ||
| + | return true; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // 2. 命令类:将按钮点击行为绑定到方法 | ||
| + | public class RelayCommand : ICommand | ||
| + | { | ||
| + | private readonly Action< | ||
| + | private readonly Predicate< | ||
| + | |||
| + | public RelayCommand(Action< | ||
| + | { | ||
| + | _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 逻辑。 | ||
| + | |||
| + | <code csharp> | ||
| + | namespace WpfMvvmExample | ||
| + | { | ||
| + | public class User | ||
| + | { | ||
| + | public string Name { get; set; } | ||
| + | public int Age { get; set; } | ||
| + | | ||
| + | // 用于显示的格式化字符串 | ||
| + | public string DisplayInfo => $" | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== 3. 视图模型 (ViewModel) ===== | ||
| + | |||
| + | 这是 MVVM 的核心。它是 View(界面)和 Model(数据)的桥梁。 | ||
| + | * 它持有数据列表 (`ObservableCollection`)。 | ||
| + | * 它持有当前输入的字段。 | ||
| + | * 它持有命令 (`AddUserCommand`)。 | ||
| + | |||
| + | <code csharp> | ||
| + | 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< | ||
| + | |||
| + | // --- 命令 --- | ||
| + | public RelayCommand AddUserCommand { get; set; } | ||
| + | |||
| + | // --- 构造函数 --- | ||
| + | public MainViewModel() | ||
| + | { | ||
| + | Users = new ObservableCollection< | ||
| + | { | ||
| + | new User { Name = " | ||
| + | }; | ||
| + | |||
| + | // 初始化命令:指定执行逻辑(AddUser) 和 判断逻辑(CanAddUser) | ||
| + | AddUserCommand = new RelayCommand(AddUser, | ||
| + | } | ||
| + | |||
| + | // --- 逻辑方法 --- | ||
| + | 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。 | ||
| + | |||
| + | <code xml> | ||
| + | <Window x: | ||
| + | xmlns=" | ||
| + | xmlns: | ||
| + | xmlns: | ||
| + | xmlns: | ||
| + | xmlns: | ||
| + | mc: | ||
| + | Title=" | ||
| + | | ||
| + | <Grid Margin=" | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | <!-- 输入区域 --> | ||
| + | < | ||
| + | < | ||
| + | <!-- UpdateSourceTrigger=PropertyChanged 表示每敲一个字都同步给 ViewModel --> | ||
| + | <TextBox Text=" | ||
| + | | ||
| + | | ||
| + | < | ||
| + | <TextBox Text=" | ||
| + | | ||
| + | </ | ||
| + | |||
| + | <!-- 按钮区域 --> | ||
| + | <!-- Command 绑定到 ViewModel 中的 AddUserCommand --> | ||
| + | <Button Grid.Row=" | ||
| + | Command=" | ||
| + | Width=" | ||
| + | |||
| + | <!-- 列表区域 --> | ||
| + | <!-- ItemsSource 绑定到 Users 集合 --> | ||
| + | <ListBox Grid.Row=" | ||
| + | < | ||
| + | < | ||
| + | <!-- 显示 User 对象的 DisplayInfo 属性 --> | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | ===== 5. 胶水代码 (Code-Behind) ===== | ||
| + | |||
| + | 最后,我们需要把 View 和 ViewModel 连接起来。这通常在 `MainWindow.xaml.cs` 中完成。 | ||
| + | |||
| + | <code csharp> | ||
| + | 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=" | ||
| + | * WPF 会自动调用 `AddUserCommand.Execute` 来运行逻辑。 | ||
| + | * WPF 还会自动调用 `AddUserCommand.CanExecute`。如果我们在 `CanAddUser` 方法里返回 `false`(比如名字为空),**按钮会自动变灰(禁用)**。 | ||
| + | |||
| + | - **ObservableCollection**: | ||
| + | * 普通的 `List< | ||
| + | * `ObservableCollection< | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | === Tips WPF Binding 中 数据绑定Path 的用法详解 === | ||
| + | |||
| + | 在 WPF 的数据绑定表达式 `{Binding ...}` 中,`Path` 是最常用的属性之一,用于指定数据源中具体的属性名称。 | ||
| + | |||
| + | ==== 1. 显式使用 Path ==== | ||
| + | |||
| + | 这是最标准的写法,明确指定了要绑定的属性路径。 | ||
| + | |||
| + | * **语法**: `{Binding Path=PropertyName}` | ||
| + | * **含义**: 告诉绑定引擎,去 DataContext(数据上下文)中找名为 `PropertyName` 的属性。 | ||
| + | |||
| + | **示例代码:** | ||
| + | <code xml> | ||
| + | <!-- 绑定到 ViewModel 的 UserName 属性 --> | ||
| + | < | ||
| + | |||
| + | <!-- 绑定到子属性 (例如 ViewModel 有个 User 对象,User 对象有 Name 属性) --> | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | ==== 2. 隐式使用 Path (省略 Path=) ==== | ||
| + | |||
| + | WPF 的绑定语法允许省略 `Path=` 关键字。如果绑定表达式中的第一个参数没有指定属性名,WPF 默认将其视为 `Path`。 | ||
| + | |||
| + | * **语法**: `{Binding PropertyName}` | ||
| + | * **含义**: 等同于 `{Binding Path=PropertyName}`。这是最常见的简写方式。 | ||
| + | |||
| + | **示例代码:** | ||
| + | <code xml> | ||
| + | <!-- 简写方式,效果同上 --> | ||
| + | < | ||
| + | |||
| + | <!-- 嵌套属性简写 --> | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | ==== 3. Path 为 " | ||
| + | |||
| + | 有时候我们需要绑定到**数据源对象本身**,而不是它的某个属性。这在列表控件(如 ListBox、ItemsControl)的模板中非常常见,特别是当数据源是简单的字符串列表或整数列表时。 | ||
| + | |||
| + | * **语法**: `{Binding Path=.}` 或 `{Binding .}` 或 简单的 `{Binding}` | ||
| + | * **含义**: 绑定到当前的 DataContext 对象本身。 | ||
| + | |||
| + | **示例代码:** | ||
| + | <code xml> | ||
| + | <!-- 假设 DataContext 是一个字符串 "Hello World" --> | ||
| + | |||
| + | <!-- 写法一:显式点号 --> | ||
| + | < | ||
| + | |||
| + | <!-- 写法二:隐式点号 --> | ||
| + | < | ||
| + | |||
| + | <!-- 写法三:完全省略 (最推荐) --> | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | **应用场景 (ItemsControl): | ||
| + | <code xml> | ||
| + | <ListBox ItemsSource=" | ||
| + | < | ||
| + | < | ||
| + | <!-- 这里 Binding 绑定的是 StringList 中的每一个字符串本身 --> | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | ==== 4. 索引器语法 ==== | ||
| + | |||
| + | `Path` 支持使用索引器来访问集合或字典中的元素。 | ||
| + | |||
| + | * **语法**: `{Binding Path=[Index]}` | ||
| + | * **含义**: 访问集合中特定位置的元素。 | ||
| + | |||
| + | **示例代码:** | ||
| + | <code xml> | ||
| + | <!-- 绑定到某列表属性的第一个元素 --> | ||
| + | < | ||
| + | |||
| + | <!-- 如果 DataContext 本身就是列表,访问其第二个元素 --> | ||
| + | < | ||
| + | |||
| + | <!-- 字典查表:绑定到 Key 为 " | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | ==== 5. 特殊字符转义 ==== | ||
| + | |||
| + | 如果绑定的属性名称中包含特殊字符(虽然不推荐属性名包含特殊字符),或者使用了附加属性,通常需要用括号包裹。 | ||
| + | |||
| + | * **语法**: `{Binding Path=(OwnerType.AttachedProperty)}` | ||
| + | |||
| + | **示例代码:** | ||
| + | <code xml> | ||
| + | <!-- 绑定到 Grid 的 Row 附加属性 --> | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | === 总结 === | ||
| + | |||
| + | ^ 写法 ^ 解释 ^ 备注 ^ | ||
| + | | `{Binding Path=Name}` | 标准写法 | 最清晰,适合初学者理解 | | ||
| + | | `{Binding Name}` | 省略 Path | 最常用,简洁 | | ||
| + | | `{Binding}` | 绑定到对象本身 | 常用于简单数据类型的列表模板 | | ||
| + | | `{Binding .}` | 绑定到对象本身 | 同上,显式写法 | | ||