WPF 四轴上机位开发笔记:限值参数、JSON 持久化、XAML 绑定与校验

📅 2026/6/29 18:37:46
WPF 四轴上机位开发笔记:限值参数、JSON 持久化、XAML 绑定与校验
WPF 四轴上机位开发笔记限值参数、JSON 持久化、XAML 绑定与校验基于 .NET 10 WPF / MVVM / NModbus4 的四轴运动控制项目一、今日目标为 4 个轴添加速度/加速度/减速度/力矩的上下限配置限值参数持久化到 JSON 文件重启后自动加载在写入 PLC 前进行限值校验确保参数不越界修复 XAML 绑定大小写不匹配导致的静默失败二、AxisParam 模型限值属性定义在Models\AxisParam.cs中新增 8 个限值属性全部为float非空类型区别于运动参数的float?// 参数的上限下限 privatefloat_velUpLimit;privatefloat_velLowerLimit;privatefloat_accelUpLimit;privatefloat_accelLowerLimit;privatefloat_decelUpLimit;privatefloat_decelLowerLimit;privatefloat_torqueUpLimit;privatefloat_torqueLowerLimit;// 公开属性XAML 绑定目标publicfloatVelUpLimit{get_velUpLimit;set{_velUpLimitvalue;OnPropertyChanged();}}publicfloatVelLowerLimit{get_velLowerLimit;set{_velLowerLimitvalue;OnPropertyChanged();}}publicfloatAccelUpLimit{get_accelUpLimit;set{_accelUpLimitvalue;OnPropertyChanged();}}publicfloatAccelLowerLimit{get_accelLowerLimit;set{_accelLowerLimitvalue;OnPropertyChanged();}}publicfloatDecelUpLimit{get_decelUpLimit;set{_decelUpLimitvalue;OnPropertyChanged();}}publicfloatDecelLowerLimit{get_decelLowerLimit;set{_decelLowerLimitvalue;OnPropertyChanged();}}publicfloatTorqueUpLimit{get_torqueUpLimit;set{_torqueUpLimitvalue;OnPropertyChanged();}}publicfloatTorqueLowerLimit{get_torqueLowerLimit;set{_torqueLowerLimitvalue;OnPropertyChanged();}}语法要点语法说明floatvsfloat?限值用float默认 0永远有值运动参数用float?用户可选填OnPropertyChanged()依赖[CallerMemberName]编译器自动填入属性名无需写字符串{ get; set; }完整写法因为需要后接OnPropertyChanged通知必须写完整属性体三、JSON 序列化与反序列化关键3.1 静态文件路径在MainViewModel类顶部定义统一的路径常量确保读写始终指向同一个文件privatestaticreadonlystringLimitConfigPathPath.Combine(AppDomain.CurrentDomain.BaseDirectory,LimitConfig.json);为什么必须用AppDomain.CurrentDomain.BaseDirectoryVS 调试时当前工作目录是项目根目录双击 exe 运行时是 exe 所在目录BaseDirectory永远返回 exe 所在目录bin\Debug\net10.0-windows\保证行为一致3.2 SaveLimitsToJson — 序列化使用匿名类型 System.Text.Json将 4 个轴的限值写入 JSONprivatevoidSaveLimitsToJson(){vardatanew{Axis1new{VelUpAxis1Data.Data.VelUpLimit,VelLowAxis1Data.Data.VelLowerLimit,AccelUpAxis1Data.Data.AccelUpLimit,AccelLowAxis1Data.Data.AccelLowerLimit,DecelUpAxis1Data.Data.DecelUpLimit,DecelLowAxis1Data.Data.DecelLowerLimit,TorqueUpAxis1Data.Data.TorqueUpLimit,TorqueLowAxis1Data.Data.TorqueLowerLimit,},// Axis2~Axis4 同上...};varjsonSystem.Text.Json.JsonSerializer.Serialize(data,newSystem.Text.Json.JsonSerializerOptions{WriteIndentedtrue});File.WriteAllText(LimitConfigPath,json);}生成的 JSON 格式{Axis1:{VelUp:1000,VelLow:100,AccelUp:500,AccelLow:50,DecelUp:500,DecelLow:50,TorqueUp:100,TorqueLow:10},...}语法要点匿名类型new { Axis1 new { ... } }— 不需要定义专门的 DTO 类File.WriteAllText— 覆盖写入不是追加。每次保存都是完整写出全部 4 轴数据WriteIndented true— 格式化 JSON方便人工查看3.3 LoadLimitFromJson — 反序列化由于保存时用了匿名类型反序列化时无法直接用泛型方法需要用JsonDocument手动解析privatevoidLoadLimitFromJson(){if(!File.Exists(LimitConfigPath))return;// 首次运行没有文件直接跳过varjsonFile.ReadAllText(LimitConfigPath);usingvardocSystem.Text.Json.JsonDocument.Parse(json);varrootdoc.RootElement;varaxesnew[]{Axis1Data,Axis2Data,Axis3Data,Axis4Data};string[]axisNames{Axis1,Axis2,Axis3,Axis4};for(inti0;i4;i){varelroot.GetProperty(axisNames[i]);axes[i].Data.VelUpLimitel.GetProperty(VelUp).GetSingle();axes[i].Data.VelLowerLimitel.GetProperty(VelLow).GetSingle();axes[i].Data.AccelUpLimitel.GetProperty(AccelUp).GetSingle();axes[i].Data.AccelLowerLimitel.GetProperty(AccelLow).GetSingle();axes[i].Data.DecelUpLimitel.GetProperty(DecelUp).GetSingle();axes[i].Data.DecelLowerLimitel.GetProperty(DecelLow).GetSingle();axes[i].Data.TorqueUpLimitel.GetProperty(TorqueUp).GetSingle();axes[i].Data.TorqueLowerLimitel.GetProperty(TorqueLow).GetSingle();}}语法要点语法说明JsonDocument.Parse(json)解析 JSON 字符串为可查询的文档对象using var doc确保JsonDocument使用完后释放非托管内存root.GetProperty(Axis1)获取 JSON 对象中的指定属性el.GetProperty(VelUp).GetSingle()获取属性值并转换为floatif (!File.Exists(...)) return;首次运行没有 JSON 文件时优雅退出3.4 加载时机在MainViewModel构造函数中调用一次保证程序启动时限值恢复到上次保存的值publicMainViewModel(){LoadLimitFromJson();// ← 启动时加载ConnectionCommandnewRelayCommand(Connect);// ... 其他命令绑定}在ExecuteLimitParam进入限值设置页面前再调一次保证看到最新数据privatevoidExecuteLimitParam(){LoadLimitFromJson();// ← 进页面前重新加载防外部修改varpagenewLimitAxesPage();page.DataContextthis;NavigateToPage?.Invoke(page);}四、XAML 绑定大坑大小写敏感问题描述XAML 中写的是TextBoxText{Binding Axis1Data.Data.accelUpLimit}/但 C# 属性定义是publicfloatAccelUpLimit{...}// 大写 AWPF 绑定是大小写敏感的绑定失败时没有报错不会编译失败只在输出窗口有警告。TextBox 的值永远是 0导致限值校验永远认为「下限为 0」而报错。修复XAML 所有绑定路径必须与 C# 属性名完全一致错误小写 a正确大写 AData.accelUpLimitData.AccelUpLimitData.accelLowerLimitData.AccelLowerLimit共 4 个轴 × 2 个属性 8 处。教训C# 属性命名建议统一大写开头PascalCaseXAML 绑定写完后可以用 Snoop / 输出窗口检查 Binding 是否成功或者先用FallbackValue测试绑定链是否连通五、限值校验逻辑5.1 ExecuteAxisLimit — 保存前的验证在 LimitAxesPage 点击 Confirm 按钮时调用privatevoidExecuteAxisLimit(intindex){if(_servicenull||!IsConnected)return;varaxesnew[]{Axis1Data,Axis2Data,Axis3Data,Axis4Data};vardataaxes[index].Data;varchecknew(floatup,floatlow,stringname)[]{(data.VelUpLimit,data.VelLowerLimit,VelLimit),(data.AccelUpLimit,data.AccelLowerLimit,AccelLimit),(data.DecelUpLimit,data.DecelLowerLimit,DecelLimit),(data.TorqueUpLimit,data.TorqueLowerLimit,TorqueLimit),};foreach(var(up,low,name)incheck){if(low0||uplow){System.Windows.MessageBox.Show($轴{index1}的{name}限值无效下限0上限下限);return;}}SaveLimitsToJson();System.Windows.MessageBox.Show($轴{index1}限值已保存);}语法要点(float up, float low, string name)[]— C# 7.0 值元组数组比定义类更轻量foreach (var (up, low, name) in check)— 元组解构直接取元组元素验证条件low 0 || up low— 下限必须 0上限必须 下限5.2 ValidateAxisParam — 写入前的参数值校验在写入 PLC 前Apply / Confirm验证运动参数是否超出限值publicboolValidateAxisParam(intindex,outstringmsg,stringactionMode){msg;varaxesnew[]{Axis1Data,Axis2Data,Axis3Data,Axis4Data};vardataaxes[index].Data;// 根据模式选择要检查的字段组(float?v,stringn)[]fields;if(actionModeRel)fieldsnew(float?v,stringn)[]{(data.RelPos,RelPos),(data.RelVel,RelVel),(data.RelAccel,RelAccel),(data.RelDecel,RelDecel)};elseif(actionModeAbso)fieldsnew(float?v,stringn)[]{(data.AbsoPos,AbsoPos),(data.AbsoVel,AbsoVel),(data.AbsoAccel,AbsoAccel),(data.AbsoDecel,AbsoDecel)};else{msg未知模式;returnfalse;}foreach(var(v,n)infields){// 第一步检查空值和零值if(vnull||v0f){msg$Axis{index1}的{n}无效(为空或0);returnfalse;}// 第二步根据字段名匹配对应的限值if(n.Contains(Vel)){if(vdata.VelLowerLimit||vdata.VelUpLimit){msg$Axis{index1}的{n}超出速度限值({data.VelLowerLimit}~{data.VelUpLimit});returnfalse;}}elseif(n.Contains(Accel)){if(vdata.AccelLowerLimit||vdata.AccelUpLimit){msg$Axis{index1}的{n}超出加速度限值({data.AccelLowerLimit}~{data.AccelUpLimit});returnfalse;}}elseif(n.Contains(Decel)){if(vdata.DecelLowerLimit||vdata.DecelUpLimit){msg$Axis{index1}的{n}超出减速度限值({data.DecelLowerLimit}~{data.DecelUpLimit});returnfalse;}}// RelPos / AbsoPos 没有对应的限值跳过}returntrue;}语法要点语法说明out string msg输出参数方法内部赋值调用方直接获取错误信息(float? v, string n)值元组同时携带值和名称方便错误消息拼接n.Contains(Vel)用字段名模糊匹配来确定对应限值同时覆盖 RelVel 和 AbsoVelv data.VelLowerLimit编译时float?与float可隐式比较但赋值给float时必须用.Value5.3 校验流程图用户输入值 → 点击按钮 ↓ ValidateAxisParam 检查 null / 0 ↓ (通过) 检查字段名是否包含 Vel/Accel/Decel ↓ 获取对应的 upLimit / lowerLimit ↓ v lowerLimit 或 v upLimit ? ├─ 是 → 弹窗报错不写入 └─ 否 → 写入 PLC六、今日踩坑总结坑 1XAML 绑定大小写WPF 绑定路径区分大小写accelUpLimit≠AccelUpLimit绑定失败不抛异常只在 VS 输出窗口有 BindingWarning解决方案写绑定前确认 C# 属性名或先用 FallbackValue 测试坑 2JSON 读写路径不一致保存用LimitConfigPath指向 exe 目录加载用LimitConfig.json指向工作目录VS F5 调试时工作目录 ≠ exe 目录导致保存和加载去了不同位置解决方案统一使用AppDomain.CurrentDomain.BaseDirectory拼接路径坑 3限值默认值为 0float类型默认值为 0如果用户没设置限值就去 Apply/Confirm验证v 0永远不成立建议限值校验只在限值 0 时才生效或引导用户先配置限值坑 4ValidateAxisParam验证限值的前提限值必须已经由用户设置并通过ExecuteAxisLimit保存LoadLimitFromJson必须在构造函数调用保证AccelUpLimit等不是 0如果限值文件不存在所有限值 0Vel/Accel/Decel 的非零值都会报超限七、相关文件路径文件说明Models\AxisParam.cs限值属性定义AccelUpLimit等 8 个ViewModels\MainViewModel.cs序列化/反序列化/校验逻辑View\LimitAxesPage.xaml限值编辑页面8 个 TextBox × 4 轴View\AxisParamSettingsPage.xaml绝对参数设置页面View\ManualAdjustPage.xaml手动参数设置页面Services\ModbusServiceBase.csModbus 读写服务Helpers\ModbusHelper.cs浮点数大端转换bin\Debug\net10.0-windows\LimitConfig.json限值持久化文件八、完整调用链路启动 App └→ MainViewModel 构造函数 ├→ LoadLimitFromJson() ← 从磁盘恢复限值 └→ 绑定所有 RelayCommand 用户点击 Limit Axes Param 按钮 └→ ExecuteLimitParam() ├→ LoadLimitFromJson() ← 刷新限值 └→ NavigateToPage(LimitAxesPage) ← 跳转编辑页 用户设置 VelUpLimit1000, VelLowerLimit100 ... 用户点击 Axis1 Confirm └→ ExecuteAxisLimit(0) ├→ 校验 low0 uplow ├→ SaveLimitsToJson() ← 序列化到文件 └→ MessageBox(已保存) 用户回到主页面点击 轴参数设置 → 输入 AbsoVel500 用户点击 Apply Settings └→ ExecuteSettingAbso() ├→ ValidateAxisParam(i, Abso) │ ├→ 检查 null/0 │ ├→ AbsoVel.Contains(Vel) → 检查 500 VelLowerLimit100 VelUpLimit1000 ✓ │ └→ 通过 └→ WriteMultipleRegisters() ← 写入 PLC九、性能与注意事项限值只用在上位机— 限值不会写入 PLC只用于上位机前端校验防止用户误操作JSON 文件很小— 4 轴 × 8 个 float ≈ 128 字节读写无性能问题using var doc— 尽早释放JsonDocument占用的内存每次 Load 都要创建新实例不要混淆 Rel/Abso 模式—ValidateAxisParam的actionMode参数决定校验哪些字段写入前停轮询— 所有导航方法都调_pollingTimer?.Stop()防止轮询覆盖用户输入的参数