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 { /// /// PLC 信号地址定义编辑器 ViewModel | PLC signal address config editor ViewModel /// 提供信号表加载/编辑/保存和 PLC 连接参数配置功能 /// public class PlcAddrConfigEditorViewModel : BindableBase { // === 依赖 | Dependencies === private readonly XmlSignalParser _xmlParser; private readonly ConfigLoader _configLoader; private readonly ILoggerService _logger; // === 信号分组数据 | Signal group data === /// /// 信号分组集合,每个 Group 对应一个 Tab 页 | Signal group collection, each Group maps to a Tab /// public ObservableCollection Groups { get; } private SignalGroup _selectedGroup; /// /// 当前选中的 Group(Tab 页)| Currently selected Group (Tab) /// public SignalGroup SelectedGroup { get => _selectedGroup; set => SetProperty(ref _selectedGroup, value); } // === PLC 连接参数 | PLC connection parameters === private string _ipAddress; /// /// PLC IP 地址 | PLC IP address /// public string IpAddress { get => _ipAddress; set { if (SetProperty(ref _ipAddress, value)) HasUnsavedChanges = true; } } private int _port; /// /// PLC 端口号 | PLC port number /// public int Port { get => _port; set { if (SetProperty(ref _port, value)) HasUnsavedChanges = true; } } private short _rack; /// /// 机架号 | Rack number /// public short Rack { get => _rack; set { if (SetProperty(ref _rack, value)) HasUnsavedChanges = true; } } private short _slot; /// /// 插槽号 | Slot number /// public short Slot { get => _slot; set { if (SetProperty(ref _slot, value)) HasUnsavedChanges = true; } } private PlcType _selectedPlcType; /// /// 选中的 PLC 型号 | Selected PLC type /// public PlcType SelectedPlcType { get => _selectedPlcType; set { if (SetProperty(ref _selectedPlcType, value)) HasUnsavedChanges = true; } } /// /// PLC 类型枚举值列表,供下拉框绑定 | PLC type enum values for ComboBox binding /// public PlcType[] PlcTypes { get; } = Enum.GetValues(typeof(PlcType)).Cast().ToArray(); private string _readDbBlock; /// /// 读取数据块 | Read data block /// public string ReadDbBlock { get => _readDbBlock; set { if (SetProperty(ref _readDbBlock, value)) HasUnsavedChanges = true; } } private int _readStartAddress; /// /// 读取起始地址 | Read start address /// public int ReadStartAddress { get => _readStartAddress; set { if (SetProperty(ref _readStartAddress, value)) HasUnsavedChanges = true; } } private int _readLength; /// /// 读取长度 | Read length /// public int ReadLength { get => _readLength; set { if (SetProperty(ref _readLength, value)) HasUnsavedChanges = true; } } private int _bulkReadIntervalMs = 100; /// /// 批量读取周期(毫秒)| Bulk read interval (ms) /// public int BulkReadIntervalMs { get => _bulkReadIntervalMs; set { if (SetProperty(ref _bulkReadIntervalMs, value)) HasUnsavedChanges = true; } } private int _connectTimeoutMs; /// /// 连接超时(毫秒)| Connect timeout (ms) /// public int ConnectTimeoutMs { get => _connectTimeoutMs; set { if (SetProperty(ref _connectTimeoutMs, value)) HasUnsavedChanges = true; } } private int _readTimeoutMs; /// /// 读取超时(毫秒)| Read timeout (ms) /// public int ReadTimeoutMs { get => _readTimeoutMs; set { if (SetProperty(ref _readTimeoutMs, value)) HasUnsavedChanges = true; } } private int _writeTimeoutMs; /// /// 写入超时(毫秒)| Write timeout (ms) /// public int WriteTimeoutMs { get => _writeTimeoutMs; set { if (SetProperty(ref _writeTimeoutMs, value)) HasUnsavedChanges = true; } } private bool _bReConnect; /// /// 断线重连 | Auto reconnect on disconnect /// public bool BReConnect { get => _bReConnect; set { if (SetProperty(ref _bReConnect, value)) HasUnsavedChanges = true; } } // === 文件路径 | File path === private string _xmlFilePath; /// /// XML 配置文件路径 | XML config file path /// public string XmlFilePath { get => _xmlFilePath; set => SetProperty(ref _xmlFilePath, value); } // === 状态标志 | Status flags === private bool _hasUnsavedChanges; /// /// 是否有未保存的修改 | Whether there are unsaved changes /// public bool HasUnsavedChanges { get => _hasUnsavedChanges; set => SetProperty(ref _hasUnsavedChanges, value); } /// /// 窗口关闭回调 | Window close action callback /// public Action CloseAction { get; set; } /// /// 支持的数据类型列表,供下拉框绑定 | Supported data types for ComboBox binding /// public string[] SupportedDataTypes { get; } = SignalEntry.SupportedTypes; // === 命令 | Commands === /// 保存命令 | Save command public DelegateCommand SaveCommand { get; } /// 取消命令 | Cancel command public DelegateCommand CancelCommand { get; } /// 重新加载命令 | Reload command public DelegateCommand ReloadCommand { get; } /// 添加信号命令(操作当前选中 Group)| Add signal command (operates on selected Group) public DelegateCommand AddSignalCommand { get; } /// 删除信号命令(操作当前选中 Group)| Delete signal command (operates on selected Group) public DelegateCommand DeleteSignalCommand { get; } private SignalEntry _selectedSignal; /// /// 当前选中的信号 | Currently selected signal /// public SignalEntry SelectedSignal { get => _selectedSignal; set => SetProperty(ref _selectedSignal, value); } /// 新增 Group 命令 | Add Group command public DelegateCommand AddGroupCommand { get; } /// 删除 Group 命令 | Delete Group command public DelegateCommand DeleteGroupCommand { get; } /// 重命名 Group 命令 | Rename Group command public DelegateCommand RenameGroupCommand { get; } /// /// 构造函数 | Constructor /// /// XML 信号解析器 | XML signal parser /// 配置加载器 | Config loader /// 日志服务 | Logger service 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() ?? throw new ArgumentNullException(nameof(logger)); // 初始化信号分组集合 | Initialize signal group collection Groups = new ObservableCollection(); // 加载 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"); } /// /// 从 ConfigLoader 加载 PLC 连接参数并填充绑定属性 | Load PLC config and populate binding properties /// 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}"); } } /// /// 加载信号分组数据 | Load signal group data /// 调用 XmlSignalParser.LoadFromFile 获取 List<SignalGroup>,填充 Groups 集合 /// 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(); } } /// /// 执行新增 Group 操作 | Execute add Group operation /// 弹出输入框获取 Group ID 和 DBNumber,检查 ID 唯一性 /// 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() }; Groups.Add(newGroup); SelectedGroup = newGroup; HasUnsavedChanges = true; _logger.Info($"新增 Group: ID={groupId}, DBNumber={dbNumber} | Added Group: ID={groupId}, DBNumber={dbNumber}"); } /// /// 执行重命名 Group 操作 | Execute rename Group operation /// 弹出输入框获取新名称,检查唯一性 /// 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}"); } /// /// 执行删除 Group 操作 | Execute delete Group operation /// 删除前确认提示 /// 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}"); } /// /// 执行保存操作 | Execute save operation /// 验证所有 Group 的 ID 唯一性、DBNumber 有效性、信号名称全局唯一性,然后保存 /// private void ExecuteSave() { try { var errors = new List(); // 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); } } /// /// 执行取消操作,关闭窗口 | Execute cancel, close window /// private void ExecuteCancel() { CloseAction?.Invoke(); } /// /// 执行重新加载操作 | Execute reload operation /// 存在未保存修改时提示确认 /// 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(); } /// /// 保存 PLC 连接参数到 App.config(已移除 WriteDbBlock)| Save PLC connection parameters to App.config (WriteDbBlock removed) /// 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}"); } } } }