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}");
}
}
}
}