Files
XplorePlane/XP.Hardware.PLC/ViewModels/PlcAddrConfigEditorViewModel.cs

678 lines
27 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Windows;
using Prism.Commands;
using Prism.Mvvm;
using XP.Common.GeneralForm.Views;
using XP.Common.Localization;
using XP.Common.Logging.Interfaces;
using XP.Hardware.PLC.Configs;
using XP.Hardware.PLC.Helpers;
using XP.Hardware.PLC.Models;
namespace XP.Hardware.PLC.ViewModels
{
/// <summary>
/// PLC 信号地址定义编辑器 ViewModel | PLC signal address config editor ViewModel
/// 提供信号表加载/编辑/保存和 PLC 连接参数配置功能
/// </summary>
public class PlcAddrConfigEditorViewModel : BindableBase
{
// === 依赖 | Dependencies ===
private readonly XmlSignalParser _xmlParser;
private readonly ConfigLoader _configLoader;
private readonly ILoggerService _logger;
// === 信号分组数据 | Signal group data ===
/// <summary>
/// 信号分组集合,每个 Group 对应一个 Tab 页 | Signal group collection, each Group maps to a Tab
/// </summary>
public ObservableCollection<SignalGroup> Groups { get; }
private SignalGroup _selectedGroup;
/// <summary>
/// 当前选中的 GroupTab 页)| Currently selected Group (Tab)
/// </summary>
public SignalGroup SelectedGroup
{
get => _selectedGroup;
set => SetProperty(ref _selectedGroup, value);
}
// === PLC 连接参数 | PLC connection parameters ===
private string _ipAddress;
/// <summary>
/// PLC IP 地址 | PLC IP address
/// </summary>
public string IpAddress
{
get => _ipAddress;
set { if (SetProperty(ref _ipAddress, value)) HasUnsavedChanges = true; }
}
private int _port;
/// <summary>
/// PLC 端口号 | PLC port number
/// </summary>
public int Port
{
get => _port;
set { if (SetProperty(ref _port, value)) HasUnsavedChanges = true; }
}
private short _rack;
/// <summary>
/// 机架号 | Rack number
/// </summary>
public short Rack
{
get => _rack;
set { if (SetProperty(ref _rack, value)) HasUnsavedChanges = true; }
}
private short _slot;
/// <summary>
/// 插槽号 | Slot number
/// </summary>
public short Slot
{
get => _slot;
set { if (SetProperty(ref _slot, value)) HasUnsavedChanges = true; }
}
private PlcType _selectedPlcType;
/// <summary>
/// 选中的 PLC 型号 | Selected PLC type
/// </summary>
public PlcType SelectedPlcType
{
get => _selectedPlcType;
set { if (SetProperty(ref _selectedPlcType, value)) HasUnsavedChanges = true; }
}
/// <summary>
/// PLC 类型枚举值列表,供下拉框绑定 | PLC type enum values for ComboBox binding
/// </summary>
public PlcType[] PlcTypes { get; } = Enum.GetValues(typeof(PlcType)).Cast<PlcType>().ToArray();
private string _readDbBlock;
/// <summary>
/// 读取数据块 | Read data block
/// </summary>
public string ReadDbBlock
{
get => _readDbBlock;
set { if (SetProperty(ref _readDbBlock, value)) HasUnsavedChanges = true; }
}
private int _readStartAddress;
/// <summary>
/// 读取起始地址 | Read start address
/// </summary>
public int ReadStartAddress
{
get => _readStartAddress;
set { if (SetProperty(ref _readStartAddress, value)) HasUnsavedChanges = true; }
}
private int _readLength;
/// <summary>
/// 读取长度 | Read length
/// </summary>
public int ReadLength
{
get => _readLength;
set { if (SetProperty(ref _readLength, value)) HasUnsavedChanges = true; }
}
private int _bulkReadIntervalMs = 100;
/// <summary>
/// 批量读取周期(毫秒)| Bulk read interval (ms)
/// </summary>
public int BulkReadIntervalMs
{
get => _bulkReadIntervalMs;
set { if (SetProperty(ref _bulkReadIntervalMs, value)) HasUnsavedChanges = true; }
}
private int _connectTimeoutMs;
/// <summary>
/// 连接超时(毫秒)| Connect timeout (ms)
/// </summary>
public int ConnectTimeoutMs
{
get => _connectTimeoutMs;
set { if (SetProperty(ref _connectTimeoutMs, value)) HasUnsavedChanges = true; }
}
private int _readTimeoutMs;
/// <summary>
/// 读取超时(毫秒)| Read timeout (ms)
/// </summary>
public int ReadTimeoutMs
{
get => _readTimeoutMs;
set { if (SetProperty(ref _readTimeoutMs, value)) HasUnsavedChanges = true; }
}
private int _writeTimeoutMs;
/// <summary>
/// 写入超时(毫秒)| Write timeout (ms)
/// </summary>
public int WriteTimeoutMs
{
get => _writeTimeoutMs;
set { if (SetProperty(ref _writeTimeoutMs, value)) HasUnsavedChanges = true; }
}
private bool _bReConnect;
/// <summary>
/// 断线重连 | Auto reconnect on disconnect
/// </summary>
public bool BReConnect
{
get => _bReConnect;
set { if (SetProperty(ref _bReConnect, value)) HasUnsavedChanges = true; }
}
// === 文件路径 | File path ===
private string _xmlFilePath;
/// <summary>
/// XML 配置文件路径 | XML config file path
/// </summary>
public string XmlFilePath
{
get => _xmlFilePath;
set => SetProperty(ref _xmlFilePath, value);
}
// === 状态标志 | Status flags ===
private bool _hasUnsavedChanges;
/// <summary>
/// 是否有未保存的修改 | Whether there are unsaved changes
/// </summary>
public bool HasUnsavedChanges
{
get => _hasUnsavedChanges;
set => SetProperty(ref _hasUnsavedChanges, value);
}
/// <summary>
/// 窗口关闭回调 | Window close action callback
/// </summary>
public Action CloseAction { get; set; }
/// <summary>
/// 支持的数据类型列表,供下拉框绑定 | Supported data types for ComboBox binding
/// </summary>
public string[] SupportedDataTypes { get; } = SignalEntry.SupportedTypes;
// === 命令 | Commands ===
/// <summary>保存命令 | Save command</summary>
public DelegateCommand SaveCommand { get; }
/// <summary>取消命令 | Cancel command</summary>
public DelegateCommand CancelCommand { get; }
/// <summary>重新加载命令 | Reload command</summary>
public DelegateCommand ReloadCommand { get; }
/// <summary>添加信号命令(操作当前选中 Group| Add signal command (operates on selected Group)</summary>
public DelegateCommand AddSignalCommand { get; }
/// <summary>删除信号命令(操作当前选中 Group| Delete signal command (operates on selected Group)</summary>
public DelegateCommand DeleteSignalCommand { get; }
private SignalEntry _selectedSignal;
/// <summary>
/// 当前选中的信号 | Currently selected signal
/// </summary>
public SignalEntry SelectedSignal
{
get => _selectedSignal;
set => SetProperty(ref _selectedSignal, value);
}
/// <summary>新增 Group 命令 | Add Group command</summary>
public DelegateCommand AddGroupCommand { get; }
/// <summary>删除 Group 命令 | Delete Group command</summary>
public DelegateCommand DeleteGroupCommand { get; }
/// <summary>重命名 Group 命令 | Rename Group command</summary>
public DelegateCommand RenameGroupCommand { get; }
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="xmlParser">XML 信号解析器 | XML signal parser</param>
/// <param name="configLoader">配置加载器 | Config loader</param>
/// <param name="logger">日志服务 | Logger service</param>
public PlcAddrConfigEditorViewModel(
XmlSignalParser xmlParser,
ConfigLoader configLoader,
ILoggerService logger)
{
_xmlParser = xmlParser ?? throw new ArgumentNullException(nameof(xmlParser));
_configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader));
_logger = logger?.ForModule<PlcAddrConfigEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger));
// 初始化信号分组集合 | Initialize signal group collection
Groups = new ObservableCollection<SignalGroup>();
// 加载 PLC 连接参数 | Load PLC connection parameters
LoadPlcConfig();
// 读取 XML 文件路径,未配置时使用默认路径 | Read XML file path, fallback to default
var configuredPath = ConfigurationManager.AppSettings["PlcAddrDfnXmlPath"];
XmlFilePath = !string.IsNullOrWhiteSpace(configuredPath)
? configuredPath
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PlcAddrDfn.xml");
// 初始化命令 | Initialize commands
SaveCommand = new DelegateCommand(ExecuteSave);
CancelCommand = new DelegateCommand(ExecuteCancel);
ReloadCommand = new DelegateCommand(ExecuteReload);
// 添加信号到当前选中 Group | Add signal to currently selected Group
AddSignalCommand = new DelegateCommand(() =>
{
if (SelectedGroup == null)
{
MessageBox.Show(
LocalizationHelper.Get("PlcAddrEditor_NoGroupSelected"),
LocalizationHelper.Get("PlcAddrEditor_Title"),
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var signal = new SignalEntry { OwnerCollection = SelectedGroup.Signals };
SelectedGroup.Signals.Add(signal);
HasUnsavedChanges = true;
});
// 从当前选中 Group 删除信号 | Delete signal from currently selected Group
DeleteSignalCommand = new DelegateCommand(() =>
{
if (SelectedSignal == null) return;
if (MessageBox.Show(
LocalizationHelper.Get("PlcAddrEditor_DeleteConfirm"),
LocalizationHelper.Get("PlcAddrEditor_Title"),
MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
SelectedSignal.OwnerCollection?.Remove(SelectedSignal);
SelectedSignal = null;
HasUnsavedChanges = true;
}
});
// 新增 Group | Add new Group
AddGroupCommand = new DelegateCommand(ExecuteAddGroup);
// 删除当前选中 Group | Delete currently selected Group
DeleteGroupCommand = new DelegateCommand(ExecuteDeleteGroup);
// 重命名当前选中 Group | Rename currently selected Group
RenameGroupCommand = new DelegateCommand(ExecuteRenameGroup);
// 加载信号数据 | Load signal data
LoadSignals();
_logger.Info("PLC 信号地址定义编辑器 ViewModel 初始化完成 | PlcAddrConfigEditorViewModel initialized");
}
/// <summary>
/// 从 ConfigLoader 加载 PLC 连接参数并填充绑定属性 | Load PLC config and populate binding properties
/// </summary>
private void LoadPlcConfig()
{
try
{
var config = _configLoader.LoadPlcConfig();
_ipAddress = config.IpAddress;
_port = config.Port;
_rack = config.Rack;
_slot = config.Slot;
_selectedPlcType = config.PlcType;
_readDbBlock = config.ReadDbBlock;
_readStartAddress = config.ReadStartAddress;
_readLength = config.ReadLength;
_bulkReadIntervalMs = config.BulkReadIntervalMs;
_connectTimeoutMs = config.ConnectTimeoutMs;
_readTimeoutMs = config.ReadTimeoutMs;
_writeTimeoutMs = config.WriteTimeoutMs;
_bReConnect = config.bReConnect;
}
catch (Exception ex)
{
_logger.Warn($"加载 PLC 配置失败,使用默认值 | Failed to load PLC config, using defaults: {ex.Message}");
}
}
/// <summary>
/// 加载信号分组数据 | Load signal group data
/// 调用 XmlSignalParser.LoadFromFile 获取 List&lt;SignalGroup&gt;,填充 Groups 集合
/// </summary>
private void LoadSignals()
{
try
{
var groups = _xmlParser.LoadFromFile(XmlFilePath);
Groups.Clear();
foreach (var group in groups)
{
// 为每个 Signal 设置 OwnerCollection | Set OwnerCollection for each Signal
foreach (var signal in group.Signals)
{
signal.OwnerCollection = group.Signals;
}
Groups.Add(group);
}
// 默认选中第一个 Group | Select first Group by default
SelectedGroup = Groups.FirstOrDefault();
HasUnsavedChanges = false;
var totalSignals = groups.Sum(g => g.Signals.Count);
_logger.Info($"成功加载信号数据:共 {totalSignals} 条信号,来自 {groups.Count} 个 Group | Loaded {totalSignals} signals from {groups.Count} groups");
}
catch (Exception ex)
{
_logger.Error(ex, $"加载信号数据失败 | Failed to load signals: {ex.Message}");
Groups.Clear();
}
}
/// <summary>
/// 执行新增 Group 操作 | Execute add Group operation
/// 弹出输入框获取 Group ID 和 DBNumber,检查 ID 唯一性
/// </summary>
private void ExecuteAddGroup()
{
// 弹出输入框获取 Group ID,内置唯一性验证 | Prompt for Group ID with uniqueness validation
var groupId = InputDialog.Show(
LocalizationHelper.Get("PlcAddrEditor_InputGroupId"),
LocalizationHelper.Get("PlcAddrEditor_AddGroup"),
validate: input =>
{
if (string.IsNullOrWhiteSpace(input))
return LocalizationHelper.Get("Message_InvalidInput");
if (Groups.Any(g => string.Equals(g.GroupId, input, StringComparison.Ordinal)))
return LocalizationHelper.Get("PlcAddrEditor_GroupIdDuplicate");
return null;
});
if (groupId == null)
return;
// 弹出输入框获取 DBNumber,内置非负整数验证 | Prompt for DBNumber with non-negative integer validation
var dbNumberStr = InputDialog.Show(
LocalizationHelper.Get("PlcAddrEditor_InputDbNumber"),
LocalizationHelper.Get("PlcAddrEditor_AddGroup"),
"0",
validate: input =>
{
if (!int.TryParse(input, out int val) || val < 0)
return LocalizationHelper.Get("PlcAddrEditor_InvalidDbNumber");
return null;
});
if (dbNumberStr == null)
return;
var dbNumber = int.Parse(dbNumberStr);
// 创建新 Group 并添加到集合 | Create new Group and add to collection
var newGroup = new SignalGroup
{
GroupId = groupId,
DBNumber = dbNumber,
Signals = new ObservableCollection<SignalEntry>()
};
Groups.Add(newGroup);
SelectedGroup = newGroup;
HasUnsavedChanges = true;
_logger.Info($"新增 Group: ID={groupId}, DBNumber={dbNumber} | Added Group: ID={groupId}, DBNumber={dbNumber}");
}
/// <summary>
/// 执行重命名 Group 操作 | Execute rename Group operation
/// 弹出输入框获取新名称,检查唯一性
/// </summary>
private void ExecuteRenameGroup()
{
if (SelectedGroup == null)
{
MessageBox.Show(
LocalizationHelper.Get("PlcAddrEditor_NoGroupSelected"),
LocalizationHelper.Get("PlcAddrEditor_Title"),
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var oldId = SelectedGroup.GroupId;
var newId = InputDialog.Show(
LocalizationHelper.Get("PlcAddrEditor_InputNewGroupId"),
LocalizationHelper.Get("PlcAddrEditor_RenameGroup"),
oldId,
validate: input =>
{
if (string.IsNullOrWhiteSpace(input))
return LocalizationHelper.Get("Message_InvalidInput");
if (input == oldId)
return null; // 没改名,允许通过 | Same name, allow
if (Groups.Any(g => string.Equals(g.GroupId, input, StringComparison.Ordinal)))
return LocalizationHelper.Get("PlcAddrEditor_GroupIdDuplicate");
return null;
});
if (newId == null || newId == oldId)
return;
SelectedGroup.GroupId = newId;
HasUnsavedChanges = true;
_logger.Info($"重命名 Group: {oldId} → {newId} | Renamed Group: {oldId} → {newId}");
}
/// <summary>
/// 执行删除 Group 操作 | Execute delete Group operation
/// 删除前确认提示
/// </summary>
private void ExecuteDeleteGroup()
{
if (SelectedGroup == null)
{
MessageBox.Show(
LocalizationHelper.Get("PlcAddrEditor_NoGroupSelected"),
LocalizationHelper.Get("PlcAddrEditor_Title"),
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show(
string.Format(LocalizationHelper.Get("PlcAddrEditor_DeleteGroupConfirm"), SelectedGroup.GroupId),
LocalizationHelper.Get("PlcAddrEditor_Title"),
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
var groupId = SelectedGroup.GroupId;
Groups.Remove(SelectedGroup);
SelectedGroup = Groups.FirstOrDefault();
HasUnsavedChanges = true;
_logger.Info($"删除 Group: ID={groupId} | Deleted Group: ID={groupId}");
}
/// <summary>
/// 执行保存操作 | Execute save operation
/// 验证所有 Group 的 ID 唯一性、DBNumber 有效性、信号名称全局唯一性,然后保存
/// </summary>
private void ExecuteSave()
{
try
{
var errors = new List<string>();
// 1. 验证所有 Group 的 ID 唯一性 | Validate Group ID uniqueness
var duplicateGroupIds = Groups
.GroupBy(g => g.GroupId)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
foreach (var dupId in duplicateGroupIds)
{
errors.Add($"Group ID 重复 | Duplicate Group ID: {dupId}");
}
// 2. 验证所有 Group 的 DBNumber 有效性(非负整数)| Validate DBNumber validity (non-negative integer)
foreach (var group in Groups)
{
if (group.DBNumber < 0)
{
errors.Add($"Group [{group.GroupId}] 的 DBNumber 无效(必须为非负整数)| Group [{group.GroupId}] has invalid DBNumber (must be non-negative)");
}
}
// 3. 验证每个 Group 内信号名称唯一性 | Validate signal name uniqueness within each Group
foreach (var group in Groups)
{
var intraGroupDuplicates = group.Signals
.Where(s => !string.IsNullOrWhiteSpace(s.Name))
.GroupBy(s => s.Name)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
foreach (var dupName in intraGroupDuplicates)
{
errors.Add($"Group [{group.GroupId}] 内信号名称重复 | Duplicate signal name in Group [{group.GroupId}]: {dupName}");
}
}
// 4. 验证所有 Group 中信号名称全局唯一性(跨 Group 检查)| Validate global signal name uniqueness across Groups
var allSignals = Groups.SelectMany(g => g.Signals.Select(s => new { Signal = s, GroupId = g.GroupId })).ToList();
var crossGroupDuplicates = allSignals
.Where(x => !string.IsNullOrWhiteSpace(x.Signal.Name))
.GroupBy(x => x.Signal.Name)
.Where(g => g.Select(x => x.GroupId).Distinct().Count() > 1)
.ToList();
foreach (var dup in crossGroupDuplicates)
{
var groupIds = string.Join(", ", dup.Select(x => x.GroupId).Distinct());
errors.Add($"跨 Group 信号名称重复 | Cross-Group duplicate signal name: {dup.Key} (Groups: {groupIds})");
}
// 5. 检查所有信号条目的字段验证 | Check field validation for all signal entries
foreach (var group in Groups)
{
foreach (var signal in group.Signals.Where(s => !string.IsNullOrEmpty(s.ValidationError)))
{
errors.Add($"Group [{group.GroupId}] 信号 [{signal.Name}]: {signal.ValidationError}");
}
}
if (errors.Count > 0)
{
var errorMsg = LocalizationHelper.Get("PlcAddrEditor_ValidationFailed") + "\n" + string.Join("\n", errors);
MessageBox.Show(errorMsg, LocalizationHelper.Get("PlcAddrEditor_Title"), MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// 保存信号配置到 XML | Save signal configuration to XML
_xmlParser.SaveToFile(XmlFilePath, Groups.ToList());
// 保存 PlcConfig 参数到 App.config | Save PlcConfig parameters to App.config
SavePlcConfig();
HasUnsavedChanges = false;
_logger.Info("信号数据和 PLC 配置保存成功 | Signals and PLC config saved successfully");
MessageBox.Show(LocalizationHelper.Get("PlcAddrEditor_SaveSuccess"), LocalizationHelper.Get("PlcAddrEditor_Title"), MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger.Error(ex, $"保存失败 | Save failed: {ex.Message}");
MessageBox.Show(LocalizationHelper.Get("PlcAddrEditor_SaveFailed") + $"\n{ex.Message}", LocalizationHelper.Get("PlcAddrEditor_Title"), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 执行取消操作,关闭窗口 | Execute cancel, close window
/// </summary>
private void ExecuteCancel()
{
CloseAction?.Invoke();
}
/// <summary>
/// 执行重新加载操作 | Execute reload operation
/// 存在未保存修改时提示确认
/// </summary>
private void ExecuteReload()
{
var result = MessageBox.Show(
LocalizationHelper.Get("PlcAddrEditor_ReloadConfirm"),
LocalizationHelper.Get("PlcAddrEditor_Title"),
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
LoadSignals();
}
/// <summary>
/// 保存 PLC 连接参数到 App.config(已移除 WriteDbBlock| Save PLC connection parameters to App.config (WriteDbBlock removed)
/// </summary>
private void SavePlcConfig()
{
try
{
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var settings = config.AppSettings.Settings;
void SetOrAdd(string key, string value)
{
if (settings[key] == null)
settings.Add(key, value);
else
settings[key].Value = value;
}
SetOrAdd("Plc:IpAddress", IpAddress ?? string.Empty);
SetOrAdd("Plc:Port", Port.ToString());
SetOrAdd("Plc:Rack", Rack.ToString());
SetOrAdd("Plc:Slot", Slot.ToString());
SetOrAdd("Plc:PlcType", SelectedPlcType.ToString());
SetOrAdd("Plc:ReadDbBlock", ReadDbBlock ?? string.Empty);
SetOrAdd("Plc:ReadStartAddress", ReadStartAddress.ToString());
SetOrAdd("Plc:ReadLength", ReadLength.ToString());
SetOrAdd("Plc:BulkReadIntervalMs", BulkReadIntervalMs.ToString());
SetOrAdd("Plc:ConnectTimeoutMs", ConnectTimeoutMs.ToString());
SetOrAdd("Plc:ReadTimeoutMs", ReadTimeoutMs.ToString());
SetOrAdd("Plc:WriteTimeoutMs", WriteTimeoutMs.ToString());
SetOrAdd("Plc:bReConnect", BReConnect.ToString());
config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");
_logger.Info("PLC 配置参数已保存到 App.config | PLC config saved to App.config");
}
catch (Exception ex)
{
_logger.Warn($"保存 PLC 配置到 App.config 失败 | Failed to save PLC config to App.config: {ex.Message}");
}
}
}
}