1.8. WPF 进阶:MVVM 与实践
MVVM 模式是 WPF 的核心设计模式。
1.8.1. MVVM 模式详解
┌─────────────────────────────────────────────────────────┐
│ View(XAML) │
│ 负责 UI 呈现、动画、视觉布局 │
│ 通过 Binding 连接到 ViewModel │
│ × 尽量不包含代码逻辑 │
├─────────────────────────────────────────────────────────┤
│ ViewModel │
│ 负责 UI 状态和逻辑 │
│ 实现 INotifyPropertyChanged │
│ 暴露属性 + 命令(ICommand) │
│ × 不直接引用 UI 控件 │
├─────────────────────────────────────────────────────────┤
│ Model │
│ 业务数据 + 领域逻辑 │
│ 数据库/网络/文件访问 │
│ × 不知道 View 和 ViewModel 的存在 │
└─────────────────────────────────────────────────────────┘
1.8.2. 完整 MVVM 示例
1.8.2.1. Model
public class UserModel
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public DateTime CreatedAt { get; set; }
}
1.8.2.2. ViewModel
public class UserViewModel : ViewModelBase
{
private readonly IUserService _userService;
private ObservableCollection<UserModel> _users = new();
public ObservableCollection<UserModel> Users
{
get => _users;
set => SetProperty(ref _users, value);
}
private UserModel? _selectedUser;
public UserModel? SelectedUser
{
get => _selectedUser;
set
{
SetProperty(ref _selectedUser, value);
DeleteCommand.RaiseCanExecuteChanged();
}
}
private bool _isLoading;
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
private string _searchText = "";
public string SearchText
{
get => _searchText;
set
{
SetProperty(ref _searchText, value);
_ = SearchAsync(); // 输入即搜索
}
}
// 命令
public AsyncRelayCommand LoadCommand { get; }
public RelayCommand DeleteCommand { get; }
public UserViewModel(IUserService userService)
{
_userService = userService;
LoadCommand = new AsyncRelayCommand(async () =>
{
IsLoading = true;
try
{
var result = await _userService.GetAllAsync();
Users = new ObservableCollection<UserModel>(result);
}
finally
{
IsLoading = false;
}
});
DeleteCommand = new RelayCommand(
execute: async _ =>
{
if (SelectedUser == null) return;
await _userService.DeleteAsync(SelectedUser.Id);
Users.Remove(SelectedUser);
},
canExecute: _ => SelectedUser != null);
}
private async Task SearchAsync()
{
if (string.IsNullOrWhiteSpace(SearchText))
{
await LoadCommand.ExecuteAsync(null);
return;
}
var result = await _userService.SearchAsync(SearchText);
Users = new ObservableCollection<UserModel>(result);
}
}
1.8.2.3. View(XAML)
<Window x:Class="WpfApp.Views.UserListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
Title="用户管理" Height="600" Width="900">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 搜索栏 -->
<StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,12">
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Width="250" PlaceholderText="搜索用户..."/>
<Button Content="加载" Command="{Binding LoadCommand}"
Width="80" Margin="10,0,0,0"/>
</StackPanel>
<!-- 用户列表 -->
<DataGrid Grid.Row="1"
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="150"/>
<DataGridTextColumn Header="邮箱" Binding="{Binding Email}" Width="*"/>
<DataGridTextColumn Header="创建时间"
Binding="{Binding CreatedAt, StringFormat={}{0:yyyy-MM-dd}}"
Width="120"/>
</DataGrid.Columns>
</DataGrid>
<!-- 状态栏 -->
<StatusBar Grid.Row="2" Margin="0,12,0,0">
<TextBlock Text="{Binding StatusText}"/>
<Separator/>
<TextBlock Text="{Binding Users.Count, StringFormat=共 {0} 条记录}"/>
</StatusBar>
</Grid>
</Window>
1.8.2.4. Code-Behind(仅 View 层)
public partial class UserListView : Window
{
public UserListView()
{
InitializeComponent();
// ViewModel 通过 DI 注入
DataContext = App.GetService<UserViewModel>();
Loaded += async (_, _) =>
{
if (DataContext is UserViewModel vm)
await vm.LoadCommand.ExecuteAsync(null);
};
}
}
1.8.3. 依赖注入集成
// App.xaml.cs
public partial class App : Application
{
private static IServiceProvider? _serviceProvider;
protected override void OnStartup(StartupEventArgs e)
{
var services = new ServiceCollection();
// 注册服务
services.AddSingleton<IUserService, UserService>();
services.AddDbContext<AppDbContext>();
// 注册 ViewModels
services.AddTransient<UserViewModel>();
services.AddTransient<MainViewModel>();
// 注册 Views
services.AddTransient<UserListView>();
services.AddTransient<MainWindow>();
_serviceProvider = services.BuildServiceProvider();
// 启动主窗口
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
public static T GetService<T>() where T : notnull
=> _serviceProvider!.GetRequiredService<T>();
}
1.8.4. 触发器与动画
<Button Content="悬停效果">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="Gray"/>
<!-- 属性触发器 -->
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Blue"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="DarkBlue"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<!-- DataTrigger(根据数据改变 UI) -->
<TextBlock Text="{Binding Status}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Status}" Value="Error">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="Success">
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
1.8.5. 转换器
// 值转换器
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
bool boolValue = (bool)value;
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return (Visibility)value == Visibility.Visible;
}
}
// 注册并使用
// <Window.Resources>
// <local:BoolToVisibilityConverter x:Key="BoolToVis"/>
// </Window.Resources>
// <TextBlock Visibility="{Binding IsActive,
// Converter={StaticResource BoolToVis}}"/>