Agregado de progress dialog
This commit is contained in:
parent
ea96ba2bb9
commit
38c54ab40b
|
@ -3,7 +3,10 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Xml;
|
||||||
using S7Explorer.Models;
|
using S7Explorer.Models;
|
||||||
|
|
||||||
namespace S7Explorer.Parsers
|
namespace S7Explorer.Parsers
|
||||||
|
@ -13,6 +16,12 @@ namespace S7Explorer.Parsers
|
||||||
private readonly string _projectFilePath;
|
private readonly string _projectFilePath;
|
||||||
private readonly string _projectDirectory;
|
private readonly string _projectDirectory;
|
||||||
|
|
||||||
|
// Evento para notificar mensajes de log
|
||||||
|
public event EventHandler<string> LogEvent;
|
||||||
|
|
||||||
|
// Evento para notificar actualizaciones del árbol
|
||||||
|
public event EventHandler<S7Object> StructureUpdatedEvent;
|
||||||
|
|
||||||
public S7ProjectParser(string projectFilePath)
|
public S7ProjectParser(string projectFilePath)
|
||||||
{
|
{
|
||||||
_projectFilePath = projectFilePath;
|
_projectFilePath = projectFilePath;
|
||||||
|
@ -22,64 +31,23 @@ namespace S7Explorer.Parsers
|
||||||
throw new ArgumentException("No se pudo determinar el directorio del proyecto");
|
throw new ArgumentException("No se pudo determinar el directorio del proyecto");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<S7Project> ParseProjectAsync()
|
public void ParseDevices(S7Object devicesFolder, CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
return await Task.Run(() =>
|
|
||||||
{
|
|
||||||
// Crear objeto de proyecto
|
|
||||||
var project = new S7Project(_projectFilePath);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Estructura básica del proyecto
|
|
||||||
var devicesFolder = new S7Object
|
|
||||||
{
|
|
||||||
Name = "Dispositivos",
|
|
||||||
ObjectType = S7ObjectType.Folder,
|
|
||||||
Parent = project
|
|
||||||
};
|
|
||||||
project.Children.Add(devicesFolder);
|
|
||||||
|
|
||||||
// Parsear dispositivos
|
|
||||||
ParseDevices(devicesFolder);
|
|
||||||
|
|
||||||
// Para uso en el futuro: más carpetas de alto nivel
|
|
||||||
var sharedFolder = new S7Object
|
|
||||||
{
|
|
||||||
Name = "Datos Globales",
|
|
||||||
ObjectType = S7ObjectType.Folder,
|
|
||||||
Parent = project
|
|
||||||
};
|
|
||||||
project.Children.Add(sharedFolder);
|
|
||||||
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// En caso de error, al menos retornamos un proyecto básico con el error
|
|
||||||
var errorObject = new S7Object
|
|
||||||
{
|
|
||||||
Name = "Error al parsear proyecto",
|
|
||||||
Description = ex.Message,
|
|
||||||
ObjectType = S7ObjectType.Folder,
|
|
||||||
Parent = project
|
|
||||||
};
|
|
||||||
project.Children.Add(errorObject);
|
|
||||||
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ParseDevices(S7Object devicesFolder)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Obtener lista de dispositivos a partir del archivo de información
|
Log("Iniciando parseo de dispositivos");
|
||||||
|
|
||||||
|
// Obtener lista de dispositivos a partir de los archivos de información
|
||||||
var deviceIdInfos = ParseDeviceIdInfos();
|
var deviceIdInfos = ParseDeviceIdInfos();
|
||||||
|
Log($"Se encontraron {deviceIdInfos.Count} dispositivos");
|
||||||
|
|
||||||
foreach (var deviceInfo in deviceIdInfos)
|
foreach (var deviceInfo in deviceIdInfos)
|
||||||
{
|
{
|
||||||
|
// Verificar cancelación
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Log($"Procesando dispositivo: {deviceInfo.Name}");
|
||||||
|
|
||||||
var device = new S7Object
|
var device = new S7Object
|
||||||
{
|
{
|
||||||
Name = deviceInfo.Name,
|
Name = deviceInfo.Name,
|
||||||
|
@ -88,28 +56,74 @@ namespace S7Explorer.Parsers
|
||||||
};
|
};
|
||||||
devicesFolder.Children.Add(device);
|
devicesFolder.Children.Add(device);
|
||||||
|
|
||||||
|
// Notificar actualización de UI
|
||||||
|
NotifyStructureUpdated(devicesFolder);
|
||||||
|
|
||||||
// Crear carpetas para cada tipo de bloque
|
// Crear carpetas para cada tipo de bloque
|
||||||
var dbFolder = CreateBlockFolder(device, "Bloques de datos (DB)");
|
var dbFolder = CreateBlockFolder(device, "Bloques de datos (DB)");
|
||||||
var fbFolder = CreateBlockFolder(device, "Bloques de función (FB)");
|
var fbFolder = CreateBlockFolder(device, "Bloques de función (FB)");
|
||||||
var fcFolder = CreateBlockFolder(device, "Funciones (FC)");
|
var fcFolder = CreateBlockFolder(device, "Funciones (FC)");
|
||||||
var obFolder = CreateBlockFolder(device, "Bloques de organización (OB)");
|
var obFolder = CreateBlockFolder(device, "Bloques de organización (OB)");
|
||||||
|
var udtFolder = CreateBlockFolder(device, "Tipos de datos (UDT)");
|
||||||
var symbolsFolder = CreateBlockFolder(device, "Tabla de símbolos");
|
var symbolsFolder = CreateBlockFolder(device, "Tabla de símbolos");
|
||||||
|
|
||||||
// Parsear bloques y símbolos si se dispone de IDs
|
// Notificar actualización de UI nuevamente
|
||||||
|
NotifyStructureUpdated(device);
|
||||||
|
|
||||||
|
// Parsear símbolos - esto lo hacemos primero porque suele ser más rápido
|
||||||
if (deviceInfo.SymbolListId.HasValue)
|
if (deviceInfo.SymbolListId.HasValue)
|
||||||
{
|
{
|
||||||
ParseSymbols(symbolsFolder, deviceInfo.SymbolListId.Value);
|
Log($"Parseando símbolos con ID: 0x{deviceInfo.SymbolListId.Value:X8}");
|
||||||
|
ParseSymbolsList(symbolsFolder, deviceInfo.SymbolListId.Value, cancellationToken);
|
||||||
|
// Notificar actualización de UI
|
||||||
|
NotifyStructureUpdated(symbolsFolder);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogWarning("No se encontró ID de lista de símbolos para este dispositivo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parsear bloques
|
||||||
if (deviceInfo.SubblockListId.HasValue)
|
if (deviceInfo.SubblockListId.HasValue)
|
||||||
{
|
{
|
||||||
ParseBlocks(dbFolder, fbFolder, fcFolder, obFolder, deviceInfo.SubblockListId.Value);
|
Log($"Parseando bloques con ID: 0x{deviceInfo.SubblockListId.Value:X8}");
|
||||||
|
|
||||||
|
// Parseamos por tipo y notificamos después de cada tipo para no bloquear la UI
|
||||||
|
ParseBlocksOfType(dbFolder, "00006", "DB", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||||
|
NotifyStructureUpdated(dbFolder);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
ParseBlocksOfType(fbFolder, "00004", "FB", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||||
|
NotifyStructureUpdated(fbFolder);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
ParseBlocksOfType(fcFolder, "00003", "FC", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||||
|
NotifyStructureUpdated(fcFolder);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
ParseBlocksOfType(obFolder, "00008", "OB", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||||
|
NotifyStructureUpdated(obFolder);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
ParseBlocksOfType(udtFolder, "00001", "UDT", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||||
|
NotifyStructureUpdated(udtFolder);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogWarning("No se encontró ID de lista de bloques para este dispositivo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
LogWarning("Parseo de dispositivos cancelado por el usuario");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Si falla el parseo, añadimos un nodo de error pero seguimos con el resto
|
// Si falla el parseo, añadimos un nodo de error pero seguimos con el resto
|
||||||
|
LogError($"Error al parsear dispositivos: {ex.Message}");
|
||||||
|
|
||||||
var errorNode = new S7Object
|
var errorNode = new S7Object
|
||||||
{
|
{
|
||||||
Name = "Error al parsear dispositivos",
|
Name = "Error al parsear dispositivos",
|
||||||
|
@ -118,6 +132,7 @@ namespace S7Explorer.Parsers
|
||||||
Parent = devicesFolder
|
Parent = devicesFolder
|
||||||
};
|
};
|
||||||
devicesFolder.Children.Add(errorNode);
|
devicesFolder.Children.Add(errorNode);
|
||||||
|
NotifyStructureUpdated(devicesFolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,16 +152,33 @@ namespace S7Explorer.Parsers
|
||||||
{
|
{
|
||||||
var result = new List<DeviceIdInfo>();
|
var result = new List<DeviceIdInfo>();
|
||||||
|
|
||||||
// Esto es una simplificación - en una implementación real
|
|
||||||
// necesitarías parsear los archivos reales como en el proyecto C++
|
|
||||||
var s7resoffPath = Path.Combine(_projectDirectory, "hrs", "S7RESOFF.DBF");
|
|
||||||
|
|
||||||
if (File.Exists(s7resoffPath))
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Leer la tabla de dispositivos
|
// Parsear S7RESOFF.DBF
|
||||||
|
var s7resoffPath = Path.Combine(_projectDirectory, "hrs", "S7RESOFF.DBF");
|
||||||
|
if (File.Exists(s7resoffPath))
|
||||||
|
{
|
||||||
|
Log($"Leyendo archivo S7RESOFF.DBF: {s7resoffPath}");
|
||||||
var records = DbfParser.ReadDbfFile(s7resoffPath, new[] { "ID", "NAME", "RSRVD4_L" });
|
var records = DbfParser.ReadDbfFile(s7resoffPath, new[] { "ID", "NAME", "RSRVD4_L" });
|
||||||
|
Log($"Se encontraron {records.Count} registros en S7RESOFF.DBF");
|
||||||
|
|
||||||
|
// Leer linkhrs.lnk para obtener IDs de Subblock y SymbolList
|
||||||
|
var linkhrsPath = Path.Combine(_projectDirectory, "hrs", "linkhrs.lnk");
|
||||||
|
byte[] linkhrsData = null;
|
||||||
|
if (File.Exists(linkhrsPath))
|
||||||
|
{
|
||||||
|
Log($"Leyendo archivo linkhrs.lnk: {linkhrsPath}");
|
||||||
|
linkhrsData = File.ReadAllBytes(linkhrsPath);
|
||||||
|
Log($"Leídos {linkhrsData.Length} bytes de linkhrs.lnk");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogWarning($"Archivo linkhrs.lnk no encontrado: {linkhrsPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constantes para IDs
|
||||||
|
const uint SubblockListIdMagic = 0x00116001;
|
||||||
|
const uint SymbolListIdMagic = 0x00113001;
|
||||||
|
|
||||||
foreach (var record in records)
|
foreach (var record in records)
|
||||||
{
|
{
|
||||||
|
@ -155,258 +187,378 @@ namespace S7Explorer.Parsers
|
||||||
Name = DbfParser.ConvertCP1252ToUtf8(record["NAME"]),
|
Name = DbfParser.ConvertCP1252ToUtf8(record["NAME"]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Procesamiento simplificado para IDs de Subblock y Symbol List
|
// Obtener offset en linkhrs.lnk
|
||||||
device.SubblockListId = DbfParser.StringToInt(record["RSRVD4_L"]);
|
var offset = DbfParser.StringToInt(record["RSRVD4_L"]);
|
||||||
|
if (offset.HasValue && linkhrsData != null && offset.Value < linkhrsData.Length - 512)
|
||||||
|
{
|
||||||
|
Log($"Procesando dispositivo '{device.Name}' con offset: {offset.Value}");
|
||||||
|
|
||||||
// NOTA: En una implementación completa, tendrías que leer linkhrs.lnk
|
// Leer 512 bytes (128 uint32) desde el offset
|
||||||
// para obtener SubblockListId y SymbolListId reales
|
for (int i = offset.Value; i < offset.Value + 512 - 8; i += 4)
|
||||||
|
{
|
||||||
|
if (i + 4 >= linkhrsData.Length) break;
|
||||||
|
|
||||||
|
uint value = BitConverter.ToUInt32(linkhrsData, i);
|
||||||
|
if (value == SubblockListIdMagic && i + 4 < linkhrsData.Length)
|
||||||
|
{
|
||||||
|
device.SubblockListId = BitConverter.ToUInt32(linkhrsData, i + 4);
|
||||||
|
Log($" Encontrado SubblockListId: 0x{device.SubblockListId:X8}");
|
||||||
|
}
|
||||||
|
else if (value == SymbolListIdMagic && i + 4 < linkhrsData.Length)
|
||||||
|
{
|
||||||
|
device.SymbolListId = BitConverter.ToUInt32(linkhrsData, i + 4);
|
||||||
|
Log($" Encontrado SymbolListId: 0x{device.SymbolListId:X8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogWarning($"No se pudo obtener offset para '{device.Name}' o offset inválido");
|
||||||
|
}
|
||||||
|
|
||||||
result.Add(device);
|
result.Add(device);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
else
|
||||||
{
|
{
|
||||||
// Error de parseo - ignorar y continuar
|
LogWarning($"Archivo S7RESOFF.DBF no encontrado: {s7resoffPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Registrar error pero continuar
|
||||||
|
LogError($"Error parseando información de dispositivos: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
// Si no se encuentran dispositivos, crear uno de ejemplo
|
// Si no encontramos dispositivos, crear uno simple con valores por defecto
|
||||||
if (result.Count == 0)
|
if (result.Count == 0)
|
||||||
{
|
{
|
||||||
|
LogWarning("No se encontraron dispositivos, creando dispositivo por defecto");
|
||||||
result.Add(new DeviceIdInfo
|
result.Add(new DeviceIdInfo
|
||||||
{
|
{
|
||||||
Name = "Dispositivo de ejemplo"
|
Name = Path.GetFileNameWithoutExtension(_projectFilePath),
|
||||||
|
SubblockListId = 0,
|
||||||
|
SymbolListId = 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParseSymbols(S7Object symbolsFolder, int symbolListId)
|
private void ParseSymbolsList(S7Object symbolsFolder, uint symbolListId, CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
// Esta es una implementación de muestra
|
|
||||||
// En una versión completa, leerías los archivos YDBs reales
|
|
||||||
|
|
||||||
// Crear algunos símbolos de ejemplo
|
|
||||||
var symbols = new List<S7Symbol>
|
|
||||||
{
|
|
||||||
new S7Symbol {
|
|
||||||
Name = "Motor_Start",
|
|
||||||
Address = "I0.0",
|
|
||||||
DataType = "BOOL",
|
|
||||||
Comment = "Pulsador de inicio del motor"
|
|
||||||
},
|
|
||||||
new S7Symbol {
|
|
||||||
Name = "Motor_Stop",
|
|
||||||
Address = "I0.1",
|
|
||||||
DataType = "BOOL",
|
|
||||||
Comment = "Pulsador de parada del motor"
|
|
||||||
},
|
|
||||||
new S7Symbol {
|
|
||||||
Name = "Motor_Running",
|
|
||||||
Address = "Q0.0",
|
|
||||||
DataType = "BOOL",
|
|
||||||
Comment = "Motor en marcha"
|
|
||||||
},
|
|
||||||
new S7Symbol {
|
|
||||||
Name = "Temperature",
|
|
||||||
Address = "IW64",
|
|
||||||
DataType = "INT",
|
|
||||||
Comment = "Temperatura del proceso"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var symbol in symbols)
|
|
||||||
{
|
|
||||||
symbol.Parent = symbolsFolder;
|
|
||||||
symbolsFolder.Children.Add(symbol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ParseBlocks(S7Object dbFolder, S7Object fbFolder, S7Object fcFolder, S7Object obFolder, int subblockListId)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Usar parser específico para las funciones (FC)
|
// Verificar cancelación
|
||||||
var fcParser = new FCParser(_projectDirectory);
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var functions = fcParser.ParseFunctionBlocks(subblockListId);
|
|
||||||
|
|
||||||
// Añadir bloques FC al árbol de proyecto
|
// Construir la ruta del archivo de símbolos basada en SymbolListId
|
||||||
foreach (var fc in functions)
|
string symbolsPath = FindSymbolsPath(symbolListId);
|
||||||
|
if (string.IsNullOrEmpty(symbolsPath))
|
||||||
{
|
{
|
||||||
fc.Parent = fcFolder;
|
LogWarning($"No se pudo encontrar el archivo de símbolos para ID: 0x{symbolListId:X8}");
|
||||||
fcFolder.Children.Add(fc);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Para los otros tipos de bloques, seguimos usando los ejemplos por ahora
|
Log($"Procesando archivo de símbolos: {symbolsPath}");
|
||||||
// En una implementación completa, crearíamos parsers específicos para cada tipo
|
|
||||||
AddSampleDataBlocks(dbFolder);
|
// Leer SYMLIST.DBF
|
||||||
AddSampleFunctionBlocks(fbFolder);
|
var records = DbfParser.ReadDbfFile(symbolsPath, new[] { "_SKZ", "_OPIEC", "_DATATYP", "_COMMENT" });
|
||||||
AddSampleOrgBlocks(obFolder);
|
Log($"Se encontraron {records.Count} registros en SYMLIST.DBF");
|
||||||
|
|
||||||
|
int symbolCount = 0;
|
||||||
|
int updateFrequency = Math.Max(1, records.Count / 10); // Actualizar UI cada 10% aprox
|
||||||
|
|
||||||
|
foreach (var record in records)
|
||||||
|
{
|
||||||
|
// Verificar cancelación periódicamente
|
||||||
|
if (symbolCount % 50 == 0)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo procesar entradas con código
|
||||||
|
if (!string.IsNullOrEmpty(record["_OPIEC"]))
|
||||||
|
{
|
||||||
|
string code = record["_OPIEC"].Trim();
|
||||||
|
|
||||||
|
// Solo queremos símbolos I, M, Q y DB para la lista de símbolos
|
||||||
|
if (code.StartsWith("I") || code.StartsWith("M") || code.StartsWith("Q") || code.StartsWith("DB"))
|
||||||
|
{
|
||||||
|
var symbol = new S7Symbol
|
||||||
|
{
|
||||||
|
Name = DbfParser.ConvertCP1252ToUtf8(record["_SKZ"]),
|
||||||
|
Address = code,
|
||||||
|
DataType = record["_DATATYP"],
|
||||||
|
Comment = DbfParser.ConvertCP1252ToUtf8(record["_COMMENT"]),
|
||||||
|
Parent = symbolsFolder,
|
||||||
|
ObjectType = S7ObjectType.Symbol
|
||||||
|
};
|
||||||
|
symbolsFolder.Children.Add(symbol);
|
||||||
|
symbolCount++;
|
||||||
|
|
||||||
|
// Actualizar UI periódicamente para mostrar progreso
|
||||||
|
if (symbolCount % updateFrequency == 0)
|
||||||
|
{
|
||||||
|
Log($"Procesados {symbolCount} símbolos...");
|
||||||
|
NotifyStructureUpdated(symbolsFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"Se agregaron {symbolCount} símbolos a la tabla de símbolos");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
LogWarning("Parseo de símbolos cancelado por el usuario");
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// En caso de error, añadimos un objeto de error informativo
|
LogError($"Error parseando símbolos: {ex.Message}");
|
||||||
var errorObject = new S7Object
|
|
||||||
|
var errorSymbol = new S7Symbol
|
||||||
{
|
{
|
||||||
Name = "Error en parseo de bloques",
|
Name = "Error al parsear símbolos",
|
||||||
Description = ex.Message,
|
Description = ex.Message,
|
||||||
ObjectType = S7ObjectType.Folder
|
Parent = symbolsFolder,
|
||||||
|
ObjectType = S7ObjectType.Symbol
|
||||||
};
|
};
|
||||||
|
symbolsFolder.Children.Add(errorSymbol);
|
||||||
|
NotifyStructureUpdated(symbolsFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lo añadimos a cada carpeta para que sea visible
|
private string FindSymbolsPath(uint symbolListId)
|
||||||
dbFolder.Children.Add(errorObject);
|
|
||||||
fbFolder.Children.Add(new S7Object
|
|
||||||
{
|
{
|
||||||
Name = errorObject.Name,
|
try
|
||||||
Description = errorObject.Description,
|
{
|
||||||
ObjectType = errorObject.ObjectType
|
// Buscar en SYMLISTS.DBF el path basado en el ID
|
||||||
|
var symlistsPath = Path.Combine(_projectDirectory, "YDBs", "SYMLISTS.DBF");
|
||||||
|
if (File.Exists(symlistsPath))
|
||||||
|
{
|
||||||
|
Log($"Buscando ruta de símbolos en SYMLISTS.DBF para ID: 0x{symbolListId:X8}");
|
||||||
|
var records = DbfParser.ReadDbfFile(symlistsPath, new[] { "_ID", "_DBPATH" });
|
||||||
|
|
||||||
|
foreach (var record in records)
|
||||||
|
{
|
||||||
|
var id = DbfParser.StringToInt(record["_ID"]);
|
||||||
|
if (id.HasValue && id.Value == symbolListId)
|
||||||
|
{
|
||||||
|
string dbPath = record["_DBPATH"];
|
||||||
|
string fullPath = Path.Combine(_projectDirectory, "YDBs", dbPath, "SYMLIST.DBF");
|
||||||
|
Log($"Encontrada ruta de símbolos: {fullPath}");
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogWarning($"No se encontró registro en SYMLISTS.DBF para ID: 0x{symbolListId:X8}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogWarning($"Archivo SYMLISTS.DBF no encontrado: {symlistsPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError($"Error buscando ruta de símbolos: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no se encuentra, intentar una búsqueda directa
|
||||||
|
string symbolFolderPath = Path.Combine(_projectDirectory, "YDBs", symbolListId.ToString("X8"));
|
||||||
|
string symbolFilePath = Path.Combine(symbolFolderPath, "SYMLIST.DBF");
|
||||||
|
if (Directory.Exists(symbolFolderPath) && File.Exists(symbolFilePath))
|
||||||
|
{
|
||||||
|
Log($"Encontrada ruta de símbolos mediante búsqueda directa: {symbolFilePath}");
|
||||||
|
return symbolFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseBlocksOfType(S7Object blockFolder, string blockType, string prefix,
|
||||||
|
uint subblockListId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Construir la ruta a SUBBLK.DBF
|
||||||
|
string subblockPath = Path.Combine(_projectDirectory, "ombstx", "offline",
|
||||||
|
subblockListId.ToString("X8"), "SUBBLK.DBF");
|
||||||
|
|
||||||
|
if (!File.Exists(subblockPath))
|
||||||
|
{
|
||||||
|
LogWarning($"Archivo SUBBLK.DBF no encontrado: {subblockPath}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"Procesando bloques de tipo {prefix} desde: {subblockPath}");
|
||||||
|
|
||||||
|
// Leer SUBBLK.DBF
|
||||||
|
var records = DbfParser.ReadDbfFile(subblockPath, new[]
|
||||||
|
{
|
||||||
|
"SUBBLKTYP", "BLKNUMBER", "BLKNAME", "AUTHOR",
|
||||||
|
"FAMILY", "VERSION", "CREATEDATE", "MODDATE"
|
||||||
});
|
});
|
||||||
fcFolder.Children.Add(new S7Object
|
|
||||||
|
// Filtrar registros para este tipo de bloque
|
||||||
|
var blockRecords = records.Where(r => r["SUBBLKTYP"] == blockType).ToList();
|
||||||
|
Log($"Se encontraron {blockRecords.Count} bloques {prefix}");
|
||||||
|
|
||||||
|
if (blockRecords.Count == 0)
|
||||||
{
|
{
|
||||||
Name = errorObject.Name,
|
return;
|
||||||
Description = errorObject.Description,
|
}
|
||||||
ObjectType = errorObject.ObjectType
|
|
||||||
});
|
// Contador de bloques
|
||||||
obFolder.Children.Add(new S7Object
|
int blockCount = 0;
|
||||||
{
|
int updateFrequency = Math.Max(1, blockRecords.Count / 5); // Actualizar UI cada 20% aprox
|
||||||
Name = errorObject.Name,
|
|
||||||
Description = errorObject.Description,
|
// Definir tipo de objeto según prefijo
|
||||||
ObjectType = errorObject.ObjectType
|
S7ObjectType objectType;
|
||||||
|
switch (prefix)
|
||||||
|
{
|
||||||
|
case "DB": objectType = S7ObjectType.DataBlock; break;
|
||||||
|
case "FB": objectType = S7ObjectType.FunctionBlock; break;
|
||||||
|
case "FC": objectType = S7ObjectType.Function; break;
|
||||||
|
case "OB": objectType = S7ObjectType.Organization; break;
|
||||||
|
default: objectType = S7ObjectType.Folder; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesar cada registro
|
||||||
|
foreach (var record in blockRecords)
|
||||||
|
{
|
||||||
|
// Verificar cancelación periódicamente
|
||||||
|
if (blockCount % 20 == 0)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo procesar si tenemos un número de bloque válido
|
||||||
|
if (int.TryParse(record["BLKNUMBER"], out int blockNumber))
|
||||||
|
{
|
||||||
|
// Crear objeto de bloque
|
||||||
|
var block = new S7Block
|
||||||
|
{
|
||||||
|
Name = DbfParser.ConvertCP1252ToUtf8(record["BLKNAME"]),
|
||||||
|
Number = $"{prefix}{blockNumber}",
|
||||||
|
AuthorName = DbfParser.ConvertCP1252ToUtf8(record["AUTHOR"]),
|
||||||
|
Family = DbfParser.ConvertCP1252ToUtf8(record["FAMILY"]),
|
||||||
|
Version = record["VERSION"],
|
||||||
|
ObjectType = objectType,
|
||||||
|
Parent = blockFolder
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intentar extraer fecha de modificación
|
||||||
|
if (DateTime.TryParse(record["MODDATE"], out DateTime modDate))
|
||||||
|
{
|
||||||
|
block.Modified = modDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añadir a la carpeta correspondiente
|
||||||
|
blockFolder.Children.Add(block);
|
||||||
|
blockCount++;
|
||||||
|
|
||||||
|
// Log detallado cada cierto número de bloques para evitar saturar el log
|
||||||
|
if (blockCount <= 5 || blockCount % 20 == 0)
|
||||||
|
{
|
||||||
|
Log($"Agregado bloque {block.Number} - {block.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar UI periódicamente para mostrar progreso
|
||||||
|
if (blockCount % updateFrequency == 0)
|
||||||
|
{
|
||||||
|
Log($"Procesados {blockCount}/{blockRecords.Count} bloques {prefix}...");
|
||||||
|
NotifyStructureUpdated(blockFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenar bloques por número
|
||||||
|
SortBlocksInFolder(blockFolder);
|
||||||
|
|
||||||
|
Log($"Completado procesamiento de {blockCount} bloques {prefix}");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
LogWarning($"Parseo de bloques {prefix} cancelado por el usuario");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError($"Error en parseo de bloques {prefix}: {ex.Message}");
|
||||||
|
blockFolder.Children.Add(new S7Object
|
||||||
|
{
|
||||||
|
Name = $"Error en bloques {prefix}",
|
||||||
|
Description = ex.Message,
|
||||||
|
Parent = blockFolder
|
||||||
});
|
});
|
||||||
|
NotifyStructureUpdated(blockFolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSampleDataBlocks(S7Object dbFolder)
|
private void SortBlocksInFolder(S7Object folder)
|
||||||
{
|
{
|
||||||
var dbs = new List<S7DataBlock>
|
// Crear una nueva lista ordenada
|
||||||
{
|
var sortedList = folder.Children
|
||||||
new S7DataBlock {
|
.OrderBy(block => {
|
||||||
Name = "Datos_proceso",
|
if (block.Number == null) return int.MaxValue;
|
||||||
Number = "DB1",
|
|
||||||
Size = 124,
|
|
||||||
IsInstanceDb = false,
|
|
||||||
Description = "Datos de proceso",
|
|
||||||
Modified = DateTime.Now.AddDays(-5)
|
|
||||||
},
|
|
||||||
new S7DataBlock {
|
|
||||||
Name = "Parámetros",
|
|
||||||
Number = "DB2",
|
|
||||||
Size = 234,
|
|
||||||
IsInstanceDb = false,
|
|
||||||
Description = "Parámetros de configuración",
|
|
||||||
Modified = DateTime.Now.AddDays(-10)
|
|
||||||
},
|
|
||||||
new S7DataBlock {
|
|
||||||
Name = "Motor_Inst",
|
|
||||||
Number = "DB10",
|
|
||||||
Size = 86,
|
|
||||||
IsInstanceDb = true,
|
|
||||||
Description = "Instancia de FB1",
|
|
||||||
Modified = DateTime.Now.AddDays(-2)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var db in dbs)
|
string numPart = new string(block.Number
|
||||||
|
.SkipWhile(c => !char.IsDigit(c))
|
||||||
|
.TakeWhile(char.IsDigit)
|
||||||
|
.ToArray());
|
||||||
|
|
||||||
|
return int.TryParse(numPart, out int num) ? num : int.MaxValue;
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Limpiar y añadir los elementos ordenados
|
||||||
|
folder.Children.Clear();
|
||||||
|
foreach (var item in sortedList)
|
||||||
{
|
{
|
||||||
db.Parent = dbFolder;
|
folder.Children.Add(item);
|
||||||
dbFolder.Children.Add(db);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSampleFunctionBlocks(S7Object fbFolder)
|
private void NotifyStructureUpdated(S7Object obj)
|
||||||
{
|
{
|
||||||
var fbs = new List<S7FunctionBlock>
|
StructureUpdatedEvent?.Invoke(this, obj);
|
||||||
{
|
|
||||||
new S7FunctionBlock {
|
|
||||||
Name = "Motor_Control",
|
|
||||||
Number = "FB1",
|
|
||||||
Size = 328,
|
|
||||||
Language = "SCL",
|
|
||||||
Description = "Control de motor",
|
|
||||||
Modified = DateTime.Now.AddDays(-15)
|
|
||||||
},
|
|
||||||
new S7FunctionBlock {
|
|
||||||
Name = "PID_Control",
|
|
||||||
Number = "FB2",
|
|
||||||
Size = 512,
|
|
||||||
Language = "SCL",
|
|
||||||
Description = "Controlador PID",
|
|
||||||
Modified = DateTime.Now.AddDays(-20)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var fb in fbs)
|
|
||||||
{
|
|
||||||
fb.Parent = fbFolder;
|
|
||||||
fbFolder.Children.Add(fb);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSampleFunctions(S7Object fcFolder)
|
#region Logging Methods
|
||||||
{
|
|
||||||
var fcs = new List<S7Function>
|
|
||||||
{
|
|
||||||
new S7Function {
|
|
||||||
Name = "Calc_Setpoint",
|
|
||||||
Number = "FC1",
|
|
||||||
Size = 124,
|
|
||||||
ReturnType = "REAL",
|
|
||||||
Description = "Cálculo de punto de consigna",
|
|
||||||
Modified = DateTime.Now.AddDays(-8)
|
|
||||||
},
|
|
||||||
new S7Function {
|
|
||||||
Name = "Scale_Analog",
|
|
||||||
Number = "FC2",
|
|
||||||
Size = 68,
|
|
||||||
ReturnType = "REAL",
|
|
||||||
Description = "Escalado de valor analógico",
|
|
||||||
Modified = DateTime.Now.AddDays(-12)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var fc in fcs)
|
private void Log(string message)
|
||||||
{
|
{
|
||||||
fc.Parent = fcFolder;
|
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
fcFolder.Children.Add(fc);
|
string logMessage = $"[{timestamp}] {message}";
|
||||||
}
|
LogEvent?.Invoke(this, logMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSampleOrgBlocks(S7Object obFolder)
|
private void LogWarning(string message)
|
||||||
{
|
{
|
||||||
var obs = new List<S7Block>
|
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
{
|
string logMessage = $"[{timestamp}] ADVERTENCIA: {message}";
|
||||||
new S7Block {
|
LogEvent?.Invoke(this, logMessage);
|
||||||
Name = "Main",
|
|
||||||
Number = "OB1",
|
|
||||||
Size = 256,
|
|
||||||
ObjectType = S7ObjectType.Organization,
|
|
||||||
Description = "Ciclo principal",
|
|
||||||
Modified = DateTime.Now.AddDays(-1)
|
|
||||||
},
|
|
||||||
new S7Block {
|
|
||||||
Name = "Clock_100ms",
|
|
||||||
Number = "OB35",
|
|
||||||
Size = 124,
|
|
||||||
ObjectType = S7ObjectType.Organization,
|
|
||||||
Description = "Interrupción cíclica 100ms",
|
|
||||||
Modified = DateTime.Now.AddDays(-7)
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var ob in obs)
|
private void LogError(string message)
|
||||||
{
|
{
|
||||||
ob.Parent = obFolder;
|
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
obFolder.Children.Add(ob);
|
string logMessage = $"[{timestamp}] ERROR: {message}";
|
||||||
}
|
LogEvent?.Invoke(this, logMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
// Clase interna para la información de los dispositivos
|
// Clase interna para la información de los dispositivos
|
||||||
private class DeviceIdInfo
|
private class DeviceIdInfo
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public int? SubblockListId { get; set; }
|
public uint? SubblockListId { get; set; }
|
||||||
public int? SymbolListId { get; set; }
|
public uint? SymbolListId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using S7Explorer.Models;
|
||||||
|
using S7Explorer.Parsers;
|
||||||
|
|
||||||
|
namespace S7Explorer.Services
|
||||||
|
{
|
||||||
|
public class S7ParserService
|
||||||
|
{
|
||||||
|
// Evento para notificar mensajes de log
|
||||||
|
public event EventHandler<string> LogEvent;
|
||||||
|
|
||||||
|
// Evento para notificar actualizaciones del árbol
|
||||||
|
public event EventHandler<S7Object> StructureUpdatedEvent;
|
||||||
|
|
||||||
|
// Cancelation support
|
||||||
|
private CancellationTokenSource _cancellationTokenSource;
|
||||||
|
|
||||||
|
// Ruta y nombre del archivo de caché JSON
|
||||||
|
private static string GetCacheFilePath(string projectFilePath)
|
||||||
|
{
|
||||||
|
return Path.ChangeExtension(projectFilePath, ".s7cache.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuración de JSON para serialización/deserialización
|
||||||
|
private static JsonSerializerSettings JsonSettings => new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||||
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
||||||
|
TypeNameHandling = TypeNameHandling.Auto,
|
||||||
|
Formatting = Formatting.Indented
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task<S7Project> ParseProjectAsync(string projectFilePath, bool useCache = true)
|
||||||
|
{
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Intentar cargar desde caché si useCache es true
|
||||||
|
if (useCache)
|
||||||
|
{
|
||||||
|
var cachedProject = TryLoadFromCache(projectFilePath);
|
||||||
|
if (cachedProject != null)
|
||||||
|
{
|
||||||
|
LogInfo($"Proyecto cargado desde caché: {projectFilePath}");
|
||||||
|
return cachedProject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear nueva instancia del proyecto
|
||||||
|
var project = new S7Project(projectFilePath);
|
||||||
|
|
||||||
|
// Notificar al UI de la estructura base
|
||||||
|
LogInfo($"Iniciando parseo del proyecto: {projectFilePath}");
|
||||||
|
|
||||||
|
// Estructura básica del proyecto
|
||||||
|
var devicesFolder = new S7Object
|
||||||
|
{
|
||||||
|
Name = "Dispositivos",
|
||||||
|
ObjectType = S7ObjectType.Folder,
|
||||||
|
Parent = project
|
||||||
|
};
|
||||||
|
project.Children.Add(devicesFolder);
|
||||||
|
|
||||||
|
// Notificar al UI sobre la estructura inicial
|
||||||
|
NotifyStructureUpdated(project);
|
||||||
|
|
||||||
|
// Iniciar parseo en segundo plano
|
||||||
|
await Task.Run(() => ParseProjectInBackground(project, devicesFolder, projectFilePath),
|
||||||
|
_cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
LogWarning("Operación de parseo cancelada por el usuario");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError($"Error general en parseo: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CancelParsing()
|
||||||
|
{
|
||||||
|
_cancellationTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseProjectInBackground(S7Project project, S7Object devicesFolder, string projectFilePath)
|
||||||
|
{
|
||||||
|
// Crear instancia del parser
|
||||||
|
var parser = new S7ProjectParser(projectFilePath);
|
||||||
|
|
||||||
|
// Conectar eventos de log
|
||||||
|
parser.LogEvent += (sender, message) => LogEvent?.Invoke(this, message);
|
||||||
|
parser.StructureUpdatedEvent += (sender, obj) => NotifyStructureUpdated(obj);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Parseo de dispositivos
|
||||||
|
parser.ParseDevices(devicesFolder, _cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
// Guardar en caché para futuro uso
|
||||||
|
SaveToCache(project, projectFilePath);
|
||||||
|
|
||||||
|
LogInfo("Parseo del proyecto completado correctamente");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
LogWarning("Parseo cancelado por el usuario");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError($"Error en parseo: {ex.Message}");
|
||||||
|
|
||||||
|
// Añadir nodo de error
|
||||||
|
var errorNode = new S7Object
|
||||||
|
{
|
||||||
|
Name = "Error al parsear proyecto",
|
||||||
|
Description = ex.Message,
|
||||||
|
ObjectType = S7ObjectType.Folder,
|
||||||
|
Parent = devicesFolder
|
||||||
|
};
|
||||||
|
devicesFolder.Children.Add(errorNode);
|
||||||
|
|
||||||
|
// Notificar al UI
|
||||||
|
NotifyStructureUpdated(devicesFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private S7Project TryLoadFromCache(string projectFilePath)
|
||||||
|
{
|
||||||
|
string cacheFilePath = GetCacheFilePath(projectFilePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(cacheFilePath))
|
||||||
|
{
|
||||||
|
LogInfo($"No existe archivo de caché: {cacheFilePath}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el archivo de caché es más reciente que el proyecto
|
||||||
|
var projectFileInfo = new FileInfo(projectFilePath);
|
||||||
|
var cacheFileInfo = new FileInfo(cacheFilePath);
|
||||||
|
|
||||||
|
if (cacheFileInfo.LastWriteTime < projectFileInfo.LastWriteTime)
|
||||||
|
{
|
||||||
|
LogInfo("El archivo de caché es más antiguo que el proyecto. Recargando...");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogInfo($"Cargando proyecto desde caché: {cacheFilePath}");
|
||||||
|
string json = File.ReadAllText(cacheFilePath);
|
||||||
|
var project = JsonConvert.DeserializeObject<S7Project>(json, JsonSettings);
|
||||||
|
|
||||||
|
// Reconstruir referencias padre/hijo
|
||||||
|
RebuildParentChildReferences(project);
|
||||||
|
|
||||||
|
LogInfo("Proyecto cargado correctamente desde caché");
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogWarning($"Error al cargar desde caché: {ex.Message}");
|
||||||
|
// Si hay un error al cargar la caché, simplemente retornar null para forzar el reparseo
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Intentar eliminar el archivo de caché corrupto
|
||||||
|
if (File.Exists(cacheFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(cacheFilePath);
|
||||||
|
LogInfo("Archivo de caché corrupto eliminado");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignorar errores al eliminar
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveToCache(S7Project project, string projectFilePath)
|
||||||
|
{
|
||||||
|
string cacheFilePath = GetCacheFilePath(projectFilePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogInfo($"Guardando proyecto en caché: {cacheFilePath}");
|
||||||
|
string json = JsonConvert.SerializeObject(project, JsonSettings);
|
||||||
|
File.WriteAllText(cacheFilePath, json);
|
||||||
|
LogInfo("Proyecto guardado correctamente en caché");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogWarning($"Error al guardar proyecto en caché: {ex.Message}");
|
||||||
|
// Continuar a pesar del error, solo es caché
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruir referencias padre/hijo después de deserializar JSON
|
||||||
|
private void RebuildParentChildReferences(S7Object root)
|
||||||
|
{
|
||||||
|
foreach (var child in root.Children)
|
||||||
|
{
|
||||||
|
child.Parent = root;
|
||||||
|
RebuildParentChildReferences(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyStructureUpdated(S7Object obj)
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
StructureUpdatedEvent?.Invoke(this, obj);
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Logging Methods
|
||||||
|
|
||||||
|
private void LogInfo(string message)
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
|
string logMessage = $"[{timestamp}] {message}";
|
||||||
|
LogEvent?.Invoke(this, logMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogWarning(string message)
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
|
string logMessage = $"[{timestamp}] ADVERTENCIA: {message}";
|
||||||
|
LogEvent?.Invoke(this, logMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogError(string message)
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
|
string logMessage = $"[{timestamp}] ERROR: {message}";
|
||||||
|
LogEvent?.Invoke(this, logMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
@ -12,6 +13,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||||
using Ookii.Dialogs.Wpf;
|
using Ookii.Dialogs.Wpf;
|
||||||
using S7Explorer.Models;
|
using S7Explorer.Models;
|
||||||
using S7Explorer.Parsers;
|
using S7Explorer.Parsers;
|
||||||
|
using S7Explorer.Services;
|
||||||
|
|
||||||
namespace S7Explorer.ViewModels
|
namespace S7Explorer.ViewModels
|
||||||
{
|
{
|
||||||
|
@ -24,6 +26,9 @@ namespace S7Explorer.ViewModels
|
||||||
private ObservableCollection<S7Object> _projectStructure;
|
private ObservableCollection<S7Object> _projectStructure;
|
||||||
private string _projectInfo;
|
private string _projectInfo;
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
|
private string _logText;
|
||||||
|
private bool _useCache = true;
|
||||||
|
private S7ParserService _parserService;
|
||||||
|
|
||||||
public string SearchText
|
public string SearchText
|
||||||
{
|
{
|
||||||
|
@ -57,6 +62,18 @@ namespace S7Explorer.ViewModels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string LogText
|
||||||
|
{
|
||||||
|
get => _logText;
|
||||||
|
set => SetProperty(ref _logText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UseCache
|
||||||
|
{
|
||||||
|
get => _useCache;
|
||||||
|
set => SetProperty(ref _useCache, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Actualiza la información de interfaz de una función seleccionada
|
/// Actualiza la información de interfaz de una función seleccionada
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -67,14 +84,12 @@ namespace S7Explorer.ViewModels
|
||||||
// Aquí podríamos cargar información adicional específica de la función
|
// Aquí podríamos cargar información adicional específica de la función
|
||||||
// Por ejemplo, cargar el código de la función, detalles de parámetros, etc.
|
// Por ejemplo, cargar el código de la función, detalles de parámetros, etc.
|
||||||
|
|
||||||
// Actualizar estadísticas o análisis de la función
|
|
||||||
// Por ejemplo, mostrar uso de la función en otros bloques
|
|
||||||
|
|
||||||
// Este método se ejecutará cuando el usuario seleccione una FC en el árbol
|
// Este método se ejecutará cuando el usuario seleccione una FC en el árbol
|
||||||
|
LogInfo($"Seleccionada función: {fc.Name} ({fc.Number})");
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Ignorar errores para no interrumpir la experiencia del usuario
|
LogError($"Error al actualizar interfaz de función: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,13 +113,37 @@ namespace S7Explorer.ViewModels
|
||||||
|
|
||||||
public IRelayCommand OpenProjectCommand { get; }
|
public IRelayCommand OpenProjectCommand { get; }
|
||||||
public IRelayCommand SearchCommand { get; }
|
public IRelayCommand SearchCommand { get; }
|
||||||
|
public IRelayCommand ClearLogCommand { get; }
|
||||||
|
public IRelayCommand<string> RefreshProjectCommand { get; } // Cambiado a string
|
||||||
|
public IRelayCommand CancelLoadingCommand { get; }
|
||||||
|
|
||||||
public MainViewModel()
|
public MainViewModel()
|
||||||
{
|
{
|
||||||
ProjectStructure = new ObservableCollection<S7Object>();
|
ProjectStructure = new ObservableCollection<S7Object>();
|
||||||
OpenProjectCommand = new RelayCommand(OpenProject);
|
OpenProjectCommand = new RelayCommand(OpenProject);
|
||||||
SearchCommand = new RelayCommand(SearchInProject);
|
SearchCommand = new RelayCommand(SearchInProject);
|
||||||
|
ClearLogCommand = new RelayCommand(ClearLog);
|
||||||
|
RefreshProjectCommand = new RelayCommand<string>(RefreshProject); // Cambiado a string
|
||||||
|
CancelLoadingCommand = new RelayCommand(CancelLoading);
|
||||||
|
|
||||||
|
_parserService = new S7ParserService();
|
||||||
|
_parserService.LogEvent += OnParserLogEvent;
|
||||||
|
_parserService.StructureUpdatedEvent += OnStructureUpdated;
|
||||||
|
|
||||||
ProjectInfo = "No hay proyecto abierto";
|
ProjectInfo = "No hay proyecto abierto";
|
||||||
|
LogText = "S7 Project Explorer iniciado. Versión 1.0\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshProject(string useCacheString)
|
||||||
|
{
|
||||||
|
// Convertir el string a booleano
|
||||||
|
bool useCache = useCacheString == "True";
|
||||||
|
|
||||||
|
if (_currentProject != null)
|
||||||
|
{
|
||||||
|
UseCache = useCache;
|
||||||
|
LoadProjectAsync(_currentProject.FilePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenProject()
|
private void OpenProject()
|
||||||
|
@ -122,33 +161,77 @@ namespace S7Explorer.ViewModels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshProject(bool useCache)
|
||||||
|
{
|
||||||
|
if (_currentProject != null)
|
||||||
|
{
|
||||||
|
UseCache = useCache;
|
||||||
|
LoadProjectAsync(_currentProject.FilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelLoading()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsLoading)
|
||||||
|
{
|
||||||
|
LogWarning("Cancelando operación de carga por petición del usuario...");
|
||||||
|
_parserService.CancelParsing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError($"Error al cancelar la operación: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void LoadProjectAsync(string filePath)
|
private async void LoadProjectAsync(string filePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
ProjectInfo = "Cargando proyecto...";
|
ProjectInfo = "Cargando proyecto...";
|
||||||
|
LogInfo($"Iniciando carga del proyecto: {filePath}");
|
||||||
|
|
||||||
// Reiniciar estado
|
// Reiniciar estado
|
||||||
ProjectStructure.Clear();
|
ProjectStructure.Clear();
|
||||||
_currentProject = null;
|
_currentProject = null;
|
||||||
|
|
||||||
// Parsear proyecto
|
// Parsear proyecto
|
||||||
var parser = new S7ProjectParser(filePath);
|
_currentProject = await _parserService.ParseProjectAsync(filePath, UseCache);
|
||||||
_currentProject = await parser.ParseProjectAsync();
|
|
||||||
|
|
||||||
// Actualizar UI
|
// Actualizar UI
|
||||||
ProjectStructure.Add(_currentProject);
|
ProjectStructure.Add(_currentProject);
|
||||||
|
|
||||||
|
// Expandir nodo raíz si no estamos cargando desde caché (ya que la caché preserva el estado de expansión)
|
||||||
|
if (!UseCache)
|
||||||
|
{
|
||||||
_currentProject.IsExpanded = true;
|
_currentProject.IsExpanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Actualizar info
|
// Actualizar info
|
||||||
ProjectInfo = $"Proyecto: {Path.GetFileNameWithoutExtension(filePath)}";
|
string projectName = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
ProjectInfo = $"Proyecto: {projectName}";
|
||||||
|
LogInfo($"Proyecto cargado: {projectName}");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// La operación fue cancelada
|
||||||
|
ProjectInfo = "Carga cancelada";
|
||||||
|
LogWarning("Operación de carga cancelada por el usuario");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"Error al cargar el proyecto: {ex.Message}", "Error",
|
MessageBox.Show($"Error al cargar el proyecto: {ex.Message}", "Error",
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
ProjectInfo = "Error al cargar el proyecto";
|
ProjectInfo = "Error al cargar el proyecto";
|
||||||
|
LogError($"Error al cargar proyecto: {ex.Message}");
|
||||||
|
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
LogError($" Detalle: {ex.InnerException.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
@ -156,6 +239,24 @@ namespace S7Explorer.ViewModels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnParserLogEvent(object sender, string message)
|
||||||
|
{
|
||||||
|
// Este evento se desencadena en un hilo de fondo, así que aseguramos
|
||||||
|
// que se ejecute en el hilo de la UI
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
LogText += message + Environment.NewLine;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStructureUpdated(object sender, S7Object updatedObject)
|
||||||
|
{
|
||||||
|
// Este evento ya debe estar en el hilo de la UI
|
||||||
|
// No necesitamos hacer nada especial aquí, ya que la actualización
|
||||||
|
// se refleja automáticamente en el árbol gracias a la implementación de INPC
|
||||||
|
// y ObservableCollection
|
||||||
|
}
|
||||||
|
|
||||||
private void SearchInProject()
|
private void SearchInProject()
|
||||||
{
|
{
|
||||||
if (_currentProject == null || string.IsNullOrWhiteSpace(SearchText))
|
if (_currentProject == null || string.IsNullOrWhiteSpace(SearchText))
|
||||||
|
@ -163,6 +264,8 @@ namespace S7Explorer.ViewModels
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
LogInfo($"Buscando: \"{SearchText}\"");
|
||||||
|
|
||||||
// Recopilar todos los objetos en el proyecto
|
// Recopilar todos los objetos en el proyecto
|
||||||
var allObjects = GetAllObjects(_currentProject);
|
var allObjects = GetAllObjects(_currentProject);
|
||||||
|
|
||||||
|
@ -180,7 +283,7 @@ namespace S7Explorer.ViewModels
|
||||||
{
|
{
|
||||||
// Si es una función específica (por ejemplo "fc5")
|
// Si es una función específica (por ejemplo "fc5")
|
||||||
if (obj is S7Function func &&
|
if (obj is S7Function func &&
|
||||||
func.Number.ToLowerInvariant() == searchText)
|
func.Number?.ToLowerInvariant() == searchText)
|
||||||
{
|
{
|
||||||
matchingObjects.Add(func);
|
matchingObjects.Add(func);
|
||||||
}
|
}
|
||||||
|
@ -205,9 +308,9 @@ namespace S7Explorer.ViewModels
|
||||||
// Buscar en los parámetros de la función
|
// Buscar en los parámetros de la función
|
||||||
foreach (var param in func.Parameters)
|
foreach (var param in func.Parameters)
|
||||||
{
|
{
|
||||||
if (param.Name.ToLowerInvariant().Contains(searchText) ||
|
if (param.Name?.ToLowerInvariant().Contains(searchText) == true ||
|
||||||
param.DataType.ToLowerInvariant().Contains(searchText) ||
|
param.DataType?.ToLowerInvariant().Contains(searchText) == true ||
|
||||||
(param.Description?.ToLowerInvariant().Contains(searchText) ?? false))
|
(param.Description?.ToLowerInvariant().Contains(searchText) == true))
|
||||||
{
|
{
|
||||||
matchingObjects.Add(func);
|
matchingObjects.Add(func);
|
||||||
break;
|
break;
|
||||||
|
@ -224,7 +327,8 @@ namespace S7Explorer.ViewModels
|
||||||
|
|
||||||
if (matchingObjects.Count == 0)
|
if (matchingObjects.Count == 0)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"No se encontraron coincidencias para: {SearchText}",
|
LogInfo($"No se encontraron coincidencias para: \"{SearchText}\"");
|
||||||
|
MessageBox.Show($"No se encontraron coincidencias para: \"{SearchText}\"",
|
||||||
"Búsqueda", MessageBoxButton.OK, MessageBoxImage.Information);
|
"Búsqueda", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -232,10 +336,12 @@ namespace S7Explorer.ViewModels
|
||||||
// Seleccionar el primer objeto coincidente y expandir su ruta
|
// Seleccionar el primer objeto coincidente y expandir su ruta
|
||||||
var firstMatch = matchingObjects.First();
|
var firstMatch = matchingObjects.First();
|
||||||
SelectAndExpandToObject(firstMatch);
|
SelectAndExpandToObject(firstMatch);
|
||||||
|
LogInfo($"Encontrado: {firstMatch.Name} ({firstMatch.ObjectType})");
|
||||||
|
|
||||||
// Informar al usuario
|
// Informar al usuario
|
||||||
if (matchingObjects.Count > 1)
|
if (matchingObjects.Count > 1)
|
||||||
{
|
{
|
||||||
|
LogInfo($"Se encontraron {matchingObjects.Count} coincidencias en total.");
|
||||||
MessageBox.Show($"Se encontraron {matchingObjects.Count} coincidencias. " +
|
MessageBox.Show($"Se encontraron {matchingObjects.Count} coincidencias. " +
|
||||||
"Mostrando la primera coincidencia.",
|
"Mostrando la primera coincidencia.",
|
||||||
"Búsqueda", MessageBoxButton.OK, MessageBoxImage.Information);
|
"Búsqueda", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
@ -243,6 +349,7 @@ namespace S7Explorer.ViewModels
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
LogError($"Error durante la búsqueda: {ex.Message}");
|
||||||
MessageBox.Show($"Error durante la búsqueda: {ex.Message}",
|
MessageBox.Show($"Error durante la búsqueda: {ex.Message}",
|
||||||
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
|
@ -276,5 +383,39 @@ namespace S7Explorer.ViewModels
|
||||||
// Seleccionar el objeto
|
// Seleccionar el objeto
|
||||||
SelectedTreeItem = obj;
|
SelectedTreeItem = obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ClearLog()
|
||||||
|
{
|
||||||
|
LogText = string.Empty;
|
||||||
|
LogInfo("Log limpiado por el usuario");
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Logging Methods
|
||||||
|
|
||||||
|
public void LogInfo(string message)
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
LogText += $"[{timestamp}] INFO: {message}{Environment.NewLine}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogWarning(string message)
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
LogText += $"[{timestamp}] ADVERTENCIA: {message}{Environment.NewLine}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogError(string message)
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
LogText += $"[{timestamp}] ERROR: {message}{Environment.NewLine}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogDebug(string message)
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
LogText += $"[{timestamp}] DEBUG: {message}{Environment.NewLine}";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,17 +2,26 @@
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" xmlns:local="clr-namespace:S7Explorer" mc:Ignorable="d"
|
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" xmlns:local="clr-namespace:S7Explorer"
|
||||||
Title="S7 Project Explorer" Height="600" Width="900">
|
xmlns:sys="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d" Title="S7 Project Explorer" Height="700"
|
||||||
|
Width="1000">
|
||||||
|
<Window.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="150" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<Grid Grid.Row="0" Margin="8">
|
<Grid Grid.Row="0" Margin="8">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
@ -22,12 +31,20 @@
|
||||||
<Button Grid.Column="0" Content="Abrir Proyecto" Command="{Binding OpenProjectCommand}" Padding="8,4"
|
<Button Grid.Column="0" Content="Abrir Proyecto" Command="{Binding OpenProjectCommand}" Padding="8,4"
|
||||||
Margin="0,0,8,0" />
|
Margin="0,0,8,0" />
|
||||||
|
|
||||||
<TextBox Grid.Column="1" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" Padding="4"
|
<Button Grid.Column="1" Content="Recargar" Command="{Binding RefreshProjectCommand}"
|
||||||
|
CommandParameter="{x:Static sys:Boolean.TrueString}" Padding="8,4" Margin="0,0,4,0"
|
||||||
|
ToolTip="Recargar desde caché" />
|
||||||
|
|
||||||
|
<Button Grid.Column="2" Content="Recargar Completo" Command="{Binding RefreshProjectCommand}"
|
||||||
|
CommandParameter="{x:Static sys:Boolean.FalseString}" Padding="8,4" Margin="0,0,8,0"
|
||||||
|
ToolTip="Reprocesar el proyecto" />
|
||||||
|
|
||||||
|
<TextBox Grid.Column="3" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" Padding="4"
|
||||||
Margin="0,0,8,0" VerticalContentAlignment="Center" KeyDown="SearchBox_KeyDown" />
|
Margin="0,0,8,0" VerticalContentAlignment="Center" KeyDown="SearchBox_KeyDown" />
|
||||||
|
|
||||||
<Button Grid.Column="2" Content="Buscar" Command="{Binding SearchCommand}" Padding="8,4" Margin="0,0,8,0" />
|
<Button Grid.Column="4" Content="Buscar" Command="{Binding SearchCommand}" Padding="8,4" Margin="0,0,8,0" />
|
||||||
|
|
||||||
<TextBlock Grid.Column="3" Text="{Binding ProjectInfo}" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="5" Text="{Binding ProjectInfo}" VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
|
@ -63,5 +80,44 @@
|
||||||
<xctk:PropertyGrid Grid.Column="2" Margin="0,0,8,8" SelectedObject="{Binding SelectedObject}"
|
<xctk:PropertyGrid Grid.Column="2" Margin="0,0,8,8" SelectedObject="{Binding SelectedObject}"
|
||||||
AutoGenerateProperties="True" ShowSearchBox="True" ShowSortOptions="True" ShowTitle="True" />
|
AutoGenerateProperties="True" ShowSearchBox="True" ShowSortOptions="True" ShowTitle="True" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Log Section Splitter -->
|
||||||
|
<GridSplitter Grid.Row="2" Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
<!-- Log Section -->
|
||||||
|
<DockPanel Grid.Row="3" LastChildFill="True" Margin="8">
|
||||||
|
<DockPanel DockPanel.Dock="Top" LastChildFill="True" Margin="0,0,0,4">
|
||||||
|
<TextBlock Text="Log" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||||
|
|
||||||
|
<!-- Botón para cancelar operaciones -->
|
||||||
|
<Button Content="Cancelar" Command="{Binding CancelLoadingCommand}" Width="80" Margin="0,0,8,0"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisConverter}}" />
|
||||||
|
|
||||||
|
<!-- Indicador de carga -->
|
||||||
|
<TextBlock Text="Cargando..." VerticalAlignment="Center" Margin="0,0,8,0"
|
||||||
|
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisConverter}}" />
|
||||||
|
|
||||||
|
<Button Content="Limpiar" Command="{Binding ClearLogCommand}" Width="80" HorizontalAlignment="Right" />
|
||||||
|
</DockPanel>
|
||||||
|
<TextBox x:Name="LogTextBox" Text="{Binding LogText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
IsReadOnly="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
||||||
|
FontFamily="Consolas" Background="#F5F5F5" TextWrapping="NoWrap" />
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Overlay para mostrar progreso durante la carga -->
|
||||||
|
<Grid Grid.RowSpan="4" Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisConverter}}"
|
||||||
|
Background="Transparent" Panel.ZIndex="100">
|
||||||
|
<Border Padding="20" HorizontalAlignment="Center" VerticalAlignment="Center" Background="#80000000"
|
||||||
|
CornerRadius="10" Opacity="0.7">
|
||||||
|
<StackPanel Orientation="Vertical" Margin="10">
|
||||||
|
<TextBlock Text="Cargando proyecto..." Foreground="White" FontWeight="Bold" Margin="0,0,0,10"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
<ProgressBar IsIndeterminate="True" Height="20" Width="200" />
|
||||||
|
<Button Content="Cancelar" HorizontalAlignment="Center" Margin="0,10,0,0"
|
||||||
|
Command="{Binding CancelLoadingCommand}" Padding="10,5" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Windows;
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using S7Explorer.ViewModels;
|
using S7Explorer.ViewModels;
|
||||||
|
@ -13,6 +14,22 @@ namespace S7Explorer.Views
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContext = new MainViewModel();
|
DataContext = new MainViewModel();
|
||||||
|
|
||||||
|
// Configurar TextBox de log para Auto-scroll
|
||||||
|
SetupLogTextBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupLogTextBox()
|
||||||
|
{
|
||||||
|
// Auto-scroll al final del texto cuando se actualiza
|
||||||
|
if (LogTextBox != null)
|
||||||
|
{
|
||||||
|
DependencyPropertyDescriptor.FromProperty(TextBox.TextProperty, typeof(TextBox))
|
||||||
|
.AddValueChanged(LogTextBox, (s, e) =>
|
||||||
|
{
|
||||||
|
LogTextBox.ScrollToEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
||||||
|
@ -23,8 +40,6 @@ namespace S7Explorer.Views
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void SearchBox_KeyDown(object sender, KeyEventArgs e)
|
private void SearchBox_KeyDown(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == Key.Enter && ViewModel != null)
|
if (e.Key == Key.Enter && ViewModel != null)
|
||||||
|
|
Loading…
Reference in New Issue