C# 零基础到精通教程 - WPF 专题二:数据绑定与 MVVM

写在前面

专题一我们学习了 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 练习题

基础题

  1. 创建一个简单的温度转换器,实现摄氏度和华氏度的双向转换,使用 TwoWay 绑定。

  2. 实现 INotifyPropertyChanged,创建一个个人信息 ViewModel,UI 能够实时显示用户的修改。

  3. 使用 ObservableCollection 实现一个待办事项列表,支持添加、删除、标记完成。

应用题

  1. 实现一个购物车应用:

    • 商品列表(从数据源加载)

    • 添加到购物车

    • 购物车显示数量、总价

    • 支持删除和修改数量

  2. 实现一个数据验证系统:

    • 用户注册表单

    • 验证:姓名非空、邮箱格式、年龄范围

    • 实时显示验证错误

    • 提交按钮在验证通过前禁用

相关推荐
Xin_ye100861 小时前
C# 零基础到精通教程 - WPF 专题一:WPF 入门与 XAML 基础
c#·wpf
我是一颗柠檬1 小时前
【Java后端技术亮点】Feed流三级缓存设计,从10秒到100毫秒的优化实战
java·开发语言·后端·缓存
兆。1 小时前
LangChain文档处理集成指南:面向知识管理开发者
开发语言·langchain·c#
Brilliantwxx1 小时前
【算法从零到千】【1-7】 双指针算法
开发语言·c++·笔记·算法·leetcode·推荐算法
超梦dasgg1 小时前
Java 正则表达式 完整详解(语法 + 核心类 + 常用方法 + 实战案例)
java·开发语言·正则表达式
方也_arkling1 小时前
【Java-Day17】API篇-BigInteger和BigDecimal
java·开发语言
星辰_mya1 小时前
ThreadLocal之微服务链路追踪
java·开发语言·前端
m0_617493941 小时前
PySide6/PyQt6实现中英文切换完整教程(Qt Designer + Qt Linguist + 动态切换)
开发语言·qt
眠りたいです1 小时前
现代C++:C++17中的新语言特性
开发语言·c++·c++17