写在前面
专题一我们学习了 WPF 的基础知识和 XAML 布局。但如果你把所有逻辑都写在后台代码(xaml.cs)里,随着应用变大,代码会变得难以维护。这一专题要学习的是 WPF 的核心精髓------数据绑定和 MVVM 模式,这是开发专业 WPF 应用的必经之路。
2.1 什么是数据绑定?
2.1.1 传统方式 vs 数据绑定
csharp
// 传统 WinForms 方式:手动同步
// 文本框内容改变时,需要手动更新变量
textBox1.TextChanged += (s, e) => {
userName = textBox1.Text;
};
// 变量改变时,需要手动更新 UI
userName = "张三";
textBox1.Text = userName;
// WPF 数据绑定:自动同步
// XAML
<TextBox Text="{Binding UserName}" />
// C#:只需改变数据源,UI 自动更新
UserName = "张三"; // UI 自动更新
2.1.2 数据绑定的核心概念
text
┌─────────────────────────────────────────────────────────────────┐
│ 数据绑定架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 数据源 │ │ 目标 │ │
│ │ (Source) │ │ (Target) │ │
│ │ │ 数据绑定 │ │ │
│ │ ViewModel │ ◄──────────────────► │ 控件 │ │
│ │ Model │ │ (TextBox) │ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ INotifyPropertyChanged │ │ 依赖属性 │ │
│ │ 通知源变化 │ │ (Dependency │ │
│ └──────────────────────────┘ │ Property) │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2 绑定的基本使用
2.2.1 简单绑定
xml
<Window x:Class="BindingDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="绑定演示" Height="450" Width="500">
<StackPanel Margin="20">
<!-- 绑定到控件的另一个属性 -->
<TextBox x:Name="InputBox" Margin="0,10"/>
<TextBlock Text="{Binding Text, ElementName=InputBox}" Margin="0,10"/>
<!-- 绑定到静态资源 -->
<TextBlock Text="{x:Static sys:Environment.MachineName}" Margin="0,10"/>
<!-- 绑定到相对源 -->
<Button Content="点击我" Width="100"
Tag="{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth}"/>
<!-- 绑定到父级数据上下文 -->
<TextBlock Text="{Binding Path=UserName}" Margin="0,10"/>
</StackPanel>
</Window>
2.2.2 绑定模式
| 模式 | 说明 | 场景 |
|---|---|---|
OneWay |
源变化 → 目标变化(单向) | 显示数据 |
TwoWay |
双向同步 | 编辑表单 |
OneTime |
初始化一次 | 静态标签 |
OneWayToSource |
目标变化 → 源变化 | 只收集输入 |
xml
<StackPanel Margin="20">
<!-- OneWay:源变化时更新目标 -->
<TextBlock Text="{Binding CurrentTime, Mode=OneWay}" />
<!-- TwoWay:双向绑定(用户输入会更新源)-->
<TextBox Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<!-- OneTime:只在初始化时绑定一次 -->
<TextBlock Text="{Binding Version, Mode=OneTime}"/>
<!-- OneWayToSource:只在目标变化时更新源 -->
<TextBox Text="{Binding SearchKeyword, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"/>
<!-- 默认模式取决于目标属性 -->
<!-- TextBox.Text 默认是 TwoWay -->
<!-- TextBlock.Text 默认是 OneWay -->
<!-- UpdateSourceTrigger:控制何时更新源 -->
<!--
PropertyChanged - 每次属性变化都更新
LostFocus - 失去焦点时更新(TextBox 默认)
Explicit - 手动调用 UpdateSource()
-->
</StackPanel>
2.2.3 绑定路径
xml
<StackPanel>
<!-- 简单属性绑定 -->
<TextBlock Text="{Binding Name}"/>
<!-- 嵌套属性绑定 -->
<TextBlock Text="{Binding Address.City}"/>
<!-- 索引器绑定 -->
<TextBlock Text="{Binding Items[0].Name}"/>
<!-- 集合计数 -->
<TextBlock Text="{Binding Items.Count}"/>
<!-- 路径中的路径 -->
<TextBlock Text="{Binding /Name}"/> <!-- 当前项的 Name -->
<TextBlock Text="{Binding /Items/Name}"/> <!-- 集合中所有项的 Name -->
</StackPanel>
2.2.4 值转换器(Value Converter)
csharp
// 自定义转换器
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}
public class PriceToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double price)
{
if (price < 50) return "Green";
if (price < 100) return "Orange";
return "Red";
}
return "Black";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class GenderToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Gender gender)
{
return gender == Gender.Male ? "男" : "女";
}
return "未知";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
string str = value as string;
if (str == "男") return Gender.Male;
if (str == "女") return Gender.Female;
return Gender.Unknown;
}
}
public enum Gender { Male, Female, Unknown }
// XAML 中使用转换器
<Window.Resources>
<local:BooleanToVisibilityConverter x:Key="BoolToVis"/>
<local:PriceToColorConverter x:Key="PriceToColor"/>
<local:GenderToStringConverter x:Key="GenderToString"/>
</Window.Resources>
<StackPanel>
<!-- 可见性转换 -->
<Button Content="只对管理员可见" Visibility="{Binding IsAdmin, Converter={StaticResource BoolToVis}}"/>
<!-- 价格转颜色 -->
<TextBlock Text="{Binding Price, StringFormat=C}"
Foreground="{Binding Price, Converter={StaticResource PriceToColor}}"/>
<!-- 枚举转字符串 -->
<TextBlock Text="{Binding Gender, Converter={StaticResource GenderToString}}"/>
</StackPanel>
2.3 INotifyPropertyChanged
2.3.1 为什么需要这个接口?
csharp
// 不实现 INotifyPropertyChanged:UI 不会自动更新
public class Person
{
public string Name { get; set; } // 改变时 UI 不会更新!
}
// 实现 INotifyPropertyChanged:UI 自动更新
public class ObservablePerson : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name)); // 通知 UI 更新
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
2.3.2 基类实现
csharp
// 创建可观测对象基类
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual 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 (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
// 批量更新时暂停通知(可选)
private bool _isBulkUpdating;
private List<string> _pendingChanges = new List<string>();
protected void BeginBulkUpdate()
{
_isBulkUpdating = true;
_pendingChanges.Clear();
}
protected void EndBulkUpdate()
{
_isBupdating = false;
foreach (var property in _pendingChanges.Distinct())
{
OnPropertyChanged(property);
}
_pendingChanges.Clear();
}
protected bool SetPropertyWithBulk<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
if (_isBulkUpdating)
{
_pendingChanges.Add(propertyName);
}
else
{
OnPropertyChanged(propertyName);
}
return true;
}
}
// ViewModel 使用基类
public class UserViewModel : ObservableObject
{
private string _name;
private int _age;
private string _email;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public int Age
{
get => _age;
set => SetProperty(ref _age, value);
}
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
}
2.3.3 集合变化通知
csharp
// ObservableCollection 自动通知集合变化(添加、删除、清空)
public class UserListViewModel : ObservableObject
{
public ObservableCollection<UserViewModel> Users { get; set; }
public UserListViewModel()
{
Users = new ObservableCollection<UserViewModel>();
}
public void AddUser(string name, int age)
{
Users.Add(new UserViewModel { Name = name, Age = age });
// UI 自动更新
}
public void RemoveUser(UserViewModel user)
{
Users.Remove(user);
// UI 自动更新
}
}
2.4 MVVM 模式
2.4.1 MVVM 架构图
text
┌─────────────────────────────────────────────────────────────────┐
│ MVVM 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ View │ │ ViewModel │ │ Model │ │
│ │ (视图) │ │ (视图模型) │ │ (模型) │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ • XAML │ │ • 业务逻辑 │ │ • 数据实体 │ │
│ │ • 控件 │ ◄──► │ • 命令 │ ◄──► │ • 数据访问 │ │
│ │ • 样式 │ │ • 状态管理 │ │ • 验证规则 │ │
│ │ • 动画 │ │ • 数据绑定 │ │ • 实体关系 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 数据绑定层 │ │
│ │ INotifyPropertyChanged, ICommand, IDataErrorInfo │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.4.2 三层职责
| 层次 | 职责 | 不应包含 |
|---|---|---|
| View | 展示 UI、用户交互 | 业务逻辑、数据访问 |
| ViewModel | 业务逻辑、状态管理、命令 | 数据库操作、UI 代码 |
| Model | 数据实体、验证规则、数据访问 | UI 相关代码 |
2.4.3 完整 MVVM 示例
csharp
// ========== Model ==========
// Models/User.cs
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public DateTime CreatedAt { get; set; }
}
// Services/IUserService.cs
public interface IUserService
{
Task<List<User>> GetUsersAsync();
Task<User> GetUserAsync(int id);
Task<User> CreateUserAsync(User user);
Task UpdateUserAsync(User user);
Task DeleteUserAsync(int id);
}
// Services/UserService.cs
public class UserService : IUserService
{
private readonly List<User> _users = new();
private int _nextId = 1;
public UserService()
{
// 模拟数据
_users.Add(new User { Id = _nextId++, Name = "张三", Email = "zhangsan@example.com", Age = 25, CreatedAt = DateTime.Now.AddDays(-10) });
_users.Add(new User { Id = _nextId++, Name = "李四", Email = "lisi@example.com", Age = 30, CreatedAt = DateTime.Now.AddDays(-5) });
_users.Add(new User { Id = _nextId++, Name = "王五", Email = "wangwu@example.com", Age = 28, CreatedAt = DateTime.Now.AddDays(-2) });
}
public async Task<List<User>> GetUsersAsync()
{
await Task.Delay(100); // 模拟网络延迟
return _users.ToList();
}
public async Task<User> GetUserAsync(int id)
{
await Task.Delay(100);
return _users.FirstOrDefault(u => u.Id == id);
}
public async Task<User> CreateUserAsync(User user)
{
await Task.Delay(100);
user.Id = _nextId++;
user.CreatedAt = DateTime.Now;
_users.Add(user);
return user;
}
public async Task UpdateUserAsync(User user)
{
await Task.Delay(100);
var existing = _users.FirstOrDefault(u => u.Id == user.Id);
if (existing != null)
{
existing.Name = user.Name;
existing.Email = user.Email;
existing.Age = user.Age;
}
}
public async Task DeleteUserAsync(int id)
{
await Task.Delay(100);
var user = _users.FirstOrDefault(u => u.Id == id);
if (user != null)
{
_users.Remove(user);
}
}
}
// ========== ViewModel ==========
// ViewModels/RelayCommand.cs
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 ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
}
// ViewModels/UserViewModel.cs
public class UserViewModel : ObservableObject
{
private User _user;
private bool _isSelected;
public UserViewModel(User user)
{
_user = user;
}
public int Id => _user.Id;
public string Name
{
get => _user.Name;
set
{
if (SetProperty(ref _user.Name, value))
{
// 可以添加验证逻辑
ValidateName();
}
}
}
public string Email
{
get => _user.Email;
set => SetProperty(ref _user.Email, value);
}
public int Age
{
get => _user.Age;
set => SetProperty(ref _user.Age, value);
}
public DateTime CreatedAt => _user.CreatedAt;
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
private void ValidateName()
{
if (string.IsNullOrWhiteSpace(Name))
{
// 可以设置错误信息
}
}
}
// ViewModels/UserListViewModel.cs
public class UserListViewModel : ObservableObject
{
private readonly IUserService _userService;
private ObservableCollection<UserViewModel> _users;
private UserViewModel _selectedUser;
private bool _isLoading;
private string _searchKeyword;
public UserListViewModel(IUserService userService)
{
_userService = userService;
_users = new ObservableCollection<UserViewModel>();
// 初始化命令
LoadUsersCommand = new RelayCommand(async _ => await LoadUsersAsync(), _ => !IsLoading);
AddUserCommand = new RelayCommand(_ => OpenAddUserDialog(), _ => !IsLoading);
EditUserCommand = new RelayCommand(_ => OpenEditUserDialog(), _ => SelectedUser != null);
DeleteUserCommand = new RelayCommand(async _ => await DeleteUserAsync(), _ => SelectedUser != null);
RefreshCommand = new RelayCommand(async _ => await LoadUsersAsync());
}
public ObservableCollection<UserViewModel> Users
{
get => _users;
set => SetProperty(ref _users, value);
}
public UserViewModel SelectedUser
{
get => _selectedUser;
set
{
SetProperty(ref _selectedUser, value);
// 当选中变化时,通知相关命令的可用性变化
(EditUserCommand as RelayCommand)?.RaiseCanExecuteChanged();
(DeleteUserCommand as RelayCommand)?.RaiseCanExecuteChanged();
}
}
public bool IsLoading
{
get => _isLoading;
set
{
SetProperty(ref _isLoading, value);
(LoadUsersCommand as RelayCommand)?.RaiseCanExecuteChanged();
(AddUserCommand as RelayCommand)?.RaiseCanExecuteChanged();
(RefreshCommand as RelayCommand)?.RaiseCanExecuteChanged();
}
}
public string SearchKeyword
{
get => _searchKeyword;
set
{
if (SetProperty(ref _searchKeyword, value))
{
// 搜索关键词变化时,触发筛选
ApplyFilter();
}
}
}
// 命令属性
public ICommand LoadUsersCommand { get; }
public ICommand AddUserCommand { get; }
public ICommand EditUserCommand { get; }
public ICommand DeleteUserCommand { get; }
public ICommand RefreshCommand { get; }
private async Task LoadUsersAsync()
{
IsLoading = true;
try
{
var users = await _userService.GetUsersAsync();
Users.Clear();
foreach (var user in users)
{
Users.Add(new UserViewModel(user));
}
ApplyFilter();
}
catch (Exception ex)
{
MessageBox.Show($"加载用户失败:{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsLoading = false;
}
}
private void ApplyFilter()
{
if (string.IsNullOrWhiteSpace(SearchKeyword))
{
// 显示所有
foreach (var user in Users)
{
// 这里需要结合 ItemsSource 的筛选
}
}
}
private void OpenAddUserDialog()
{
var dialog = new EditUserDialog();
dialog.Owner = Application.Current.MainWindow;
if (dialog.ShowDialog() == true)
{
// 添加新用户
_ = AddUserAsync(dialog.Result);
}
}
private async Task AddUserAsync(User userData)
{
IsLoading = true;
try
{
var newUser = await _userService.CreateUserAsync(userData);
Users.Add(new UserViewModel(newUser));
MessageBox.Show("用户添加成功", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"添加用户失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsLoading = false;
}
}
private void OpenEditUserDialog()
{
if (SelectedUser == null) return;
var dialog = new EditUserDialog(SelectedUser);
dialog.Owner = Application.Current.MainWindow;
if (dialog.ShowDialog() == true)
{
// 更新用户
_ = UpdateUserAsync(dialog.Result);
}
}
private async Task UpdateUserAsync(User userData)
{
IsLoading = true;
try
{
await _userService.UpdateUserAsync(userData);
// 更新 ViewModel 中的数据
var vm = Users.FirstOrDefault(u => u.Id == userData.Id);
if (vm != null)
{
vm.Name = userData.Name;
vm.Email = userData.Email;
vm.Age = userData.Age;
}
MessageBox.Show("用户更新成功", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"更新用户失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsLoading = false;
}
}
private async Task DeleteUserAsync()
{
if (SelectedUser == null) return;
var result = MessageBox.Show($"确定要删除用户“{SelectedUser.Name}”吗?", "确认删除",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) return;
IsLoading = true;
try
{
await _userService.DeleteUserAsync(SelectedUser.Id);
Users.Remove(SelectedUser);
MessageBox.Show("用户删除成功", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"删除用户失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsLoading = false;
}
}
}
// ========== View ==========
<!-- Views/UserListView.xaml -->
<Window x:Class="WpfMvvmDemo.Views.UserListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="用户管理" Height="500" Width="700"
WindowStartupLocation="CenterScreen">
<Window.DataContext>
<local:UserListViewModel/>
</Window.DataContext>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题栏 -->
<Border Grid.Row="0" Background="#2196F3" Padding="10" Margin="-10,0,-10,10">
<TextBlock Text="用户管理" FontSize="20" FontWeight="Bold" Foreground="White"/>
</Border>
<!-- 工具栏 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="刷新" Command="{Binding RefreshCommand}" Width="80" Margin="0,0,10,0"/>
<Button Content="添加" Command="{Binding AddUserCommand}" Width="80" Margin="0,0,10,0"/>
<Button Content="编辑" Command="{Binding EditUserCommand}" Width="80" Margin="0,0,10,0"/>
<Button Content="删除" Command="{Binding DeleteUserCommand}" Width="80" Margin="0,0,10,0"/>
<Separator Width="20"/>
<Label Content="搜索:" VerticalAlignment="Center"/>
<TextBox Text="{Binding SearchKeyword, UpdateSourceTrigger=PropertyChanged}"
Width="200" Margin="5,0"/>
<ProgressBar IsIndeterminate="{Binding IsLoading}"
Width="100" Height="20" Margin="20,0,0,0"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVis}}"/>
</StackPanel>
<!-- 用户列表 -->
<DataGrid Grid.Row="2"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser}"
AutoGenerateColumns="False"
IsReadOnly="True"
AlternatingRowBackground="#F5F5F5">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="50"/>
<DataGridTextColumn Header="姓名" Binding="{Binding Name}" Width="120"/>
<DataGridTextColumn Header="邮箱" Binding="{Binding Email}" Width="200"/>
<DataGridTextColumn Header="年龄" Binding="{Binding Age}" Width="80"/>
<DataGridTextColumn Header="创建时间" Binding="{Binding CreatedAt, StringFormat=yyyy-MM-dd}" Width="120"/>
</DataGrid.Columns>
</DataGrid>
<!-- 状态栏 -->
<StatusBar Grid.Row="3" Margin="-10,-5,-10,-10" Height="30">
<StatusBarItem>
<TextBlock>
共 <Run Text="{Binding Users.Count}"/> 条记录
</TextBlock>
</StatusBarItem>
<Separator/>
<StatusBarItem>
<TextBlock Text="{Binding SelectedUser.Name, StringFormat=当前选中:{0}}" />
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
2.4.4 编辑用户对话框
xml
<!-- Views/EditUserDialog.xaml -->
<Window x:Class="WpfMvvmDemo.Views.EditUserDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="用户信息" Height="350" Width="400"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize">
<Window.Resources>
<Style TargetType="TextBox">
<Setter Property="Height" Value="30"/>
<Setter Property="Margin" Value="0,5,0,10"/>
<Setter Property="Padding" Value="5"/>
</Style>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="基本信息" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
<Label Grid.Row="1" Content="姓名:" Target="{Binding ElementName=NameBox}"/>
<TextBox Grid.Row="2" x:Name="NameBox" Text="{Binding User.Name, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="3" Content="邮箱:" Target="{Binding ElementName=EmailBox}" Margin="0,5,0,0"/>
<TextBox Grid.Row="4" x:Name="EmailBox" Text="{Binding User.Email, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="5" Content="年龄:" Target="{Binding ElementName=AgeBox}" Margin="0,5,0,0"/>
<TextBox Grid.Row="6" x:Name="AgeBox" Text="{Binding User.Age, UpdateSourceTrigger=PropertyChanged}"/>
<StackPanel Grid.Row="7" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
<Button Content="确定" Width="80" Height="30" Margin="0,0,10,0"
Click="OkButton_Click" IsDefault="True"/>
<Button Content="取消" Width="80" Height="30"
Click="CancelButton_Click" IsCancel="True"/>
</StackPanel>
</Grid>
</Window>
csharp
// Views/EditUserDialog.xaml.cs
public partial class EditUserDialog : Window
{
public User Result { get; private set; }
private User _user;
public EditUserDialog() : this(null)
{
}
public EditUserDialog(UserViewModel userVm)
{
InitializeComponent();
if (userVm != null)
{
_user = new User
{
Id = userVm.Id,
Name = userVm.Name,
Email = userVm.Email,
Age = userVm.Age
};
}
else
{
_user = new User();
}
DataContext = this;
}
public User User => _user;
private void OkButton_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(_user.Name))
{
MessageBox.Show("请输入姓名", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
Result = _user;
DialogResult = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
2.4.5 依赖注入配置
csharp
// App.xaml.cs
public partial class App : Application
{
private IServiceProvider _serviceProvider;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
// 注册服务
services.AddSingleton<IUserService, UserService>();
services.AddTransient<UserListViewModel>();
services.AddTransient<MainWindow>();
_serviceProvider = services.BuildServiceProvider();
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
}
2.5 常见错误与陷阱
错误1:忘记设置 DataContext
csharp
// ❌ 错误:绑定找不到数据
<TextBlock Text="{Binding UserName}" /> // DataContext 为 null
// ✅ 正确
this.DataContext = viewModel;
错误2:INotifyPropertyChanged 拼写错误
csharp
// ❌ 错误:字符串错误
OnPropertyChanged("username"); // 大小写不匹配
// ✅ 正确
OnPropertyChanged(nameof(UserName)); // 使用 nameof 避免错误
错误3:集合更新不通知
csharp
// ❌ 错误:使用普通 List
public List<User> Users { get; set; } // 增删时 UI 不更新
// ✅ 正确:使用 ObservableCollection
public ObservableCollection<User> Users { get; set; } // 自动通知
错误4:命令未触发 CanExecute 刷新
csharp
// ❌ 错误:修改属性后命令状态未更新
public bool CanSave => HasChanges; // 属性变化时,CanSave 不会重新评估
// ✅ 正确:手动刷新命令状态
CommandManager.InvalidateRequerySuggested();
2.6 练习题
基础题
-
创建一个简单的温度转换器,实现摄氏度和华氏度的双向转换,使用 TwoWay 绑定。
-
实现 INotifyPropertyChanged,创建一个个人信息 ViewModel,UI 能够实时显示用户的修改。
-
使用 ObservableCollection 实现一个待办事项列表,支持添加、删除、标记完成。
应用题
-
实现一个购物车应用:
-
商品列表(从数据源加载)
-
添加到购物车
-
购物车显示数量、总价
-
支持删除和修改数量
-
-
实现一个数据验证系统:
-
用户注册表单
-
验证:姓名非空、邮箱格式、年龄范围
-
实时显示验证错误
-
提交按钮在验证通过前禁用
-