📚 目录
- 项目概述
- MVVM模式介绍
- 项目结构详解
- 核心技术实现
- [1. 页面切换实现](#1. 页面切换实现)
- [2. 数据绑定实现](#2. 数据绑定实现)
- [3. 事件绑定实现](#3. 事件绑定实现)
- [4. 依赖注入(DI)](#4. 依赖注入(DI))
- [5. 值转换器(Converters)](#5. 值转换器(Converters))
- 新手入门指南
- 常见问题解答
项目概述
这是一个基于 WPF MVVM架构 的上位机监控系统,具有以下特点:
核心功能
- ✅ 设备监控:实时监控压力、流量、温度、电机转速等参数
- ✅ 数据采集:通过PLC通信
- ✅ 报警管理:实时报警和历史报警记录
- ✅ 数据查询:生产记录查询和导出
- ✅ 系统设置:设备参数配置
- ✅ 日志管理:运行日志记录和查看
技术栈
xml
<!-- 主要NuGet包 -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageReference Include="LiveCharts.Wpf" Version="0.9.7" />
<PackageReference Include="HandyControl" Version="3.5.1" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="NModbus4" Version="2.1.0" />
<PackageReference Include="FreeSql.Provider.SqliteCore" Version="3.5.305" />
MVVM模式介绍
什么是MVVM?
MVVM (Model-View-ViewModel) 是一种专门为WPF设计的架构模式:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ View │◄───────►│ ViewModel │◄───────►│ Model │
│ (XAML UI) │ 绑定 │ (逻辑层) │ 数据 │ (数据模型) │
└─────────────┘ └─────────────┘ └─────────────┘
三层职责
1. Model(模型层)
- 纯数据类,不包含UI逻辑
- 负责数据的存储和业务逻辑
示例 :<Models/DeviceState.cs>
csharp
public class DeviceState
{
public double Pressure { get; set; } // 压力
public double FlowRate { get; set; } // 流量
public double CurrentTemp { get; set; } // 温度
public int ActualCount { get; set; } // 产量
}
2. View(视图层)
- XAML文件,定义UI界面
- 通过数据绑定与ViewModel通信
- 不包含业务逻辑,只负责界面展示
示例 :<Views/DashBoardView.xaml>
xml
<!-- 绑定ViewModel中的属性 -->
<TextBlock Text="{Binding CurrentTemp}"
FontSize="32"
FontWeight="Bold"/>
3. ViewModel(视图模型层)
- 连接View和Model的桥梁
- 包含界面逻辑和状态
- 使用属性提供数据给View
- 使用命令响应用户操作
示例 :<ViewModels/DashBoardViewModel.cs>
csharp
public partial class DashBoardViewModel : ObservableObject
{
[ObservableProperty]
private double currentTemp; // 属性:自动生成CurrentTemp
[RelayCommand]
private async Task StartProductionAsync() // 命令:自动生成StartProductionCommand
{
// 启动生产的逻辑
}
}
核心技术实现
1. 页面切换实现
原理图解
┌─────────────────────────────────────────────────────┐
│ MainWindow (主窗口) │
├────────────┬────────────────────────────────────────┤
│ │ │
│ Navigation │ ContentControl │
│ Menu │ (Content="{Binding MainContent}") │
│ │ │
│ - 监控 │ ┌──────────────────────┐ │
│ - 查询 │ │ 当前显示的页面 │ │
│ - 报警 │ │ (DashBoardView) │ │
│ - 设置 │ │ (AlarmsView) │ │
│ │ └──────────────────────┘ │
└────────────┴────────────────────────────────────────┘
实现步骤
步骤1:主窗口XAML设置内容容器
MainWindow.xaml
xml
<Window x:Class="WPF_MVVM_ModbusRTU.MainWindow">
<Grid>
<!-- 左侧导航栏 -->
<UserControls:NavigationMenu Grid.Column="0" Grid.RowSpan="2"/>
<!-- 右侧内容区 - 页面切换的关键 -->
<ContentControl Grid.Column="1" Grid.Row="1"
Content="{Binding MainContent}"/>
</Grid>
</Window>
关键点:
ContentControl是一个容器,可以显示任何内容Content="{Binding MainContent}"绑定到ViewModel的MainContent属性- 当MainContent改变时,显示的内容自动切换
步骤2:MainWindowViewModel定义页面切换逻辑
MainWindowViewModel.cs
csharp
public partial class MainWindowViewModel : ObservableObject
{
private readonly IServiceProvider _serviceProvider;
// 属性:当前显示的页面内容
[ObservableProperty]
private object mainContent;
public MainWindowViewModel(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
NavgiateToDashboard(); // 启动时默认显示监控页面
}
// 命令:导航到指定页面
[RelayCommand]
private void Navgiate(string? destination)
{
if (string.IsNullOrEmpty(destination)) return;
switch (destination)
{
case "DashBoard":
// 从DI容器获取ViewModel实例
MainContent = _serviceProvider.GetRequiredService<DashBoardViewModel>();
break;
case "DashQuery":
MainContent = _serviceProvider.GetRequiredService<DashQueryViewModel>();
break;
case "Alarms":
MainContent = _serviceProvider.GetRequiredService<AlarmsViewModel>();
break;
case "Setting":
MainContent = _serviceProvider.GetRequiredService<SettingViewModel>();
break;
}
}
}
关键点:
[ObservableProperty]自动生成属性更改通知- 当
MainContent赋值时,界面自动更新 - 使用依赖注入获取ViewModel实例
步骤3:导航菜单触发切换命令
xml
<UserControl>
<StackPanel>
<!-- 单选按钮样式,点击时触发导航命令 -->
<RadioButton GroupName="Nav"
IsChecked="True"
Command="{Binding DataContext.NavgiateCommand,
RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="DashBoard">
<StackPanel Orientation="Horizontal">
<TextBlock Text="📊" FontSize="16"/>
<TextBlock Text="主页监控"/>
</StackPanel>
</RadioButton>
<RadioButton GroupName="Nav"
Command="{Binding DataContext.NavgiateCommand,
RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Alarms">
<StackPanel Orientation="Horizontal">
<TextBlock Text="⚠" FontSize="16"/>
<TextBlock Text="报警管理"/>
</StackPanel>
</RadioButton>
</StackPanel>
</UserControl>
关键点:
Command="{Binding DataContext.NavgiateCommand, ...}"- 因为UserControl的DataContext是ViewModel,但命令在MainWindow的DataContext中
- 使用
RelativeSource找到父级Window的DataContext
CommandParameter="DashBoard"传递导航目标GroupName="Nav"确保只能选中一个
步骤4:自动关联View和ViewModel
WPF会自动根据ViewModel类型查找对应的View:
DashBoardViewModel → 自动查找 → DashBoardView
AlarmsViewModel → 自动查找 → AlarmsView
命名规则:
- ViewModel:
DashBoardViewModel - View:
DashBoardView(去掉ViewModel后缀)
页面切换流程总结
用户点击导航按钮
↓
触发NavgiateCommand,传递参数"DashBoard"
↓
Navgiate方法执行
↓
从DI容器获取DashBoardViewModel
↓
赋值给MainContent属性
↓
[ObservableProperty]自动触发PropertyChanged事件
↓
ContentControl接收到通知,内容更新
↓
WPF自动查找并显示DashBoardView
↓
DashBoardView的DataContext自动设置为DashBoardViewModel
↓
页面切换完成!
2. 数据绑定实现
绑定语法
基本绑定
xml
<!-- 绑定到属性 -->
<TextBlock Text="{Binding CurrentTemp}"/>
带格式化的绑定
xml
<!-- 数值格式化:显示千分位 -->
<TextBlock Text="{Binding ActualCount, StringFormat='{}{0:N0}'}"/>
<!-- 日期格式化 -->
<TextBlock Text="{Binding CurrentTime, StringFormat='{}yyyy-MM-dd HH:mm:ss'}"/>
绑定到嵌套属性
xml
<!-- 绑定到集合中的属性 -->
<TextBlock Text="{Binding Path=AlarmRecord.Title}"/>
实战示例
示例1:简单数据绑定
xml
<TextBlock Text="{Binding CurrentTemp}"
Foreground="{StaticResource AppAccentColor}"
FontSize="32"
FontWeight="Bold"/>
csharp
[ObservableProperty]
private double currentTemp; // 自动生成CurrentTemp属性
private void OnDataReceived(DeviceState e)
{
CurrentTemp = e.CurrentTemp; // 赋值自动触发UI更新
}
示例2:集合数据绑定
xml
<!-- 绑定到集合 -->
<ItemsControl ItemsSource="{Binding recentAlarms}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#333" BorderThickness="0,0,0,1" Padding="5">
<TextBlock FontSize="12">
<Run Text="[" Foreground="White"/>
<Run Text="{Binding TimeStr}" Foreground="White"/>
<Run Text="]" Foreground="White"/>
<Run Text="{Binding Title}" Foreground="Red"/>
</TextBlock>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
csharp
// 集合属性:使用ObservableCollection才能自动更新UI
public ObservableCollection<AlarmUiModel> recentAlarms { get; }
= new ObservableCollection<AlarmUiModel>();
// 添加数据时UI自动更新
private void AlarmService_AlarmTriggered(object? sender, AlarmRecord e)
{
Application.Current.Dispatcher.Invoke(() =>
{
recentAlarms.Insert(0, AlarmUiModel.FromRecord(e)); // 插入到开头
if (recentAlarms.Count > 10)
{
recentAlarms.RemoveAt(recentAlarms.Count - 1); // 保持最多10条
}
});
}
示例3:双向绑定
SettingView.xaml (假设)
xml
<!-- TextBox支持双向绑定 -->
<TextBox Text="{Binding DeviceIp, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/>
<!-- CheckBox绑定 -->
<CheckBox IsChecked="{Binding AutoConnect, Mode=TwoWay}"/>
<!-- Slider绑定 -->
<Slider Value="{Binding Temperature, Mode=TwoWay}"
Minimum="0" Maximum="150"/>
数据绑定模式
| 模式 | 说明 | 使用场景 |
|---|---|---|
OneWay |
ViewModel→View | 只显示数据,不需要修改 |
TwoWay |
ViewModel↔View | 表单输入、用户设置 |
OneWayToSource |
View→ViewModel | 只需要接收用户输入 |
OneTime |
只绑定一次 | 数据不会变化的静态内容 |
CommunityToolkit.Mvvm 简化绑定
传统方式(不推荐):
csharp
private double _currentTemp;
public double CurrentTemp
{
get { return _currentTemp; }
set
{
_currentTemp = value;
OnPropertyChanged(nameof(CurrentTemp));
}
}
使用CommunityToolkit.Mvvm(推荐):
csharp
[ObservableProperty]
private double currentTemp; // 自动生成属性和通知代码!
生成的代码(编译时自动生成):
csharp
public double CurrentTemp
{
get => currentTemp;
set
{
if (EqualityComparer<double>.Default.Equals(currentTemp, value)) return;
currentTemp = value;
OnPropertyChanged(nameof(CurrentTemp));
}
}
3. 事件绑定实现
传统方式 vs MVVM方式
❌ 传统方式(代码后置)
xml
<!-- XAML -->
<Button Click="Button_Click" Content="点击"/>
csharp
// MainWindow.xaml.cs - 代码后置
private void Button_Click(object sender, RoutedEventArgs e)
{
// 逻辑代码
}
问题:
- 违反MVVM原则
- 逻辑在View层,难以单元测试
- 无法复用
✅ MVVM方式(命令绑定)
xml
<!-- XAML -->
<Button Command="{Binding StartCommand}" Content="启动"/>
csharp
// ViewModel
[RelayCommand]
private void Start()
{
// 逻辑代码
}
命令绑定实现
示例1:基本命令绑定
xml
<StackPanel Orientation="Horizontal">
<Button Content="☑︎ 启 动"
Command="{Binding StartProductionCommand}"
Width="120"
Height="30"/>
<Button Content="⏹︎ 停 止"
Command="{Binding StopProductionCommand}"
Width="120"
Height="30"/>
<Button Content="♻︎ 复 位"
Command="{Binding ResetProductionCommand}"
Width="120"
Height="30"/>
</StackPanel>
csharp
[RelayCommand]
private async Task StartProductionAsync()
{
try
{
DeviceStatus = "启动中......";
await PlcService.WriteCommandAsync("Start", true);
await Task.Delay(2000);
DeviceStatus = "运行中......";
LogService.Info("发送启动命令!");
}
catch (Exception ex)
{
DeviceStatus = "启动失败......";
LogService.Error("发送启动命令失败!", ex);
}
}
关键点:
[RelayCommand]自动生成StartProductionCommand属性- 支持异步方法(async Task)
- 自动处理异常
示例2:带参数的命令绑定
xml
<!-- CommandParameter传递当前报警对象 -->
<Button Content="确认/复位"
Command="{Binding DataContext.AcknowledgeAlarmCommand,
RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Width="90"/>
csharp
[RelayCommand]
private async Task AcknowledgeAlarmAsync(AlarmUiModel alarm)
{
if (alarm == null) return;
try
{
var success = await AlarmService.AcknowledgeAlarmAsync(alarm.Id, "");
if (success)
{
Application.Current.Dispatcher.Invoke(() =>
{
ActiveAlarms.Remove(alarm);
ActiveAlarmCount = ActiveAlarms.Count();
});
LogService.Info($"确认报警成功{alarm.Code}");
}
}
catch (Exception ex)
{
LogService.Error($"确认报警异常{alarm.Code}", ex);
}
}
关键点:
CommandParameter="{Binding}"传递当前数据项- 命令方法接收参数:
AcknowledgeAlarmAsync(AlarmUiModel alarm)
示例3:在DataTemplate中绑定命令
xml
<ItemsControl ItemsSource="{Binding ActiveAlarms}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.AcknowledgeAlarmCommand,
RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Content="确认"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
关键点:
- 在DataTemplate中,DataContext是数据项(如AlarmUiModel)
- 使用
RelativeSource找到父级UserControl的DataContext - 然后访问其中的命令
命令的CanExecute(控制按钮是否可用)
csharp
[RelayCommand]
private bool CanStart()
{
// 返回false时按钮自动禁用
return !IsRunning;
}
[RelayCommand]
private void Start()
{
IsRunning = true;
// StartProductionCommand自动重新评估CanExecute
}
CommunityToolkit.Mvvm 命令特性
| 特性 | 说明 |
|---|---|
[RelayCommand] |
自动生成Command属性 |
| 支持异步 | async Task 方法 |
| 支持参数 | 命令方法可以接收参数 |
| CanExecute | 自动管理按钮启用/禁用状态 |
| 自动通知 | 修改属性后命令自动刷新状态 |
4. 依赖注入(DI)
什么是依赖注入?
没有DI的问题:
csharp
public class DashBoardViewModel
{
private readonly AlarmService _alarmService;
public DashBoardViewModel()
{
// 硬编码依赖,难以测试和替换
_alarmService = new AlarmService();
}
}
使用DI的好处:
csharp
public class DashBoardViewModel
{
private readonly AlarmService _alarmService;
// 通过构造函数注入,解耦
public DashBoardViewModel(AlarmService alarmService)
{
_alarmService = alarmService;
}
}
配置依赖注入
App.xaml.cs
csharp
public partial class App : Application
{
public IServiceProvider ServiceProvider { get; private set; }
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 1. 创建服务集合
var services = new ServiceCollection();
// 2. 注册服务
ConfigureServices(services);
// 3. 构建服务提供者
ServiceProvider = services.BuildServiceProvider();
// 4. 使用服务
var mainVM = ServiceProvider.GetRequiredService<MainWindowViewModel>();
var mainWindow = new MainWindow { DataContext = mainVM };
mainWindow.Show();
}
private void ConfigureServices(IServiceCollection services)
{
// 注册所有ViewModel为单例
services.AddSingleton<AlarmsViewModel>();
services.AddSingleton<DashBoardViewModel>();
services.AddSingleton<DashQueryViewModel>();
services.AddSingleton<LogsViewModel>();
services.AddSingleton<SettingViewModel>();
services.AddSingleton<MainWindowViewModel>();
}
}
服务生命周期
| 生命周期 | 说明 | 使用场景 |
|---|---|---|
AddSingleton |
整个应用程序生命周期只创建一次 | ViewModel、全局服务 |
AddTransient |
每次请求都创建新实例 | 简单的工具类 |
AddScoped |
在同一作用域内共享(WPF很少用) | - |
在ViewModel中使用DI
csharp
public partial class DashBoardViewModel : ObservableObject
{
private readonly PlcService _plcService;
private readonly AlarmService _alarmService;
// 构造函数注入
public DashBoardViewModel(PlcService plcService, AlarmService alarmService)
{
_plcService = plcService;
_alarmService = alarmService;
// 订阅事件
_plcService.DataReceived += OnDataReceived;
_alarmService.AlarmTriggered += OnAlarmTriggered;
}
}
5. 值转换器(Converters)
什么是转换器?
转换器用于在绑定源 和目标之间转换数据:
数据源 ──[转换器]──> UI显示
bool → Visibility
enum → Brush/Color
int → string (格式化)
内置转换器
BooleanToVisibilityConverter
xml
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</UserControl.Resources>
<!-- true: Visible, false: Collapsed -->
<TextBlock Text="系统正常"
Visibility="{Binding IsHealthy,
Converter={StaticResource BooleanToVisibilityConverter}}"/>
自定义转换器
示例1:LightStateToBrushConverter
<Converters/LightStateToBrushConverter.cs>
csharp
public class LightStateToBrushConverter : IValueConverter
{
private static readonly Color OffColor = Colors.DimGray;
private static readonly Color GreenColor = Colors.Green;
private static readonly Color YellowColor = Colors.Yellow;
private static readonly Color RedColor = Colors.Red;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// 1. 获取输入值(枚举状态)
var state = value is LightState state ? state : LightState.Off;
// 2. 获取参数(指定哪种颜色)
var role = (parameter as string)?.ToLowerInvariant() ?? string.Empty;
// 3. 根据状态和参数返回对应颜色
if (state == LightState.Off) return OffColor;
return role switch
{
"green" => state == LightState.Green ? GreenColor : OffColor,
"yellow" => state == LightState.Yellow ? YellowColor : OffColor,
"red" => state == LightState.Red ? RedColor : OffColor,
_ => OffColor
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
使用示例:
xml
<UserControl.Resources>
<converters:LightStateToBrushConverter x:Key="LightStateToBrushConverter"/>
</UserControl.Resources>
<Ellipse Fill="{Binding Light1State,
Converter={StaticResource LightStateToBrushConverter},
ConverterParameter=green}"/>
示例2:ZeroToVisibilityConverter
<Converters/ZeroToVisibilityConverter.cs>
csharp
public class ZeroToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// 如果值为0,返回Visible,否则返回Collapsed
if (value is int intValue && intValue == 0)
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
使用示例:
xml
<StackPanel Visibility="{Binding ActiveAlarmCount,
Converter={StaticResource ZeroToVisibilityConverter}}">
<TextBlock Text="✔" FontSize="50" Foreground="Green"/>
<TextBlock Text="系统正常 无报警" FontSize="20"/>
</StackPanel>
多值转换器(IMultiValueConverter)
当需要绑定多个值时使用:
csharp
public class TemperatureWarningConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 &&
values[0] is double current &&
values[1] is double max)
{
return current > max ? Colors.Red : Colors.Green;
}
return Colors.Green;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
使用示例:
xml
<TextBlock Foreground="Green">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TemperatureWarningConverter}">
<Binding Path="CurrentTemp"/>
<Binding Path="MaxTemp"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
新手入门指南
从零开始创建WPF MVVM项目
步骤1:创建新项目
bash
# 使用Visual Studio或命令行
dotnet new wpf -n WpfMvvmApp
cd WpfMvvmApp
步骤2:安装NuGet包
bash
# MVVM工具包
dotnet add package CommunityToolkit.Mvvm
# 依赖注入
dotnet add package Microsoft.Extensions.DependencyInjection
# UI框架(可选)
dotnet add package HandyControl
步骤3:创建项目结构
bash
mkdir Models ViewModels Views Converters Services
步骤4:创建第一个Model
<Models/User.cs>
csharp
namespace WpfMvvmApp.Models
{
public class User
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
}
}
步骤5:创建第一个ViewModel
<ViewModels/MainViewModel.cs>
csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using WpfMvvmApp.Models;
namespace WpfMvvmApp.ViewModels
{
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string title = "WPF MVVM 应用";
[ObservableProperty]
private User currentUser = new User { Name = "张三", Age = 25 };
[RelayCommand]
private void ShowMessage()
{
MessageBox.Show($"你好,{CurrentUser.Name}!");
}
[RelayCommand]
private void IncrementAge()
{
CurrentUser.Age++;
}
}
}
步骤6:创建View
<Views/MainView.xaml>
xml
<UserControl x:Class="WpfMvvmApp.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Margin="20">
<StackPanel>
<TextBlock Text="{Binding Title}"
FontSize="24"
FontWeight="Bold"
Margin="0,0,0,20"/>
<TextBlock Text="姓名:" Margin="0,0,0,5"/>
<TextBox Text="{Binding CurrentUser.Name, Mode=TwoWay}"
Margin="0,0,0,15"/>
<TextBlock Text="年龄:" Margin="0,0,0,5"/>
<TextBlock Text="{Binding CurrentUser.Age}"
FontSize="18"
Margin="0,0,0,15"/>
<StackPanel Orientation="Horizontal">
<Button Content="显示消息"
Command="{Binding ShowMessageCommand}"
Width="100"
Height="30"
Margin="0,0,10,0"/>
<Button Content="增加年龄"
Command="{Binding IncrementAgeCommand}"
Width="100"
Height="30"/>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
步骤7:配置依赖注入
<App.xaml.cs>
csharp
using Microsoft.Extensions.DependencyInjection;
using System.Windows;
using WpfMvvmApp.ViewModels;
using WpfMvvmApp.Views;
namespace WpfMvvmApp
{
public partial class App : Application
{
public IServiceProvider ServiceProvider { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
ConfigureServices(services);
ServiceProvider = services.BuildServiceProvider();
var mainWindow = new MainWindow
{
DataContext = ServiceProvider.GetRequiredService<MainViewModel>()
};
mainWindow.Show();
}
private void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<MainViewModel>();
services.AddSingleton<MainView>();
}
}
}
步骤8:设置主窗口
<MainWindow.xaml>
xml
<Window x:Class="WpfMvvmApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfMvvmApp.Views"
Title="MainWindow" Height="450" Width="800">
<Grid>
<local:MainView/>
</Grid>
</Window>
步骤9:运行测试
bash
dotnet run
开发调试技巧
1. 输出绑定错误
在App.xaml.cs中启用WPF绑定错误跟踪:
csharp
public App()
{
// 启用WPF诊断
PresentationTraceSources.Refresh();
PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.All;
}
2. 使用断点调试
在ViewModel的属性setter中设置断点:
csharp
[ObservableProperty]
private string name; // 在这里设置断点查看赋值情况
3. 实时可视化树
使用Visual Studio的"实时可视化树"查看UI元素和DataContext。
4. 日志输出
使用Serilog记录ViewModel中的操作:
csharp
[RelayCommand]
private void SaveData()
{
Log.Information("保存数据:{Data}", CurrentUser);
// 保存逻辑
}
常见问题解答
Q1: 数据绑定不工作?
检查清单:
- ✅ DataContext是否设置?
- ✅ 属性名拼写是否正确?
- ✅ 是否使用
[ObservableProperty]? - ✅ 是否在UI线程更新数据?
解决方案:
csharp
// 确保在UI线程更新
Application.Current.Dispatcher.Invoke(() =>
{
CurrentTemp = newValue;
});
Q2: 命令不触发?
检查清单:
- ✅ 命令名是否正确(加上Command后缀)?
- ✅ DataContext是否正确?
- ✅ 是否使用
[RelayCommand]?
常见错误:
xml
<!-- ❌ 错误:忘记Command后缀 -->
<Button Command="{Binding Start}"/>
<!-- ✅ 正确 -->
<Button Command="{Binding StartCommand}"/>
Q3: RelativeSource怎么用?
xml
<!-- 绑定到父级Window的DataContext -->
<Button Command="{Binding DataContext.SaveCommand,
RelativeSource={RelativeSource AncestorType=Window}}"/>
<!-- 绑定到自身 -->
<TextBlock Text="{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth}"/>
<!-- 绑定到父级UserControl -->
<Button Command="{Binding DataContext.CloseCommand,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
Q4: ObservableCollection vs List?
csharp
// ❌ 错误:List不会通知UI更新
public List<User> Users { get; set; } = new List<User>();
// ✅ 正确:ObservableCollection会通知UI更新
public ObservableCollection<User> Users { get; set; }
= new ObservableCollection<User>();
Q5: 如何处理异步操作?
csharp
[RelayCommand]
private async Task LoadDataAsync()
{
try
{
IsLoading = true; // 显示加载动画
var data = await _service.GetDataAsync();
Items.Clear();
foreach (var item in data)
{
Items.Add(item);
}
}
finally
{
IsLoading = false; // 隐藏加载动画
}
}
Q6: 如何验证用户输入?
csharp
public partial class SettingsViewModel : ObservableObject
{
[ObservableProperty]
[NotifyDataErrorInfo]
private string _email = string.Empty;
public IEnumerable<ValidationError> ValidateEmail(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
yield return new ValidationError("邮箱不能为空");
}
else if (!value.Contains("@"))
{
yield return new ValidationError("邮箱格式不正确");
}
}
}
xml
<TextBox Text="{Binding Email, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="{Binding Path=(Validation.Errors)[0].ErrorContent,
ElementName=EmailTextBox}"
Foreground="Red"/>
总结
核心要点
- MVVM模式:分离UI和逻辑,提高可维护性
- 数据绑定 :使用
[ObservableProperty]简化属性定义 - 命令绑定 :使用
[RelayCommand]处理用户交互 - 页面切换:通过ContentControl + ViewModel切换实现
- 依赖注入:使用Microsoft.Extensions.DependencyInjection管理依赖
- 值转换器:实现数据类型转换
学习路径
1. 创建第一个MVVM项目
↓
2. 实现基本数据绑定
↓
3. 添加命令处理
↓
4. 实现多页面切换
↓
5. 配置依赖注入
↓
6. 添加转换器和验证
↓
7. 集成数据库和网络通信