WPF MVVM 模式(无调库)项目创建笔记

📚 目录


项目概述

这是一个基于 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:导航菜单触发切换命令

NavigationMenu.xaml

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:简单数据绑定

DashBoardView.xaml

xml 复制代码
<TextBlock Text="{Binding CurrentTemp}" 
           Foreground="{StaticResource AppAccentColor}" 
           FontSize="32" 
           FontWeight="Bold"/>

DashBoardViewModel.cs

csharp 复制代码
[ObservableProperty]
private double currentTemp;  // 自动生成CurrentTemp属性

private void OnDataReceived(DeviceState e)
{
    CurrentTemp = e.CurrentTemp;  // 赋值自动触发UI更新
}
示例2:集合数据绑定

DashBoardView.xaml

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>

DashBoardViewModel.cs

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:基本命令绑定

DashBoardView.xaml

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>

DashBoardViewModel.cs

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:带参数的命令绑定

AlarmsView.xaml

xml 复制代码
<!-- CommandParameter传递当前报警对象 -->
<Button Content="确认/复位" 
        Command="{Binding DataContext.AcknowledgeAlarmCommand, 
                  RelativeSource={RelativeSource AncestorType=UserControl}}"
        CommandParameter="{Binding}"  
        Width="90"/>

AlarmsViewModel.cs

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: 数据绑定不工作?

检查清单

  1. ✅ DataContext是否设置?
  2. ✅ 属性名拼写是否正确?
  3. ✅ 是否使用[ObservableProperty]
  4. ✅ 是否在UI线程更新数据?

解决方案

csharp 复制代码
// 确保在UI线程更新
Application.Current.Dispatcher.Invoke(() =>
{
    CurrentTemp = newValue;
});

Q2: 命令不触发?

检查清单

  1. ✅ 命令名是否正确(加上Command后缀)?
  2. ✅ DataContext是否正确?
  3. ✅ 是否使用[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"/>

总结

核心要点

  1. MVVM模式:分离UI和逻辑,提高可维护性
  2. 数据绑定 :使用[ObservableProperty]简化属性定义
  3. 命令绑定 :使用[RelayCommand]处理用户交互
  4. 页面切换:通过ContentControl + ViewModel切换实现
  5. 依赖注入:使用Microsoft.Extensions.DependencyInjection管理依赖
  6. 值转换器:实现数据类型转换

学习路径

复制代码
1. 创建第一个MVVM项目
   ↓
2. 实现基本数据绑定
   ↓
3. 添加命令处理
   ↓
4. 实现多页面切换
   ↓
5. 配置依赖注入
   ↓
6. 添加转换器和验证
   ↓
7. 集成数据库和网络通信

参考资源

相关推荐
LN花开富贵21 小时前
【ROS】鱼香ROS2学习笔记二
linux·笔记·python·学习·嵌入式
ouliten21 小时前
C++笔记:std::invoke
c++·笔记
风曦Kisaki1 天前
# LAMP 架构 + Discuz! 论坛实战笔记
笔记·架构
xuanwenchao1 天前
ROS2学习笔记 - 1、编写运行第一个程序
笔记·学习
独小乐1 天前
018.使用I2C总线EEPROM|千篇笔记实现嵌入式全栈/裸机篇
linux·笔记·单片机·嵌入式硬件·arm·信息与通信
YuanDaima20481 天前
二分查找基础原理与题目说明
开发语言·数据结构·人工智能·笔记·python·算法
卖报的大地主1 天前
TPAMI 2026 | 判别和扩散生成学习融合的礼物:边界细化遥感语义分割
人工智能·笔记·学习
Yeh2020581 天前
Http笔记
笔记
lkx097881 天前
统计学基础
笔记
oi..1 天前
SRC 实战复盘:SSRF 漏洞挖掘、自动化检测及流量插件优化(含Burp suite 25.1.2文件)
笔记·web安全·网络安全·自动化·系统安全·安全架构